diff --git a/404.html b/404.html index 6e1ff891..47cf0280 100644 --- a/404.html +++ b/404.html @@ -5,7 +5,7 @@ Multi-Scrobbler - + diff --git a/FAQ/index.html b/FAQ/index.html index 02e6adaa..aac3394f 100644 --- a/FAQ/index.html +++ b/FAQ/index.html @@ -3,11 +3,11 @@ -FAQ | Multi-Scrobbler +FAQ | Multi-Scrobbler - + - + diff --git a/assets/js/04d926a5.6d08a85e.js b/assets/js/04d926a5.6d08a85e.js deleted file mode 100644 index ef6d8dbd..00000000 --- a/assets/js/04d926a5.6d08a85e.js +++ /dev/null @@ -1,13 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["4461"],{7265(e,n,i){i.d(n,{Ay:()=>c,RM:()=>o});var s=i(4848),r=i(8453),t=i(2170),l=i.n(t);let o=[];function a(e){let n={a:"a",code:"code",li:"li",p:"p",strong:"strong",ul:"ul",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(n.p,{children:["A ",(0,s.jsx)(n.strong,{children:"Source"})," that monitors Listening Activity uses one of two metrics to determine if a Song is scrobbable based on your listening:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"percent"})," of track listened to (50% or more)"]}),"\n",(0,s.jsxs)(n.li,{children:["or amount of time (",(0,s.jsx)(n.strong,{children:"duration"}),") track was listened to (4 minutes or more)"]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["These default values are based on ",(0,s.jsx)(n.a,{href:"https://www.last.fm/api/scrobbling#when-is-a-scrobble-a-scrobble",children:"last.fm's scrobble guidance."})]}),"\n",(0,s.jsxs)(n.p,{children:["These values can be customized in the ",(0,s.jsx)(n.code,{children:"options"})," of Source's ",(0,s.jsx)(n.a,{href:"/configuration#configuration-types",children:"file or aio config"}),":"]}),"\n",(0,s.jsx)(l(),{language:"json5",title:`${e.filename}.json`,children:`{ - "data": { - // ... - }, - "options": { - // highlight-start - "scrobbleThresholds": { - "duration": 40, // scrobbable if listened to for 40 seconds or more - "percent": 20 // scrobbable if listened to for 20% or more of the track's length - } - // highlight-end - } -}`})]})}function c(e={}){let{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(a,{...e})}):a(e)}},9625(e,n,i){i.r(n),i.d(n,{metadata:()=>s,default:()=>f,frontMatter:()=>d,contentTitle:()=>h,toc:()=>x,assets:()=>u});var s=JSON.parse('{"id":"configuration/configuration","title":"Overview","description":"Configuration","source":"@site/docs/configuration/configuration.mdx","sourceDirName":"configuration","slug":"/configuration/","permalink":"/multi-scrobbler/configuration/","draft":false,"unlisted":false,"editUrl":"https:/github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/configuration/configuration.mdx?plain=1","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"sidebar_position":2,"title":"Overview","description":"Configuration","toc_max_heading_level":3},"sidebar":"tutorialSidebar","previous":{"title":"From < 0.14.0","permalink":"/multi-scrobbler/updating/upgrade-path/0140"},"next":{"title":"Overview","permalink":"/multi-scrobbler/configuration/clients/"}}'),r=i(4848),t=i(8453),l=i(8194),o=i(698),a=i(2170),c=i.n(a);i(2709),i(9461),i(8675),i(7265);let d={sidebar_position:2,title:"Overview",description:"Configuration",toc_max_heading_level:3},h,u={},x=[{value:"Configuration Types",id:"configuration-types",level:2},{value:"Setup Sources and Clients",id:"setup-sources-and-clients",level:2},{value:"Secrets Interpolation",id:"secrets-interpolation",level:2},{value:"Base URL",id:"base-url",level:2},{value:"Caching",id:"caching",level:2},{value:"Secondary Caching Store",id:"secondary-caching",level:3},{value:"Database",id:"database",level:2},{value:"Retention",id:"retention",level:3},{value:"Configuring Compaction Policy",id:"configuring-compaction-policy",level:4},{value:"Configuring Deletion Policy",id:"configuring-deletion-policy",level:4},{value:"Debug Mode",id:"debug-mode",level:2},{value:"Disable Web",id:"disable-web",level:2},{value:"Monitoring",id:"monitoring",level:2},{value:"Upstream Services Status",id:"upstream-services-status",level:3},{value:"Webhook Configurations",id:"webhook-configurations",level:3},{value:"Gotify",id:"gotify",level:4},{value:"Ntfy",id:"ntfy",level:4},{value:"Apprise",id:"apprise",level:4},{value:"Health Endpoint",id:"health-endpoint",level:3},{value:"Prometheus",id:"prometheus",level:3}];function j(e){let n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components},{Details:s,DetailsAdmo:a}=n;return s||p("Details",!0),a||p("DetailsAdmo",!0),(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["Check the ",(0,r.jsx)(n.a,{href:"/multi-scrobbler/FAQ",children:(0,r.jsx)(n.strong,{children:"FAQ"})})," if you have any issues after configuration!"]})}),"\n",(0,r.jsx)(n.h2,{id:"configuration-types",children:"Configuration Types"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/sources",children:(0,r.jsx)(n.strong,{children:"Sources"})})," and ",(0,r.jsx)(n.a,{href:"/configuration/clients",children:(0,r.jsx)(n.strong,{children:"Clients"})})," are configured using:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"environmental (ENV) variables"}),"\n",(0,r.jsx)(n.li,{children:"client/source specific json config files"}),"\n",(0,r.jsx)(n.li,{children:"an all-in-one json config file"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"MS will parse configuration from all configuration types."})," You can mix and match configurations but it is generally better to stick to one type."]}),"\n",(0,r.jsxs)(l.A,{groupId:"configType",queryString:!0,children:[(0,r.jsxs)(o.A,{value:"env",label:"ENV",children:[(0,r.jsx)(n.p,{children:"MS will parse environmental variables present in the OS/container when it is run."}),(0,r.jsxs)(n.p,{children:["This method means MS ",(0,r.jsx)(n.strong,{children:"does not require config files"})," but it will still create and use data files in the ",(0,r.jsx)(n.a,{href:"/installation/#config-and-data-directories",children:"local"})," or ",(0,r.jsx)(n.a,{href:"/installation/?dockerSetting=storage#recommended-settings",children:"docker"})," data directory."]}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Use ENV-based configuration if..."}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"You are the only person for whom MS is scrobbling for or"}),"\n",(0,r.jsxs)(n.li,{children:["You have a very simple setup for MS such as one scrobble ",(0,r.jsx)(n.a,{href:"/configuration/clients",children:"Client"})," and one ",(0,r.jsx)(n.a,{href:"/configuration/sources",children:"Source"})," IE Jellyfin -> Koito, or"]}),"\n",(0,r.jsx)(n.li,{children:"You are not comfortable editing JSON files"}),"\n"]})]}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Config Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n image: foxxmd/multi-scrobbler\n environment:\n - JELLYFIN_URL=192.168.0.110:8096\n - JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56b\n - JELLYFIN_USER=MyUser\n - JELLYFIN_ID=myJellyin\n # ...\n # ...\n"})}),(0,r.jsx)(n.p,{children:"For a local/node installation export variables before running..."}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-shell",children:"SPOTIFY_CLIENT_ID=yourId SPOTIFY_CLIENT_SECRET=yourSecret npm run start\n"})})]})]}),(0,r.jsxs)(o.A,{value:"file",label:"File",children:[(0,r.jsxs)(n.p,{children:["MS will parse configuration files located in the directory specified by the ",(0,r.jsx)(n.code,{children:"CONFIG_DIR"})," environmental variable. This variable defaults to:"]}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Local installation -> See ",(0,r.jsx)(n.a,{href:"/installation/#config-and-data-directories",children:"Config and Data Directories"})]}),"\n",(0,r.jsxs)(n.li,{children:["Docker -> ",(0,r.jsx)(n.code,{children:"/config"})," (in the container) -- see the ",(0,r.jsxs)(n.a,{href:"/installation/?dockerSetting=storage#recommended-settings",children:[(0,r.jsx)(n.strong,{children:"Storage"})," tab in the Docker installation docs"]})," for how to configure this correctly"]}),"\n"]}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Use File-based configuration if..."}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"You are comfortable editing JSON files"}),"\n",(0,r.jsxs)(n.li,{children:["You have more than one of the same type of ",(0,r.jsx)(n.strong,{children:"Source/Client"})," you want to use EX 2x Last.fm accounts, 3x Jellyfin accounts"]}),"\n",(0,r.jsxs)(n.li,{children:["You want activity from specific ",(0,r.jsx)(n.strong,{children:"Sources"})," to scrobble to specific ",(0,r.jsx)(n.strong,{children:"Clients"})]}),"\n",(0,r.jsxs)(n.li,{children:["You need to setup more advanced configuration for a Source/Client","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Most Source/Clients only support basic configuration through ENV, more configuration is possible using File. AIO enables complete configuration."}),"\n"]}),"\n"]}),"\n"]})]}),(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["There are ",(0,r.jsx)(n.strong,{children:"example configurations"})," for all Source/Client types and AIO config located in the ",(0,r.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/tree/master/config",children:(0,r.jsx)(n.code,{children:"/config"})})," directory of this project. These can be used as-is by renaming them to ",(0,r.jsx)(n.code,{children:".json"}),".","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["These are the same examples you will find in the ",(0,r.jsx)(n.strong,{children:"Configuration"})," section of each Source/Client"]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["There is also a ",(0,r.jsx)(n.a,{href:"/configuration/kitchensink",children:(0,r.jsx)(n.strong,{children:"kitchensink example"})})," that provides examples of using all sources/clients in a complex configuration."]}),"\n"]})}),(0,r.jsxs)(n.p,{children:["Each config file is named after the ",(0,r.jsx)(n.strong,{children:"type"})," of the Client/Source. Within each file, a JSON ",(0,r.jsx)(n.strong,{children:"array"})," defines all of the instances for that type of Client/Source."]}),(0,r.jsx)(n.p,{children:"Example directory structure:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"/CONFIG_DIR\n jellyfin.json\n lastfm.json\n koito.json\n"})}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Config Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="jellyfin.json"',children:'[\n {\n "name": "Foxx JF Server",\n "id": "foxxJf",\n "data": {\n "url": "http://localhost:8096",\n "user": "FoxxMD",\n "apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"\n }\n }\n]\n'})})]}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Multiple of Same Config Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="jellyfin.json"',children:'[\n {\n "name": "Foxx JF Server",\n "id": "foxxJf",\n "data": {\n "url": "http://localhost:8096",\n "user": "FoxxMD",\n "apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"\n }\n },\n // highlight-start\n {\n "name": "Foo\'s JF Server",\n "id": "fooJf",\n "data": {\n "url": "http://192.168.0.150:8096",\n "user": "foo",\n "apiKey": "9c5bb56bd6540c756fbf481ebd9c5c40"\n }\n }\n // highlight-end\n]\n'})})]})]}),(0,r.jsxs)(o.A,{value:"aio",label:"File AIO",children:[(0,r.jsxs)(n.p,{children:["MS will parse an ",(0,r.jsx)(n.strong,{children:"all-in-one"})," configuration file (",(0,r.jsx)(n.code,{children:"config.json"}),") located in the directory specified by the ",(0,r.jsx)(n.code,{children:"CONFIG_DIR"})," environmental variable. The file's location defaults to:"]}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Local installation -> See ",(0,r.jsx)(n.a,{href:"/installation/#config-and-data-directories",children:"Config and Data Directories"})]}),"\n",(0,r.jsxs)(n.li,{children:["Docker -> ",(0,r.jsx)(n.code,{children:"/config/config.json"})," (in the container) -- see the ",(0,r.jsxs)(n.a,{href:"/installation/?dockerSetting=storage#recommended-settings",children:[(0,r.jsx)(n.strong,{children:"Storage"})," tab in the Docker installation docs"]})," for how to configure this correctly"]}),"\n"]}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Use AIO-based configuration if..."}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["...All of the reasons listed in ",(0,r.jsx)(n.a,{href:"./?configType=file#configuration-types",children:"File"})]}),"\n",(0,r.jsxs)(n.li,{children:["You want to setup ",(0,r.jsx)(n.a,{href:"#monitoring",children:"monitoring/webhooks"}),", default settings for all Sources/Clients, or global config options"]}),"\n",(0,r.jsxs)(n.li,{children:["You want to use ",(0,r.jsx)(n.a,{href:"/configuration/transforms",children:"Transformers"})," (other than what is provided for ",(0,r.jsx)(n.a,{href:"/configuration/transforms/musicbrainz/#env-configuration",children:"Musicbrainz ENV"}),")"]}),"\n",(0,r.jsx)(n.li,{children:"You want complete control over all potential MS configuration"}),"\n"]})]}),(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:'AIO config is the "complete" way to configure MS.'}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["All settings available in File/ENV are available in AIO and AIO has ",(0,r.jsx)(n.em,{children:"more"})," exclusive settings."]}),"\n",(0,r.jsxs)(n.li,{children:["AIO enables configuring ",(0,r.jsx)(n.strong,{children:"default settings"})," for all Sources/Clients"]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["There is a ",(0,r.jsx)(n.a,{href:"/configuration/kitchensink",children:(0,r.jsx)(n.strong,{children:"kitchensink example"})})," that provides examples of using all sources/clients in a complex configuration."]}),"\n",(0,r.jsxs)(n.li,{children:["Use the ",(0,r.jsx)(n.a,{href:"/playground",children:(0,r.jsx)(n.strong,{children:"Config Playground"})})," to explore the entire config schema with an editable example","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["The example used can also be found in the repo directory at ",(0,r.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/tree/master/config/config.json.example",children:(0,r.jsx)(n.code,{children:"/config/config.json.example"})})," in the example below"]}),"\n"]}),"\n"]}),"\n"]})}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Config Example"}),(0,r.jsx)(c(),{title:"CONFIG_DIR/config.json",language:"json5",children:'{\n "debugMode": false,\n "disableWeb": false,\n "sourceDefaults": {\n "logPayload": false,\n "logFilterFailure": "warn",\n "logPlayerState": false,\n "scrobbleThresholds": {\n "duration": 30,\n "percent": 50\n },\n "maxPollRetries": 1,\n "maxRequestRetries": 1,\n "retryMultiplier": 1.5\n },\n "clientDefaults": {\n "maxRequestRetries": 1,\n "retryMultiplier": 1.5\n },\n "sources": [\n {\n "type": "spotify",\n "enable": true,\n "clients": ["myConfig"],\n "name": "Cool Spotify Name",\n "id": "mySpotifyId",\n "data": {\n "clientId": "a89cba1569901a0671d5a9875fed4be1",\n "clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",\n "redirectUri": "http://localhost:9078/callback"\n }\n }\n ],\n "clients": [\n {\n "type": "maloja",\n "enable": true,\n "name": "Foxx Maloja",\n "id": "myMalojaId",\n "data": {\n "url": "http://localhost:42010",\n "apiKey": "myMalojaKey"\n }\n }\n ],\n "webhooks": [\n {\n "name": "FirstGotifyServer",\n "type": "gotify",\n "url": "http://localhost:8070",\n "token": "MyGotifyToken",\n "priorities": {\n "info": 5,\n "warn": 7,\n "error": 10\n }\n },\n {\n "type": "ntfy",\n "name": "MyNtfyFriendlyNameForLogs",\n "url": "http://localhost:9991",\n "topic": "MyMultiScrobblerTopic",\n "username": "Optional",\n "password": "Optional",\n "priorities": {\n "info": 3,\n "warn": 4,\n "error": 5\n }\n }\n ]\n}\n'})]}),(0,r.jsxs)(a,{type:"important",summary:"Source/Client AIO Configs require a 'type' property",children:[(0,r.jsxs)(n.p,{children:["Compared to their ",(0,r.jsx)(n.a,{href:"./?configType=file#configuration-types",children:"File"})," equivalent, Sources/Clients configured using AIO require an additional ",(0,r.jsx)(n.code,{children:"type"})," property."]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="jellyfin.json"',children:'[\n {\n "name": "Foxx JF Server",\n // highlight-start\n "type: "jellyfin",\n // highlight-end\n "id": "foxxJf",\n "data": {\n "url": "http://localhost:8096",\n "user": "FoxxMD",\n "apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"\n }\n }\n]\n'})}),(0,r.jsxs)(n.p,{children:["You can find an example of the required ",(0,r.jsx)(n.code,{children:"type"})," value by referring to the ",(0,r.jsx)(n.strong,{children:"AIO"})," tab in each Source/Client's Configuration section example."]})]})]})]}),"\n",(0,r.jsx)(n.h2,{id:"setup-sources-and-clients",children:"Setup Sources and Clients"}),"\n",(0,r.jsxs)(n.p,{children:["See the ",(0,r.jsx)(n.a,{href:"#configuration-types",children:"Configuration Types"})," above for your options for creating Source and Client configurations."]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Reference ",(0,r.jsx)(n.a,{href:"/configuration/sources",children:(0,r.jsx)(n.strong,{children:"Scrobble Sources"})})," for what Sources are available"]}),"\n",(0,r.jsxs)(n.li,{children:["Reference ",(0,r.jsx)(n.a,{href:"/configuration/clients",children:(0,r.jsx)(n.strong,{children:"Scrobble Clients"})})," for what Clients are available"]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Each entry for a Source/Client includes a ",(0,r.jsx)(n.strong,{children:"Configuration"})," section that describes how to configure it using a ",(0,r.jsx)(n.a,{href:"#configuration-types",children:"configuration type"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"secrets-interpolation",children:"Secrets Interpolation"}),"\n",(0,r.jsxs)(n.p,{children:["When using ",(0,r.jsx)(n.a,{href:"./?configType=file#configuration-types",children:"File"})," or ",(0,r.jsx)(n.a,{href:"./?configType=aio#configuration-types",children:"AIO"})," Configuration, Multi-Scrobbler can interpolate Environmental Variables into your json files. This can be used, for example, to keep sensitive data (like ",(0,r.jsx)(n.a,{href:"/configuration/clients/lastfm#configuration",children:"Last.fm Client/Secret"}),") out of your configuration files so that they can be committed to git."]}),"\n",(0,r.jsxs)(n.p,{children:["Multi-scrobbler will look for patterns in text fields within ",(0,r.jsx)(n.em,{children:"all"})," of your json files:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Some part of the text field matches: ",(0,r.jsx)(n.code,{children:"[[MY_ENV]]"})]}),"\n",(0,r.jsxs)(n.li,{children:["Is replaced by the value of the Environmental Variable named ",(0,r.jsx)(n.code,{children:"MY_ENV"})]}),"\n"]}),"\n",(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(n.p,{children:["Given this ",(0,r.jsx)(n.a,{href:"/configuration/clients/lastfm#configuration",children:"Last.fm"})," ",(0,r.jsx)(n.a,{href:"./?configType=file#configuration-types",children:"File"})," config:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="lastfm.json"',children:'[\n {\n "name": "myLastFmClient",\n "configureAs": "client",\n "data": {\n "apiKey": "[[MY_APIKEY]]",\n "secret": "[[MY_SECRET]]",\n "redirectUri": "http://localhost:9078/lastfm/callback"\n }\n }\n]\n'})}),(0,r.jsxs)(n.p,{children:["And these environmental variables, in this scenario set through ",(0,r.jsx)(n.code,{children:"environment"})," in the ",(0,r.jsx)(n.a,{href:"/quickstart#create-docker-compose-file",children:"docker compose installation"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"MY_APIKEY=a89cba1569901a0671d5a9875fed4be1\nMY_SECRET=ec42e09d5ae0ee0f0816ca151008412a\n"})}),(0,r.jsx)(n.p,{children:"The resulting json multi-scrobbler would use:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="lastfm.json"',children:'[\n {\n "name": "myLastFmClient",\n "configureAs": "client",\n "data": {\n "apiKey": "a89cba1569901a0671d5a9875fed4be1",\n "secret": "ec42e09d5ae0ee0f0816ca151008412a",\n "redirectUri": "http://localhost:9078/lastfm/callback"\n }\n }\n]\n'})})]}),"\n",(0,r.jsx)(a,{type:"note",summary:"Caveats",children:(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"ENV variable names/interpolation keys are case-insensitive"}),"\n",(0,r.jsxs)(n.li,{children:["Interpolation only works for ",(0,r.jsx)(n.strong,{children:"string"})," values within json. This cannot be used for numbers, booleans, objects, etc..."]}),"\n"]})}),"\n",(0,r.jsxs)(a,{type:"warning",summary:"Missing ENVs",children:[(0,r.jsxs)(n.p,{children:["Multi-scrobbler ",(0,r.jsx)(n.strong,{children:"will not"})," throw an error if the environmental value is not found. Instead, it will leave the string as-is and log a warning (",(0,r.jsx)(n.code,{children:"WARN"})," level) with the names of the missing environmental variable names like so:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"WARN : [App] [Sources] [spotify Secrets] Matched: None | Unmatched: SPOTIFY_SECRET\n"})})]}),"\n",(0,r.jsx)(a,{type:"warning",summary:"ENV Name Collisions",children:(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Verify that interpolation keys/environmental variable names you will use do not collide with existing ENV names used by multi-scrobbler."})," Use the docs search to verify the name you want to use is not already used elsewhere by multi-scrobbler."]})}),"\n",(0,r.jsx)(n.h2,{id:"base-url",children:"Base URL"}),"\n",(0,r.jsxs)(n.p,{children:["Defines the URL that is used to generate default redirect URLs for authentication for services like ",(0,r.jsx)(n.a,{href:"/configuration/sources/spotify",children:"spotify"})," and ",(0,r.jsx)(n.a,{href:"/configuration/clients/lastfm",children:"lastfm"})," -- as well as some logging hints."]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Default => ",(0,r.jsx)(n.code,{children:"http://localhost:9078"})]}),"\n",(0,r.jsxs)(n.li,{children:["Set with ",(0,r.jsx)(n.a,{href:"./?configType=env#configuration-types",children:"ENV"})," ",(0,r.jsx)(n.code,{children:"BASE_URL"})," or ",(0,r.jsx)(n.code,{children:"baseUrl"})," ",(0,r.jsx)(n.a,{href:"./?configType=aio#configuration-types",children:"all-in-one configuration"})]}),"\n",(0,r.jsxs)(n.li,{children:["If protocol is ",(0,r.jsx)(n.code,{children:"http"})," or no protocol is specified MS will try to use port ",(0,r.jsx)(n.code,{children:"9078"})," -- to override this explicitly set the port or use ",(0,r.jsx)(n.code,{children:"https"})]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Useful when running with ",(0,r.jsx)(n.a,{href:"/multi-scrobbler/installation/#docker",children:"docker"})," so that you do not need to specify redirect URLs for each configuration."]}),"\n",(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(n.p,{children:["EX Lastfm Redirect Url is ",(0,r.jsx)(n.code,{children:"BASE_URL:PORT/lastfm/callback"})," (when no other redirectUri is specified for ",(0,r.jsx)(n.a,{href:"/configuration/clients/lastfm",children:"lastfm configuration"}),")"]}),(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:(0,r.jsx)(n.code,{children:"BASE_URL"})}),(0,r.jsx)(n.th,{children:"Redirect URL"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"192.168.0.101"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"http://192.168.0.101:9078/lastfm/callback"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"http://my.domain.local"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"http://my.domain.local:9078/lastfm/callback"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"http://192.168.0.101/my/subfolder"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"http://192.168.0.101:9078/my/subfolder/lastfm/callback"})})]})]})]}),(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:(0,r.jsx)(n.code,{children:"BASE_URL"})}),(0,r.jsx)(n.th,{children:"Redirect URL"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"my.domain.local:80"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"http://192.168.0.101:80/lastfm/callback"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"my.domain.local:9000"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"http://my.domain.local:9000/lastfm/callback"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"192.168.0.101:4000/my/subfolder"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"http://192.168.0.101:4000/my/subfolder/lastfm/callback"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"https://192.168.0.101"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"https://192.168.0.101:443/lastfm/callback"})})]})]})]})]}),"\n",(0,r.jsx)(n.h2,{id:"caching",children:"Caching"}),"\n",(0,r.jsx)(n.p,{children:"Multi-scrobbler implements caching to reduce external API calls and make some actions faster."}),"\n",(0,r.jsxs)(n.p,{children:["A default ",(0,r.jsx)(n.strong,{children:"in-memory"})," cache store is used so that you always benefit from some caching. An optional, ",(0,r.jsxs)(n.a,{href:"#secondary-caching",children:[(0,r.jsx)(n.strong,{children:"secondary"})," store"]})," can be configured for greater caching capabilities."]}),"\n",(0,r.jsx)(a,{type:"note",summary:"What is Cached?",children:(0,r.jsxs)(l.A,{groupId:"cachedThings",children:[(0,r.jsx)(o.A,{value:"auth",label:"Auth Data",children:(0,r.jsx)(n.p,{children:"Authentication sessions/tokens/etc... are cached for quicker requests and for persistence across restarts."})}),(0,r.jsx)(o.A,{value:"transform",label:"Scrobble Transforms",children:(0,r.jsxs)(n.p,{children:["The results of ",(0,r.jsx)(n.a,{href:"/configuration/transforms",children:"transform rules"})," are cached so that if a scrobble with identical data (track/artists/album) is identified and it has the same set of transform rules then the cached transform results can be applied."]})}),(0,r.jsx)(o.A,{value:"metadata",label:"Transform API Calls",children:(0,r.jsxs)(n.p,{children:["API Calls to external (metadata) services used to ",(0,r.jsx)(n.a,{href:"/configuration/transforms",children:"Enhance Scrobbles"}),", like calls to ",(0,r.jsx)(n.a,{href:"/configuration/transforms/musicbrainz",children:"Musicbrainz"}),", can be cached to avoid duplicate calls and speed up scrobble transformations."]})})]})}),"\n",(0,r.jsxs)(a,{type:"important",summary:"Auth Cache Configuration",children:[(0,r.jsxs)(n.p,{children:["Auth caching defaults to a ",(0,r.jsx)(n.strong,{children:"file"})," that is stored in the ",(0,r.jsx)(n.code,{children:"DATA_DIR"})," directory using the pre-defined file name ",(0,r.jsx)(n.code,{children:"ms-auth.cache"}),"."]}),(0,r.jsxs)(n.p,{children:["This provides automatic persistence across restarts for long-lived auth data/credentials if you have configured a ",(0,r.jsx)(n.a,{href:"/installation?dockerSetting=storage#recommended-settings",children:"persisted volume/bind mount"})," for configuration (",(0,r.jsx)(n.code,{children:"/config"})," is mounted in ",(0,r.jsx)(n.a,{href:"/quickstart#create-docker-compose-file",children:"docker compose"}),")."]}),(0,r.jsxs)(n.p,{children:["If you wish to use the ",(0,r.jsx)(n.a,{href:"#secondary-caching",children:"secondary store"})," for caching Auth you must explicitly configure it. This is because valkey can potentially be ",(0,r.jsx)(n.em,{children:"ephemeral"})," if you do not provide a volume for its data directory."]}),(0,r.jsx)(n.p,{children:"To explicitly configure auth to use the secondary store:"}),(0,r.jsxs)(l.A,{groupId:"configType",queryString:!0,children:[(0,r.jsx)(o.A,{value:"env",label:"ENV",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="compose.yaml"',children:"services:\n multi-scrobbler:\n # ...\n environment:\n // highlight-start\n - CACHE_AUTH=valkey\n // highlight-end\n # ...\n"})})}),(0,r.jsx)(o.A,{value:"aio",label:"AIO",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n "cache": {\n "auth": {\n "provider": "valkey"\n }\n },\n // ...\n}\n'})})})]})]}),"\n",(0,r.jsx)(n.h3,{id:"secondary-caching",children:"Secondary Caching Store"}),"\n",(0,r.jsx)(n.p,{children:"Using a secondary store enables:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"persistence of cached data across restarts"}),"\n",(0,r.jsx)(n.li,{children:"a larger store (more data is saved)"}),"\n",(0,r.jsx)(n.li,{children:"a longer time-to-live in the store (cached data is fetchable for a longer period)"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["These benefits are particularly beneficial when using ",(0,r.jsx)(n.a,{href:"/configuration/transforms",children:"transforms"})," like Musicbrainz and it is ",(0,r.jsx)(n.strong,{children:"strongly recommended"})," for these scenarios."]}),"\n",(0,r.jsxs)(n.p,{children:["Currently, Multi-scrobbler only supports ",(0,r.jsx)(n.a,{href:"https://valkey.io/",children:(0,r.jsx)(n.strong,{children:"Valkey"})}),", an open-source fork of Redis, as a secondary store."]}),"\n",(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Adding Valkey container to multi-scrobbler"}),(0,r.jsxs)(n.p,{children:["A valkey container can be added to the ",(0,r.jsx)(n.a,{href:"/installation?runType=docker-compose#docker",children:"multi-scrobbler docker compose stack"}),":"]}),(0,r.jsxs)(l.A,{children:[(0,r.jsx)(o.A,{value:"named",label:"Named Volume",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n # ...\n // highlight-start\n valkey:\n image: valkey/valkey\n volumes:\n - valkeydata:/data\n\nvolumes:\n valkeydata:\n driver: local\n // highlight-end\n"})})}),(0,r.jsx)(o.A,{value:"bind",label:"Bind Mount",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n # ...\n // highlight-start\n valkey:\n image: valkey/valkey\n volumes:\n - ./valkeyData:/data\n // highlight-end\n"})})})]}),(0,r.jsxs)(n.p,{children:["Use ",(0,r.jsx)(n.code,{children:"redis://valkey:6379"})," as the connection string in the configurations below."]})]}),"\n",(0,r.jsx)(n.p,{children:"The connection string used by multi-scrobbler to connect to your Valkey instance must be in the form:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"redis://HOST_IP:HOST_PORT\n"})}),"\n",(0,r.jsxs)(l.A,{groupId:"configType",queryString:!0,children:[(0,r.jsx)(o.A,{value:"env",label:"ENV",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="compose.yaml"',children:"services:\n multi-scrobbler:\n # ...\n environment:\n // highlight-start\n - CACHE_VALKEY=redis://192.168.0.120:6379\n // highlight-end\n # ...\n"})})}),(0,r.jsxs)(o.A,{value:"aio",label:"AIO",children:[(0,r.jsx)(n.p,{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n "cache": {\n "valkey": "redis://192.168.0.120:6379"\n },\n // ...\n}\n'})})]})]}),"\n",(0,r.jsx)(n.h2,{id:"database",children:"Database"}),"\n",(0,r.jsxs)(n.p,{children:["Multi-scrobbler depends on a SQLite database (",(0,r.jsx)(n.code,{children:"ms.db"}),") that is created on first run and stored in the ",(0,r.jsx)(n.a,{href:"/installation/?dockerSetting=storage#recommended-settings",children:(0,r.jsx)(n.code,{children:"CONFIG_DIR"})}),". When upgrading Multi-scrobbler version, if there are any required database changes than this database is ",(0,r.jsx)(n.a,{href:"/updating#database",children:"automatically backed up and migrated."})]}),"\n",(0,r.jsxs)(n.p,{children:["The database stores ",(0,r.jsx)(n.em,{children:"all"})," Plays for your Sources/Clients as well as metadata and debugging information to help troubleshoot issues. Each Play is associated with a Source/Config in the database based on your configuration."]}),"\n",(0,r.jsxs)(n.p,{children:["You ",(0,r.jsx)(n.strong,{children:"must set IDs for each Source/Client"})," so that the database can identify these even when the configuration is changed."]}),"\n",(0,r.jsx)(n.h3,{id:"retention",children:"Retention"}),"\n",(0,r.jsxs)(a,{status:"note",summary:"How much storage does the database use?",children:[(0,r.jsx)(n.p,{children:"The amount of data stored for each Play can widely vary based on a few factors:"}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"how much data the Source service exposes"}),"\n",(0,r.jsx)(n.li,{children:"how many clients each Source is scrobble to (Plays are duplicated for each Client a Source sends a scrobble to)"}),"\n",(0,r.jsxs)(n.li,{children:["if you are using any ",(0,r.jsx)(n.a,{href:"/configuration/transforms",children:"Transforms"})," the diff of each step is stored, along with any external request/response data (like ",(0,r.jsx)(n.a,{href:"/configuration/transforms/musicbrainz",children:"Musicbrainz queries"}),")"]}),"\n"]}),(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/tests/database/drizzle.test.ts#L551",children:"MS repository contains a benchmark"})," to measure an average database size in a few common scenarios."]}),(0,r.jsxs)(l.A,{groupId:"dbSize",children:[(0,r.jsxs)(o.A,{label:"Minimal",value:"minimal",children:[(0,r.jsx)(n.p,{children:"Assuming your Sources send a minimal amount of data or you have compacted all plays:"}),(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Play Count"}),(0,r.jsx)(n.th,{children:"DB Size"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"100"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"160kb"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"1000"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"1MB"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"10k"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"10.2MB"})})]})]})]})]}),(0,r.jsxs)(o.A,{label:"With Original Input",value:"input",children:[(0,r.jsx)(n.p,{children:"Assuming your Sources have a non-trivial amount of input data (like Spotify or Listenbrainz) that is not compacted:"}),(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Play Count"}),(0,r.jsx)(n.th,{children:"DB Size"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"100"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"356kb"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"1000"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"3MB"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"10k"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"29.6MB"})})]})]})]})]}),(0,r.jsxs)(o.A,{label:"All Debug Deta",value:"all",children:[(0,r.jsx)(n.p,{children:"Assuming your Sources/Clients:"}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"have a non-trivial amount of input data (like Spotify or Listenbrainz)"}),"\n",(0,r.jsxs)(n.li,{children:["and has many ",(0,r.jsx)(n.a,{href:"/configuration/transforms",children:"Transforms"})," steps that include requests"]}),"\n",(0,r.jsx)(n.li,{children:"nothing is compacted"}),"\n"]}),(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Play Count"}),(0,r.jsx)(n.th,{children:"DB Size"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"100"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"684kb"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"1000"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"6.28MB"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"10k"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"61.3MB"})})]})]})]})]})]})]}),"\n",(0,r.jsx)(n.p,{children:"A retention policy can be configured to delete Plays, or unused debug data, from the database after a certain amount of time. If no configuration is provided then a default policy is used that should be reasonable for most users."}),"\n",(0,r.jsxs)(l.A,{groupId:"policy",queryString:!0,children:[(0,r.jsxs)(o.A,{label:"Compaction",value:"compaction",children:[(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.strong,{children:"Compaction"})," Retention Policy is used to delete different types of debug data from your stored Plays."]}),(0,r.jsx)(n.p,{children:"This is a useful way to reduce used storage space when you are not having problems with your Plays, or iterating on a configuration, that requires referencing all this extra data."}),(0,r.jsx)(n.p,{children:"There are two types of data that can be compacted (deleted from the Play):"}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"input"})," - this is the untouched data retrieved by Multi-scrobbler, from a Source, and used to generate a Play/scrobble. This can be used to reconstruct and replay a Play, when used from troubleshooting or reporting an issue"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"transform"})," - this is all of the steps generated by ",(0,r.jsx)(n.a,{href:"/configuration/transforms",children:"transforms"}),", the diff of the Play resulting from the step, and any request/responses used to complete the step"]}),"\n"]}),(0,r.jsxs)(n.admonition,{title:"Defaults",type:"note",children:[(0,r.jsx)(n.p,{children:"When no Compact configuration is provided, Multi-scrobbler uses this policy:"}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Compact (delete) ",(0,r.jsx)(n.code,{children:"input"})," and ",(0,r.jsx)(n.code,{children:"transform"})," data on all Plays after 3 days"]}),"\n"]})]}),(0,r.jsx)(n.h4,{id:"configuring-compaction-policy",children:"Configuring Compaction Policy"}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Details"}),(0,r.jsx)(n.p,{children:"Each value in the configuration properties below can be either"}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["a number of seconds EX ",(0,r.jsx)(n.code,{children:"3600"})," = 10 minutes"]}),"\n",(0,r.jsxs)(n.li,{children:["a unit of a common duration with the pattern ",(0,r.jsx)(n.code,{children:"X unit"})," EX","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"30 minutes"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"5 hours"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"2 days"})}),"\n"]}),"\n"]}),"\n"]}),(0,r.jsxs)(l.A,{groupId:"configType",queryString:!0,children:[(0,r.jsxs)(o.A,{value:"env",label:"ENV",children:[(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"COMPACT_PROPERTIES"})," - which properties to compact"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"RETENTION_COMPACT_AFTER"})," - Default to use for all Plays"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"RETENTION_COMPACT_COMPLETED_AFTER"})," - Compact only completed Plays after..."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"RETENTION_COMPACT_FAILED_AFTER"})," - Compact only failed Plays after..."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"RETENTION_COMPACT_DUPED_AFTER"})," - Compact only duped/discard Plays after..."]}),"\n"]}),(0,r.jsx)(n.p,{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ini",children:"# only delete input when compacting\nCOMPACT_PROPERTIES=input\n# compact all plays after 3 days \nRETENTION_COMPACT_AFTER=3 days\n# specifically compact completed plays after 30 minutes\nRETENTION_COMPACT_COMPLETED_AFTER=30 minutes\n"})})]}),(0,r.jsxs)(o.A,{value:"aio",label:"AIO",children:[(0,r.jsx)(n.p,{children:"Compacting all Play types and deleting both input and transform:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "database": {\n "retention": {\n "compactAfter": "3 days",\n "compact": [\n "input",\n "transform"\n ]\n }\n }\n}\n'})}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Delete only input during compacting"}),"\n",(0,r.jsx)(n.li,{children:"Compact all after 3 days except completed which compacts after 30 minutes"}),"\n"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "database": {\n "retention": {\n "compactAfter": {\n "completed": "30 minutes",\n "duped": "3 days",\n "failed": "3 days"\n },\n "compact": [\n "input"\n ]\n }\n }\n}\n'})})]})]})]})]}),(0,r.jsxs)(o.A,{label:"Deletion",value:"deletion",children:[(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.strong,{children:"Deletion"})," Retention Policy is used to delete different types of stored Plays from Multi-scrobbler database."]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"This does not delete Plays from your Clients."}),' It\'s only deleting the "in-flight" data MS used to create the scrobble that was eventually sent to your clients.']}),(0,r.jsxs)(n.admonition,{title:"Plays Should be Ephemeral",type:"warning",children:[(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Multi-Scrobbler is not designed to store Plays/Scrobbles indefinitely."})}),(0,r.jsx)(n.p,{children:"It should scale fine for thousands of scrobbles but it not meant to store 10's of thousands of scrobbles forever. It is not a scrobbler server."}),(0,r.jsxs)(n.p,{children:["You ",(0,r.jsx)(n.strong,{children:"should"})," set a reasonable deletion policy so that MS stores less than 1000 scrobbles at a time, ideally less."]})]}),(0,r.jsxs)(n.admonition,{title:"Defaults",type:"note",children:[(0,r.jsx)(n.p,{children:"When no Deletion policy configuration is provided, Multi-scrobbler uses this policy:"}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Delete all Plays after 7 days"}),"\n"]})]}),(0,r.jsx)(n.h4,{id:"configuring-deletion-policy",children:"Configuring Deletion Policy"}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Details"}),(0,r.jsx)(n.p,{children:"Each value in the configuration properties below can be either"}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["a number of seconds EX ",(0,r.jsx)(n.code,{children:"3600"})," = 10 minutes"]}),"\n",(0,r.jsxs)(n.li,{children:["a unit of a common duration with the pattern ",(0,r.jsx)(n.code,{children:"X unit"})," EX","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"30 minutes"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"5 hours"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"2 days"})}),"\n"]}),"\n"]}),"\n"]}),(0,r.jsxs)(l.A,{groupId:"configType",queryString:!0,children:[(0,r.jsxs)(o.A,{value:"env",label:"ENV",children:[(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"RETENTION_DELETE_AFTER"})," - Default to use for all Plays"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"RETENTION_DELETE_COMPLETED_AFTER"})," - Delete only completed Plays after..."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"RETENTION_DELETE_FAILED_AFTER"})," - Delete only failed Plays after..."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"RETENTION_DELETE_DUPED_AFTER"})," - Delete only duped/discard Plays after..."]}),"\n"]}),(0,r.jsx)(n.p,{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ini",children:"# delete all plays after 3 days \nRETENTION_DELETE_AFTER=3 days\n# specifically, delete completed plays after 30 minutes\nRETENTIOND_DELETE_COMPLETED_AFTER=30 minutes\n"})})]}),(0,r.jsxs)(o.A,{value:"aio",label:"AIO",children:[(0,r.jsx)(n.p,{children:"Deleting all Play types after 3 days:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "database": {\n "retention": {\n "deleteAfter": "3 days"\n }\n }\n}\n'})}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Delete all after 3 days except completed which are deleted after 30 minutes"}),"\n"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "database": {\n "retention": {\n "deleteAfter": {\n "completed": "30 minutes",\n "duped": "3 days",\n "failed": "3 days"\n },\n }\n }\n}\n'})})]})]})]})]})]}),"\n",(0,r.jsx)(n.h2,{id:"debug-mode",children:"Debug Mode"}),"\n",(0,r.jsx)(n.p,{children:"Turning on Debug Mode will"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:'override and enable all optional "debugging" options found in configuration'}),"\n",(0,r.jsx)(n.li,{children:"set log output to DEBUG"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"Use this as a shortcut for enabling output that can be used for troubleshooting and issue reporting. Be aware that logs will likely be VERY noisy while Debug Mode is on. You should only have this mode on while gathering logs for troubleshooting and then turn it off afterwards."}),"\n",(0,r.jsxs)(n.p,{children:["To set debug mode either add it to ",(0,r.jsxs)(n.a,{href:"./?configType=aio#configuration-types",children:["AIO ",(0,r.jsx)(n.code,{children:"config.json"})]})]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n "debugMode": true,\n "sources": [...],\n // ...\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["or set the ",(0,r.jsx)(n.a,{href:"./?configType=env#configuration-types",children:"ENV"})," ",(0,r.jsx)(n.code,{children:"DEBUG_MODE=true"})]}),"\n",(0,r.jsx)(n.h2,{id:"disable-web",children:"Disable Web"}),"\n",(0,r.jsx)(n.p,{children:"If you do not need the dashboard and/or ingress sources, or have security concerns about ingress and cannot control your hosting environment, the web server and API can be disabled."}),"\n",(0,r.jsx)(n.admonition,{type:"warning",children:(0,r.jsxs)(n.p,{children:["Any ",(0,r.jsxs)(n.strong,{children:[(0,r.jsx)(n.a,{href:"/configuration/sources?sourceComm=ingress#by-communication-method",children:"ingress-based sources"})," will be unusable"]})," (Webscrobbler, etc...) if this is disabled."]})}),"\n",(0,r.jsx)(n.p,{children:"Disable using either:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["ENV ",(0,r.jsx)(n.code,{children:"DISABLE_WEB=true"})]}),"\n",(0,r.jsxs)(n.li,{children:["In ",(0,r.jsx)(n.a,{href:"./?configType=aio#configuration-types",children:"All-in-One File"})," use the top-level property ",(0,r.jsx)(n.code,{children:'"disableWeb": true'})]}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"monitoring",children:"Monitoring"}),"\n",(0,r.jsx)(n.h3,{id:"upstream-services-status",children:"Upstream Services Status"}),"\n",(0,r.jsx)(n.p,{children:"A status page for monitoring the reachability of public services used by some Sources/Clients is available at"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:(0,r.jsx)(n.a,{href:"https://status.multi-scrobbler.app",children:"https://status.multi-scrobbler.app"})})}),"\n",(0,r.jsxs)(n.p,{children:["This monitor runs on a VPS and checks the uptime of ",(0,r.jsx)(n.em,{children:"actual"})," APIs for these services, not just landing pages. On a ~minute interval it checks:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Listenbrainz API"}),"\n",(0,r.jsx)(n.li,{children:"Musicbrainz API"}),"\n",(0,r.jsx)(n.li,{children:"CoverArtArchive API"}),"\n",(0,r.jsx)(n.li,{children:"Last.fm API"}),"\n",(0,r.jsx)(n.li,{children:"A few MS-hosted support services"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"webhook-configurations",children:"Webhook Configurations"}),"\n",(0,r.jsxs)(n.p,{children:["Webhooks will ",(0,r.jsx)(n.strong,{children:"push"})," a notification to your configured servers on these events:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Source polling started"}),"\n",(0,r.jsx)(n.li,{children:"Source polling retry"}),"\n",(0,r.jsx)(n.li,{children:"Source polling stopped on error"}),"\n",(0,r.jsx)(n.li,{children:"Scrobble client scrobble failure"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Webhooks are configured in the AIO ",(0,r.jsx)(n.a,{href:"#configuration-types",children:"config.json"})," file under the ",(0,r.jsx)(n.code,{children:"webhook"})," top-level property. Multiple webhooks may be configured for each webhook type."]}),"\n",(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n "sources": [\n //...\n ],\n "clients": [\n //...\n ],\n "webhooks": [\n {\n "name": "FirstGotifyServer",\n "type": "gotify",\n "url": "http://192.168.0.100:8070",\n "token": "abcd"\n },\n {\n "name": "SecondGotifyServer",\n "type": "gotify",\n //...\n },\n {\n "name": "NtfyServerOne",\n "type": "ntfy",\n //...\n },\n //...\n ]\n}\n'})})]}),"\n",(0,r.jsx)(n.h4,{id:"gotify",children:(0,r.jsx)(n.a,{href:"https://gotify.net/",children:"Gotify"})}),"\n",(0,r.jsxs)(n.p,{children:["Refer to the ",(0,r.jsx)(n.a,{href:"/playground",children:"config schema for GotifyConfig"})]}),"\n",(0,r.jsxs)(n.p,{children:["multi-scrobbler optionally supports setting message notification priority via ",(0,r.jsx)(n.code,{children:"info"})," ",(0,r.jsx)(n.code,{children:"warn"})," and ",(0,r.jsx)(n.code,{children:"error"})," mappings."]}),"\n",(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "type": "gotify",\n "name": "MyGotifyFriendlyNameForLogs",\n "url": "http://192.168.0.100:8070",\n "token": "AQZI58fA.rfSZbm",\n "priorities": {\n "info": 5,\n "warn": 7,\n "error": 10\n }\n}\n'})})]}),"\n",(0,r.jsx)(n.h4,{id:"ntfy",children:(0,r.jsx)(n.a,{href:"https://ntfy.sh/",children:"Ntfy"})}),"\n",(0,r.jsxs)(n.p,{children:["Refer to the ",(0,r.jsx)(n.a,{href:"/playground",children:"config schema for NtfyConfig"})]}),"\n",(0,r.jsxs)(n.p,{children:["multi-scrobbler optionally supports setting message notification priority via ",(0,r.jsx)(n.code,{children:"info"})," ",(0,r.jsx)(n.code,{children:"warn"})," and ",(0,r.jsx)(n.code,{children:"error"})," mappings."]}),"\n",(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "type": "ntfy",\n "name": "MyNtfyFriendlyNameForLogs",\n "url": "http://192.168.0.100:9991",\n "topic": "RvOwKJ1XtIVMXGLR",\n "username": "Optional",\n "password": "Optional",\n "priorities": {\n "info": 3,\n "warn": 4,\n "error": 5\n }\n}\n'})})]}),"\n",(0,r.jsx)(n.h4,{id:"apprise",children:(0,r.jsx)(n.a,{href:"https://github.com/caronc/apprise-api",children:"Apprise"})}),"\n",(0,r.jsxs)(n.p,{children:["Refer to the ",(0,r.jsx)(n.a,{href:"/playground",children:"config schema for AppriseConfig"})]}),"\n",(0,r.jsxs)(n.p,{children:["multi-scrobbler supports ",(0,r.jsx)(n.a,{href:"https://github.com/caronc/apprise-api?tab=readme-ov-file#stateless-solution",children:"stateless"})," and ",(0,r.jsx)(n.a,{href:"https://github.com/caronc/apprise-api?tab=readme-ov-file#persistent-storage-solution",children:"persistent storage"})," endpoints as well as ",(0,r.jsx)(n.a,{href:"https://github.com/caronc/apprise-api?tab=readme-ov-file#tagging",children:"tags"}),"/"]}),"\n",(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n "type": "apprise",\n "name": "MyAppriseFriendlyNameForLogs",\n "host": "http://192.168.0.100:8080",\n "urls": ["gotify://192.168.0.101:8070/MyToken"], // stateless endpoints\n "keys": ["e90b20526808373353afad7fb98a201198c0c3e0555bea19f182df3388af7b17"], //persistent storage endpoints\n "tags": ["my","optional","tags"]\n}\n'})})]}),"\n",(0,r.jsx)(n.h3,{id:"health-endpoint",children:"Health Endpoint"}),"\n",(0,r.jsxs)(n.p,{children:["An endpoint for monitoring the health of sources/clients is available at GET ",(0,r.jsx)(n.code,{children:"http://YourMultiScrobblerDomain/api/health"})]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Returns ",(0,r.jsx)(n.code,{children:"200 OK"})," when ",(0,r.jsx)(n.strong,{children:"everything"})," is working or ",(0,r.jsx)(n.code,{children:"500 Internal Server Error"})," if ",(0,r.jsx)(n.strong,{children:"anything"})," is not"]}),"\n",(0,r.jsxs)(n.li,{children:["The plain url (",(0,r.jsx)(n.code,{children:"/api/health"}),") aggregates status of ",(0,r.jsx)(n.strong,{children:"all clients/sources"})," -- so any failing client/source will make status return 500","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Use query params ",(0,r.jsx)(n.code,{children:"type"})," or ",(0,r.jsx)(n.code,{children:"name"})," to restrict client/sources aggregated IE ",(0,r.jsx)(n.code,{children:"/api/health?type=spotify"})," or ",(0,r.jsx)(n.code,{children:"/api/health?name=MyMaloja"})]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["On 500 the response returns a JSON payload with ",(0,r.jsx)(n.code,{children:"messages"})," array that describes any issues","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["For any clients/sources that require authentication ",(0,r.jsx)(n.code,{children:"/api/health"})," will return 500 if they are ",(0,r.jsx)(n.strong,{children:"not authenticated"})]}),"\n",(0,r.jsxs)(n.li,{children:["For sources that poll (spotify, yt music, subsonic) ",(0,r.jsx)(n.code,{children:"/api/health"})," will 500 if they are ",(0,r.jsx)(n.strong,{children:"not polling"})]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"prometheus",children:"Prometheus"}),"\n",(0,r.jsxs)(n.p,{children:["A ",(0,r.jsx)(n.a,{href:"https://prometheus.io/",children:"Prometheus"})," export endpoint is available at GET ",(0,r.jsx)(n.code,{children:"http://YourMultiScrobblerDomain/api/metrics"})]}),"\n",(0,r.jsx)(n.p,{children:"It includes metrics for:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Count of discovered plays per ",(0,r.jsx)(n.a,{href:"/configuration/sources",children:"Source"})]}),"\n",(0,r.jsxs)(n.li,{children:["Count of Queued/Scrobbled/Deadletter scrobbles per ",(0,r.jsx)(n.a,{href:"/configuration/clients",children:"Client"})]}),"\n",(0,r.jsxs)(n.li,{children:["Number of issues per Source/Client","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"If any of these metrics is > 0 it means your Source/Client is not operating normally"}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.li,{children:"Caching metrics"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Additionally, general process metrics like cpu and memory usage can be enabled with the env ",(0,r.jsx)(n.code,{children:"PROMETHEUS_FULL=true"})]}),"\n",(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Example Usage"}),(0,r.jsxs)(n.p,{children:["Update your ",(0,r.jsx)(n.a,{href:"https://prometheus.io/docs/prometheus/latest/configuration/configuration/#configuration-file",children:"Prometheus Configuration file"})," (usually at ",(0,r.jsx)(n.code,{children:"/etc/prometheus.yml"}),") to add a job to the ",(0,r.jsx)(n.code,{children:"scrape_configs"}),(0,r.jsx)(n.a,{href:"https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config",children:"section"})," like this:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"scrape_configs:\n #\n # ...your other jobs are here\n # \n - job_name: multi-scrobbler\n scrape_interval: 30s\n metrics_path: '/api/metrics'\n static_configs:\n - targets: ['myMultiscrobblerIp:9078']\n"})})]}),"\n",(0,r.jsxs)(a,{status:"tip",summary:"Grafana Dashboard",children:[(0,r.jsxs)(n.p,{children:["Github user ",(0,r.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/issues/507#issuecomment-4144982295",children:"4rft5"})," created a Grafana dashboard that uses the Prometheus metrics from above."]}),(0,r.jsx)("img",{src:i(6434).A,height:"800"}),(0,r.jsxs)(n.p,{children:["Download the json dashboard here: ",(0,r.jsx)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:i(3096).A+"",children:(0,r.jsx)(n.code,{children:"multiscrobbler-dashboard.json"})})]})]})]})}function f(e={}){let{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(j,{...e})}):j(e)}function p(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}},9461(e,n,i){i.d(n,{A:()=>h}),i(6540);var s=i(2170),r=i.n(s),t=i(5548),l=i(349),o=i(1219),a=i.n(o),c=i(4848);let d=e=>{let n,{data:i,name:s,client:l=!1}=e;try{n=a().parse(i)}catch(e){return console.error(e),(0,c.jsxs)(t.A,{type:"danger",title:"Unexpected Error",children:[(0,c.jsx)("p",{children:"Example component crashed because of error!"}),(0,c.jsx)(r(),{children:e.message})]})}n=(n=n.filter(e=>void 0===e.configureAs||e.configureAs===(l?"client":"source"))).map(e=>({...e,type:s}));let o={[l?"clients":"sources"]:n};return(0,c.jsx)(r(),{title:"CONFIG_DIR/config.json",language:"json5",children:JSON.stringify(o,null,2)})},h=e=>(0,c.jsx)(l.A,{fallback:({error:e})=>(0,c.jsx)("div",{children:(0,c.jsxs)("p",{children:["Example component crashed because of error: ",e.message,"."]})}),children:(0,c.jsx)(d,{...e})})},1672(e,n,i){i.d(n,{A:()=>l}),i(6540);var s=i(4164),r=i(8774),t=i(4848);function l({size:e=null,outline:n=!1,variant:i="primary",block:o=!1,disabled:a=!1,className:c,style:d={},link:h,label:u,onClick:x=()=>null,...j}){let f=e?({sm:"sm",small:"sm",lg:"lg",large:"lg",medium:null})[e]:"",p=f?`button--${f}`:"",m=i?`button--${i}`:"";return(0,t.jsx)(r.A,{to:a?null:h,children:(0,t.jsx)("button",{className:(0,s.A)("button",p,n?"button--outline":"",m,o?"button--block":"",a?"disabled":"",c),style:d,role:"button","aria-disabled":a,onClick:e=>{e.preventDefault(),x()},...j,children:u})})}},9643(e,n,i){i.d(n,{A:()=>a}),i(6540);var s=i(1672),r=i(4848);let t={borderRadius:"var(--ifm-button-border-radius) 0 0 var(--ifm-button-border-radius)"},l={borderRadius:"0 var(--ifm-button-border-radius) var(--ifm-button-border-radius) 0"},o={borderRadius:"0"};function a({size:e=null,variant:n="primary",block:i=!1,disabled:c=!1,className:d,style:h,options:u,value:x,defaultValue:j,onChange:f=()=>null}){if(void 0===u||0===u.length)return null;let p=u.map((i,a)=>{let c={style:o,outline:!0,size:e,variant:n,label:i[1],link:"#",onClick:()=>f(i[0])};return 0===a?c.style=t:a===u.length-1&&(c.style=l),(x??j)===i[0]&&(c.outline=!1),(0,r.jsx)(s.A,{...c},a)});return(0,r.jsx)("span",{children:p})}},8675(e,n,i){i.d(n,{A:()=>j});var s=i(6540),r=i(2170),t=i.n(r),l=i(349),o=i(8193);let a={};var c=i(9643),d=i(1922),h=i(4581),u=i(4848);let x=e=>{let{data:n,interactive:i=!0,defaultDisplay:r="interactive",schemaName:l,filePathName:x,readOnlyAtWidth:j=1,...f}=e,[p,m]=((e,n,i)=>{let r;return e in a?a[e]():o.A.canUseDOM?(a[e]=(r=[],()=>{let[t,l]=((e,n,i=!0)=>{let[r,t]=(0,s.useState)(()=>{let s=window.localStorage.getItem(e);if(i)try{return s?JSON.parse(s):n}catch(e){return console.error(e),n}try{let e;if(null===s)return n;if("true"===s)return!0;if("false"===s)return!1;if(e=s,("number"==typeof e||"string"==typeof e&&""!==e.trim())&&!isNaN(e)){if(s.includes("."))return parseFloat(s);return parseInt(s)}return s}catch(e){return console.error(e),n}}),l=(0,s.useCallback)(n=>{t(()=>(i?localStorage.setItem(e,JSON.stringify(n)):localStorage.setItem(e,n),n))},[e]);return(0,s.useEffect)(()=>{let i=i=>{i.storageArea===localStorage&&i.key===e&&t(null===i.newValue?n:i.newValue)};return window.addEventListener("storage",i),()=>window.removeEventListener("storage",i)}),[r,l]})(e,n,i),o=(0,s.useCallback)(e=>{for(let n of r)n(e)},[]);return(0,s.useEffect)(()=>(r.push(l),()=>{r.splice(r.indexOf(l),1)}),[l]),[t,o]}),a[e]()):[n,()=>null]})("docusaurus.tab.fileExampleShow",r,!1),g=(0,h.l)({desktopBreakpoint:j}),b=null,y=i&&"desktop"===g;y&&(b=(0,u.jsx)(c.A,{options:[["plain","Plain"],["interactive","Interactive"]],defaultValue:r,value:p,variant:"primary",size:"medium",onChange:e=>m(e)}));let v=null;void 0!==x&&y&&"interactive"===p&&(v=(0,u.jsx)("code",{style:{float:"right",paddingRight:"0.5em",paddingLeft:"0.5em"},children:x}));let E=null;(null!==b||null!==v)&&(E=(0,u.jsxs)("div",{style:{marginBottom:"1em"},children:[b,v]}));let A=(0,s.useMemo)(()=>y&&"interactive"===p?null:"string"!=typeof n?JSON.stringify(n,null,2):n,[i,p,n]);return(0,u.jsxs)(s.Fragment,{children:[E,y&&"plain"!==p?null:(0,u.jsx)(t(),{title:x,language:"json5",children:A}),y&&"interactive"===p?(0,u.jsx)(d.A,{configContent:n,schemaName:l}):null]})},j=e=>(0,u.jsx)(l.A,{fallback:({error:e})=>(0,u.jsx)("div",{children:(0,u.jsxs)("p",{children:["Example component crashed because of error: ",e.message,"."]})}),children:(0,u.jsx)(x,{...e})})},1922(e,n,i){i.d(n,{A:()=>f});var s=i(6540),r=i(6025),t=i(8478),l=i(5293),o=i(4020),a=i(8182),c=i.n(a),d=i(1219),h=i.n(d),u=i(4848);function x(e){let n=(0,s.useMemo)(()=>{if(void 0!==e.schema)return(0,o.IM)({schema:e.schema,ajvOptions:{strict:"log",strictSchema:"log",allowUnionTypes:!0},onCreateAjv:e=>(e.addKeyword("deprecationMessage"),c().default(e),e)})},[e.schema]),i=(0,s.useMemo)(()=>({json:"string"==typeof e.config?h().parse(e.config):e.config}),[e.config]),{colorMode:r}=(0,l.G)();return(0,u.jsx)(o.t$,{validator:n,theme:r,mode:"text",content:i,style:{height:"initial"}})}function j(e){return(0,u.jsx)(t.default,{fallback:(0,u.jsx)("div",{children:"Loading..."}),children:()=>(0,u.jsx)(x,{...e})})}function f(e){let{schemaName:n}=e,[i,t]=(0,s.useState)(void 0),[l,o]=(0,s.useState)(void 0),a=void 0===n?void 0:(0,r.Ay)(`schemas/${e.schemaName}.json`);return(0,s.useEffect)(()=>{void 0===a?t(!1):fetch(a,{headers:{Accept:"application/json"}}).then(e=>e.json()).then(e=>t(e)).catch(e=>t(e))},[t,a]),(0,u.jsxs)(s.Fragment,{children:[void 0===i&&(0,u.jsx)("div",{children:"Loading ..."}),void 0!==i&&i instanceof Error&&(0,u.jsxs)("div",{children:["Houston we have a problem : ",i.message]}),void 0!==i&&!(i instanceof Error)&&(0,u.jsx)(j,{schema:!1===i?void 0:i,config:e.configContent})]})}},2709(e,n,i){i.d(n,{A:()=>t});var s=i(6540),r=i(4848);let t=e=>{let{children:n,lower:i,client:t=!1}=e,l=n;void 0===l&&(l=(0,r.jsxs)(s.Fragment,{children:[i?"explore":"Explore"," the schema with an example and live editor/validator"]}));let o=`https://json-schema.app/view/%23/%23%2Fdefinitions%2F${e.objectName}`;return(0,r.jsx)("a",{target:"_blank",href:`${o}?url=${t?"https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fclient.json":"https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json"}`,children:l})}},3096(e,n,i){i.d(n,{A:()=>s});let s=i.p+"assets/files/multiscrobbler-dashboard-814b740dbbd83aba08598320999b9f67.json"},6434(e,n,i){i.d(n,{A:()=>s});let s=i.p+"assets/images/grafana-004a8d8d2ec395ad949e9f5b79406a32.png"}}]); \ No newline at end of file diff --git a/assets/js/04d926a5.8f53c6aa.js b/assets/js/04d926a5.8f53c6aa.js new file mode 100644 index 00000000..53f2d3ff --- /dev/null +++ b/assets/js/04d926a5.8f53c6aa.js @@ -0,0 +1,13 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["4461"],{7265(e,n,i){i.d(n,{Ay:()=>c,RM:()=>o});var s=i(4848),r=i(8453),t=i(2170),l=i.n(t);let o=[];function a(e){let n={a:"a",code:"code",li:"li",p:"p",strong:"strong",ul:"ul",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(n.p,{children:["A ",(0,s.jsx)(n.strong,{children:"Source"})," that monitors Listening Activity uses one of two metrics to determine if a Song is scrobbable based on your listening:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"percent"})," of track listened to (50% or more)"]}),"\n",(0,s.jsxs)(n.li,{children:["or amount of time (",(0,s.jsx)(n.strong,{children:"duration"}),") track was listened to (4 minutes or more)"]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["These default values are based on ",(0,s.jsx)(n.a,{href:"https://www.last.fm/api/scrobbling#when-is-a-scrobble-a-scrobble",children:"last.fm's scrobble guidance."})]}),"\n",(0,s.jsxs)(n.p,{children:["These values can be customized in the ",(0,s.jsx)(n.code,{children:"options"})," of Source's ",(0,s.jsx)(n.a,{href:"/configuration#configuration-types",children:"file or aio config"}),":"]}),"\n",(0,s.jsx)(l(),{language:"json5",title:`${e.filename}.json`,children:`{ + "data": { + // ... + }, + "options": { + // highlight-start + "scrobbleThresholds": { + "duration": 40, // scrobbable if listened to for 40 seconds or more + "percent": 20 // scrobbable if listened to for 20% or more of the track's length + } + // highlight-end + } +}`})]})}function c(e={}){let{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(a,{...e})}):a(e)}},9625(e,n,i){i.r(n),i.d(n,{metadata:()=>s,default:()=>f,frontMatter:()=>d,contentTitle:()=>h,toc:()=>x,assets:()=>u});var s=JSON.parse('{"id":"configuration/configuration","title":"Overview","description":"Configuration","source":"@site/docs/configuration/configuration.mdx","sourceDirName":"configuration","slug":"/configuration/","permalink":"/multi-scrobbler/configuration/","draft":false,"unlisted":false,"editUrl":"https:/github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/configuration/configuration.mdx?plain=1","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"sidebar_position":2,"title":"Overview","description":"Configuration","toc_max_heading_level":3},"sidebar":"tutorialSidebar","previous":{"title":"From < 0.14.0","permalink":"/multi-scrobbler/updating/upgrade-path/0140"},"next":{"title":"Overview","permalink":"/multi-scrobbler/configuration/clients/"}}'),r=i(4848),t=i(8453),l=i(8194),o=i(698),a=i(2170),c=i.n(a);i(2709),i(9461),i(8675),i(7265);let d={sidebar_position:2,title:"Overview",description:"Configuration",toc_max_heading_level:3},h,u={},x=[{value:"Configuration Types",id:"configuration-types",level:2},{value:"Setup Sources and Clients",id:"setup-sources-and-clients",level:2},{value:"Secrets Interpolation",id:"secrets-interpolation",level:2},{value:"Base URL",id:"base-url",level:2},{value:"Caching",id:"caching",level:2},{value:"Secondary Caching Store",id:"secondary-caching",level:3},{value:"Database",id:"database",level:2},{value:"Retention",id:"retention",level:3},{value:"Configuring Compaction Policy",id:"configuring-compaction-policy",level:4},{value:"Configuring Deletion Policy",id:"configuring-deletion-policy",level:4},{value:"Debug Mode",id:"debug-mode",level:2},{value:"Disable Web",id:"disable-web",level:2},{value:"Monitoring",id:"monitoring",level:2},{value:"Upstream Services Status",id:"upstream-services-status",level:3},{value:"Webhook Configurations",id:"webhook-configurations",level:3},{value:"Gotify",id:"gotify",level:4},{value:"Ntfy",id:"ntfy",level:4},{value:"Apprise",id:"apprise",level:4},{value:"Health Endpoint",id:"health-endpoint",level:3},{value:"Prometheus",id:"prometheus",level:3}];function j(e){let n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components},{Details:s,DetailsAdmo:a}=n;return s||p("Details",!0),a||p("DetailsAdmo",!0),(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["Check the ",(0,r.jsx)(n.a,{href:"/multi-scrobbler/FAQ",children:(0,r.jsx)(n.strong,{children:"FAQ"})})," if you have any issues after configuration!"]})}),"\n",(0,r.jsx)(n.h2,{id:"configuration-types",children:"Configuration Types"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/sources",children:(0,r.jsx)(n.strong,{children:"Sources"})})," and ",(0,r.jsx)(n.a,{href:"/configuration/clients",children:(0,r.jsx)(n.strong,{children:"Clients"})})," are configured using:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"environmental (ENV) variables"}),"\n",(0,r.jsx)(n.li,{children:"client/source specific json config files"}),"\n",(0,r.jsx)(n.li,{children:"an all-in-one json config file"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"MS will parse configuration from all configuration types."})," You can mix and match configurations but it is generally better to stick to one type."]}),"\n",(0,r.jsxs)(l.A,{groupId:"configType",queryString:!0,children:[(0,r.jsxs)(o.A,{value:"env",label:"ENV",children:[(0,r.jsx)(n.p,{children:"MS will parse environmental variables present in the OS/container when it is run."}),(0,r.jsxs)(n.p,{children:["This method means MS ",(0,r.jsx)(n.strong,{children:"does not require config files"})," but it will still create and use data files in the ",(0,r.jsx)(n.a,{href:"/installation/#config-and-data-directories",children:"local"})," or ",(0,r.jsx)(n.a,{href:"/installation/?dockerSetting=storage#recommended-settings",children:"docker"})," data directory."]}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Use ENV-based configuration if..."}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"You are the only person for whom MS is scrobbling for or"}),"\n",(0,r.jsxs)(n.li,{children:["You have a very simple setup for MS such as one scrobble ",(0,r.jsx)(n.a,{href:"/configuration/clients",children:"Client"})," and one ",(0,r.jsx)(n.a,{href:"/configuration/sources",children:"Source"})," IE Jellyfin -> Koito, or"]}),"\n",(0,r.jsx)(n.li,{children:"You are not comfortable editing JSON files"}),"\n"]})]}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Config Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n image: foxxmd/multi-scrobbler\n environment:\n - JELLYFIN_URL=192.168.0.110:8096\n - JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56b\n - JELLYFIN_USER=MyUser\n - JELLYFIN_ID=myJellyin\n # ...\n # ...\n"})}),(0,r.jsx)(n.p,{children:"For a local/node installation export variables before running..."}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-shell",children:"SPOTIFY_CLIENT_ID=yourId SPOTIFY_CLIENT_SECRET=yourSecret npm run start\n"})})]})]}),(0,r.jsxs)(o.A,{value:"file",label:"File",children:[(0,r.jsxs)(n.p,{children:["MS will parse configuration files located in the directory specified by the ",(0,r.jsx)(n.code,{children:"CONFIG_DIR"})," environmental variable. This variable defaults to:"]}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Local installation -> See ",(0,r.jsx)(n.a,{href:"/installation/#config-and-data-directories",children:"Config and Data Directories"})]}),"\n",(0,r.jsxs)(n.li,{children:["Docker -> ",(0,r.jsx)(n.code,{children:"/config"})," (in the container) -- see the ",(0,r.jsxs)(n.a,{href:"/installation/?dockerSetting=storage#recommended-settings",children:[(0,r.jsx)(n.strong,{children:"Storage"})," tab in the Docker installation docs"]})," for how to configure this correctly"]}),"\n"]}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Use File-based configuration if..."}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"You are comfortable editing JSON files"}),"\n",(0,r.jsxs)(n.li,{children:["You have more than one of the same type of ",(0,r.jsx)(n.strong,{children:"Source/Client"})," you want to use EX 2x Last.fm accounts, 3x Jellyfin accounts"]}),"\n",(0,r.jsxs)(n.li,{children:["You want activity from specific ",(0,r.jsx)(n.strong,{children:"Sources"})," to scrobble to specific ",(0,r.jsx)(n.strong,{children:"Clients"})]}),"\n",(0,r.jsxs)(n.li,{children:["You need to setup more advanced configuration for a Source/Client","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Most Source/Clients only support basic configuration through ENV, more configuration is possible using File. AIO enables complete configuration."}),"\n"]}),"\n"]}),"\n"]})]}),(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["There are ",(0,r.jsx)(n.strong,{children:"example configurations"})," for all Source/Client types and AIO config located in the ",(0,r.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/tree/master/config",children:(0,r.jsx)(n.code,{children:"/config"})})," directory of this project. These can be used as-is by renaming them to ",(0,r.jsx)(n.code,{children:".json"}),".","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["These are the same examples you will find in the ",(0,r.jsx)(n.strong,{children:"Configuration"})," section of each Source/Client"]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["There is also a ",(0,r.jsx)(n.a,{href:"/configuration/kitchensink",children:(0,r.jsx)(n.strong,{children:"kitchensink example"})})," that provides examples of using all sources/clients in a complex configuration."]}),"\n"]})}),(0,r.jsxs)(n.p,{children:["Each config file is named after the ",(0,r.jsx)(n.strong,{children:"type"})," of the Client/Source. Within each file, a JSON ",(0,r.jsx)(n.strong,{children:"array"})," defines all of the instances for that type of Client/Source."]}),(0,r.jsx)(n.p,{children:"Example directory structure:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"/CONFIG_DIR\n jellyfin.json\n lastfm.json\n koito.json\n"})}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Config Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="jellyfin.json"',children:'[\n {\n "name": "Foxx JF Server",\n "id": "foxxJf",\n "data": {\n "url": "http://localhost:8096",\n "user": "FoxxMD",\n "apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"\n }\n }\n]\n'})})]}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Multiple of Same Config Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="jellyfin.json"',children:'[\n {\n "name": "Foxx JF Server",\n "id": "foxxJf",\n "data": {\n "url": "http://localhost:8096",\n "user": "FoxxMD",\n "apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"\n }\n },\n // highlight-start\n {\n "name": "Foo\'s JF Server",\n "id": "fooJf",\n "data": {\n "url": "http://192.168.0.150:8096",\n "user": "foo",\n "apiKey": "9c5bb56bd6540c756fbf481ebd9c5c40"\n }\n }\n // highlight-end\n]\n'})})]}),(0,r.jsxs)(n.p,{children:["You can find an example of the file name that should be used under the ",(0,r.jsx)(n.strong,{children:"File"})," Configuration for each Source/Client. Note: this is the same value that is used for ",(0,r.jsx)(n.code,{children:"type"})," in AIO configs."]})]}),(0,r.jsxs)(o.A,{value:"aio",label:"File AIO",children:[(0,r.jsxs)(n.p,{children:["MS will parse an ",(0,r.jsx)(n.strong,{children:"all-in-one"})," configuration file (",(0,r.jsx)(n.code,{children:"config.json"}),") located in the directory specified by the ",(0,r.jsx)(n.code,{children:"CONFIG_DIR"})," environmental variable. The file's location defaults to:"]}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Local installation -> See ",(0,r.jsx)(n.a,{href:"/installation/#config-and-data-directories",children:"Config and Data Directories"})]}),"\n",(0,r.jsxs)(n.li,{children:["Docker -> ",(0,r.jsx)(n.code,{children:"/config/config.json"})," (in the container) -- see the ",(0,r.jsxs)(n.a,{href:"/installation/?dockerSetting=storage#recommended-settings",children:[(0,r.jsx)(n.strong,{children:"Storage"})," tab in the Docker installation docs"]})," for how to configure this correctly"]}),"\n"]}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Use AIO-based configuration if..."}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["...All of the reasons listed in ",(0,r.jsx)(n.a,{href:"./?configType=file#configuration-types",children:"File"})]}),"\n",(0,r.jsxs)(n.li,{children:["You want to setup ",(0,r.jsx)(n.a,{href:"#monitoring",children:"monitoring/webhooks"}),", default settings for all Sources/Clients, or global config options"]}),"\n",(0,r.jsxs)(n.li,{children:["You want to use ",(0,r.jsx)(n.a,{href:"/configuration/transforms",children:"Transformers"})," (other than what is provided for ",(0,r.jsx)(n.a,{href:"/configuration/transforms/musicbrainz/#env-configuration",children:"Musicbrainz ENV"}),")"]}),"\n",(0,r.jsx)(n.li,{children:"You want complete control over all potential MS configuration"}),"\n"]})]}),(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:'AIO config is the "complete" way to configure MS.'}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["All settings available in File/ENV are available in AIO and AIO has ",(0,r.jsx)(n.em,{children:"more"})," exclusive settings."]}),"\n",(0,r.jsxs)(n.li,{children:["AIO enables configuring ",(0,r.jsx)(n.strong,{children:"default settings"})," for all Sources/Clients"]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["There is a ",(0,r.jsx)(n.a,{href:"/configuration/kitchensink",children:(0,r.jsx)(n.strong,{children:"kitchensink example"})})," that provides examples of using all sources/clients in a complex configuration."]}),"\n",(0,r.jsxs)(n.li,{children:["Use the ",(0,r.jsx)(n.a,{href:"/playground",children:(0,r.jsx)(n.strong,{children:"Config Playground"})})," to explore the entire config schema with an editable example","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["The example used can also be found in the repo directory at ",(0,r.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/tree/master/config/config.json.example",children:(0,r.jsx)(n.code,{children:"/config/config.json.example"})})," in the example below"]}),"\n"]}),"\n"]}),"\n"]})}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Config Example"}),(0,r.jsx)(c(),{title:"CONFIG_DIR/config.json",language:"json5",children:'{\n "debugMode": false,\n "disableWeb": false,\n "sourceDefaults": {\n "logPayload": false,\n "logFilterFailure": "warn",\n "logPlayerState": false,\n "scrobbleThresholds": {\n "duration": 30,\n "percent": 50\n },\n "maxPollRetries": 1,\n "maxRequestRetries": 1,\n "retryMultiplier": 1.5\n },\n "clientDefaults": {\n "maxRequestRetries": 1,\n "retryMultiplier": 1.5\n },\n "sources": [\n {\n "type": "spotify",\n "enable": true,\n "clients": ["myConfig"],\n "name": "Cool Spotify Name",\n "id": "mySpotifyId",\n "data": {\n "clientId": "a89cba1569901a0671d5a9875fed4be1",\n "clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",\n "redirectUri": "http://localhost:9078/callback"\n }\n }\n ],\n "clients": [\n {\n "type": "maloja",\n "enable": true,\n "name": "Foxx Maloja",\n "id": "myMalojaId",\n "data": {\n "url": "http://localhost:42010",\n "apiKey": "myMalojaKey"\n }\n }\n ],\n "webhooks": [\n {\n "name": "FirstGotifyServer",\n "type": "gotify",\n "url": "http://localhost:8070",\n "token": "MyGotifyToken",\n "priorities": {\n "info": 5,\n "warn": 7,\n "error": 10\n }\n },\n {\n "type": "ntfy",\n "name": "MyNtfyFriendlyNameForLogs",\n "url": "http://localhost:9991",\n "topic": "MyMultiScrobblerTopic",\n "username": "Optional",\n "password": "Optional",\n "priorities": {\n "info": 3,\n "warn": 4,\n "error": 5\n }\n }\n ]\n}\n'})]}),(0,r.jsxs)(a,{type:"important",summary:"Source/Client AIO Configs require a 'type' property",children:[(0,r.jsxs)(n.p,{children:["Compared to their ",(0,r.jsx)(n.a,{href:"./?configType=file#configuration-types",children:"File"})," equivalent, Sources/Clients configured using AIO require an additional ",(0,r.jsx)(n.code,{children:"type"})," property."]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="config.json"',children:'{\n "sources": [\n {\n "name": "Foxx JF Server",\n // highlight-start\n "type: "jellyfin",\n // highlight-end\n "id": "foxxJf",\n "data": {\n "url": "http://localhost:8096",\n "user": "FoxxMD",\n "apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"\n }\n }\n ]\n}\n'})}),(0,r.jsxs)(n.p,{children:["You can find an example of the required ",(0,r.jsx)(n.code,{children:"type"})," value by referring to the ",(0,r.jsx)(n.strong,{children:"AIO"})," tab in each Source/Client's Configuration section example. This is the same value that is used as the name of individual ",(0,r.jsx)(n.a,{href:"./?configType=file#configuration-types",children:"File"})," config files."]})]})]})]}),"\n",(0,r.jsx)(n.h2,{id:"setup-sources-and-clients",children:"Setup Sources and Clients"}),"\n",(0,r.jsxs)(n.p,{children:["See the ",(0,r.jsx)(n.a,{href:"#configuration-types",children:"Configuration Types"})," above for your options for creating Source and Client configurations."]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Reference ",(0,r.jsx)(n.a,{href:"/configuration/sources",children:(0,r.jsx)(n.strong,{children:"Scrobble Sources"})})," for what Sources are available"]}),"\n",(0,r.jsxs)(n.li,{children:["Reference ",(0,r.jsx)(n.a,{href:"/configuration/clients",children:(0,r.jsx)(n.strong,{children:"Scrobble Clients"})})," for what Clients are available"]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Each entry for a Source/Client includes a ",(0,r.jsx)(n.strong,{children:"Configuration"})," section that describes how to configure it using a ",(0,r.jsx)(n.a,{href:"#configuration-types",children:"configuration type"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"secrets-interpolation",children:"Secrets Interpolation"}),"\n",(0,r.jsxs)(n.p,{children:["When using ",(0,r.jsx)(n.a,{href:"./?configType=file#configuration-types",children:"File"})," or ",(0,r.jsx)(n.a,{href:"./?configType=aio#configuration-types",children:"AIO"})," Configuration, Multi-Scrobbler can interpolate Environmental Variables into your json files. This can be used, for example, to keep sensitive data (like ",(0,r.jsx)(n.a,{href:"/configuration/clients/lastfm#configuration",children:"Last.fm Client/Secret"}),") out of your configuration files so that they can be committed to git."]}),"\n",(0,r.jsxs)(n.p,{children:["Multi-scrobbler will look for patterns in text fields within ",(0,r.jsx)(n.em,{children:"all"})," of your json files:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Some part of the text field matches: ",(0,r.jsx)(n.code,{children:"[[MY_ENV]]"})]}),"\n",(0,r.jsxs)(n.li,{children:["Is replaced by the value of the Environmental Variable named ",(0,r.jsx)(n.code,{children:"MY_ENV"})]}),"\n"]}),"\n",(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(n.p,{children:["Given this ",(0,r.jsx)(n.a,{href:"/configuration/clients/lastfm#configuration",children:"Last.fm"})," ",(0,r.jsx)(n.a,{href:"./?configType=file#configuration-types",children:"File"})," config:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="lastfm.json"',children:'[\n {\n "name": "myLastFmClient",\n "configureAs": "client",\n "data": {\n "apiKey": "[[MY_APIKEY]]",\n "secret": "[[MY_SECRET]]",\n "redirectUri": "http://localhost:9078/lastfm/callback"\n }\n }\n]\n'})}),(0,r.jsxs)(n.p,{children:["And these environmental variables, in this scenario set through ",(0,r.jsx)(n.code,{children:"environment"})," in the ",(0,r.jsx)(n.a,{href:"/quickstart#create-docker-compose-file",children:"docker compose installation"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"MY_APIKEY=a89cba1569901a0671d5a9875fed4be1\nMY_SECRET=ec42e09d5ae0ee0f0816ca151008412a\n"})}),(0,r.jsx)(n.p,{children:"The resulting json multi-scrobbler would use:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="lastfm.json"',children:'[\n {\n "name": "myLastFmClient",\n "configureAs": "client",\n "data": {\n "apiKey": "a89cba1569901a0671d5a9875fed4be1",\n "secret": "ec42e09d5ae0ee0f0816ca151008412a",\n "redirectUri": "http://localhost:9078/lastfm/callback"\n }\n }\n]\n'})})]}),"\n",(0,r.jsx)(a,{type:"note",summary:"Caveats",children:(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"ENV variable names/interpolation keys are case-insensitive"}),"\n",(0,r.jsxs)(n.li,{children:["Interpolation only works for ",(0,r.jsx)(n.strong,{children:"string"})," values within json. This cannot be used for numbers, booleans, objects, etc..."]}),"\n"]})}),"\n",(0,r.jsxs)(a,{type:"warning",summary:"Missing ENVs",children:[(0,r.jsxs)(n.p,{children:["Multi-scrobbler ",(0,r.jsx)(n.strong,{children:"will not"})," throw an error if the environmental value is not found. Instead, it will leave the string as-is and log a warning (",(0,r.jsx)(n.code,{children:"WARN"})," level) with the names of the missing environmental variable names like so:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"WARN : [App] [Sources] [spotify Secrets] Matched: None | Unmatched: SPOTIFY_SECRET\n"})})]}),"\n",(0,r.jsx)(a,{type:"warning",summary:"ENV Name Collisions",children:(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Verify that interpolation keys/environmental variable names you will use do not collide with existing ENV names used by multi-scrobbler."})," Use the docs search to verify the name you want to use is not already used elsewhere by multi-scrobbler."]})}),"\n",(0,r.jsx)(n.h2,{id:"base-url",children:"Base URL"}),"\n",(0,r.jsxs)(n.p,{children:["Defines the URL that is used to generate default redirect URLs for authentication for services like ",(0,r.jsx)(n.a,{href:"/configuration/sources/spotify",children:"spotify"})," and ",(0,r.jsx)(n.a,{href:"/configuration/clients/lastfm",children:"lastfm"})," -- as well as some logging hints."]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Default => ",(0,r.jsx)(n.code,{children:"http://localhost:9078"})]}),"\n",(0,r.jsxs)(n.li,{children:["Set with ",(0,r.jsx)(n.a,{href:"./?configType=env#configuration-types",children:"ENV"})," ",(0,r.jsx)(n.code,{children:"BASE_URL"})," or ",(0,r.jsx)(n.code,{children:"baseUrl"})," ",(0,r.jsx)(n.a,{href:"./?configType=aio#configuration-types",children:"all-in-one configuration"})]}),"\n",(0,r.jsxs)(n.li,{children:["If protocol is ",(0,r.jsx)(n.code,{children:"http"})," or no protocol is specified MS will try to use port ",(0,r.jsx)(n.code,{children:"9078"})," -- to override this explicitly set the port or use ",(0,r.jsx)(n.code,{children:"https"})]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Useful when running with ",(0,r.jsx)(n.a,{href:"/multi-scrobbler/installation/#docker",children:"docker"})," so that you do not need to specify redirect URLs for each configuration."]}),"\n",(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(n.p,{children:["EX Lastfm Redirect Url is ",(0,r.jsx)(n.code,{children:"BASE_URL:PORT/lastfm/callback"})," (when no other redirectUri is specified for ",(0,r.jsx)(n.a,{href:"/configuration/clients/lastfm",children:"lastfm configuration"}),")"]}),(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:(0,r.jsx)(n.code,{children:"BASE_URL"})}),(0,r.jsx)(n.th,{children:"Redirect URL"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"192.168.0.101"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"http://192.168.0.101:9078/lastfm/callback"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"http://my.domain.local"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"http://my.domain.local:9078/lastfm/callback"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"http://192.168.0.101/my/subfolder"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"http://192.168.0.101:9078/my/subfolder/lastfm/callback"})})]})]})]}),(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:(0,r.jsx)(n.code,{children:"BASE_URL"})}),(0,r.jsx)(n.th,{children:"Redirect URL"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"my.domain.local:80"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"http://192.168.0.101:80/lastfm/callback"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"my.domain.local:9000"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"http://my.domain.local:9000/lastfm/callback"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"192.168.0.101:4000/my/subfolder"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"http://192.168.0.101:4000/my/subfolder/lastfm/callback"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"https://192.168.0.101"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"https://192.168.0.101:443/lastfm/callback"})})]})]})]})]}),"\n",(0,r.jsx)(n.h2,{id:"caching",children:"Caching"}),"\n",(0,r.jsx)(n.p,{children:"Multi-scrobbler implements caching to reduce external API calls and make some actions faster."}),"\n",(0,r.jsxs)(n.p,{children:["A default ",(0,r.jsx)(n.strong,{children:"in-memory"})," cache store is used so that you always benefit from some caching. An optional, ",(0,r.jsxs)(n.a,{href:"#secondary-caching",children:[(0,r.jsx)(n.strong,{children:"secondary"})," store"]})," can be configured for greater caching capabilities."]}),"\n",(0,r.jsx)(a,{type:"note",summary:"What is Cached?",children:(0,r.jsxs)(l.A,{groupId:"cachedThings",children:[(0,r.jsx)(o.A,{value:"auth",label:"Auth Data",children:(0,r.jsx)(n.p,{children:"Authentication sessions/tokens/etc... are cached for quicker requests and for persistence across restarts."})}),(0,r.jsx)(o.A,{value:"transform",label:"Scrobble Transforms",children:(0,r.jsxs)(n.p,{children:["The results of ",(0,r.jsx)(n.a,{href:"/configuration/transforms",children:"transform rules"})," are cached so that if a scrobble with identical data (track/artists/album) is identified and it has the same set of transform rules then the cached transform results can be applied."]})}),(0,r.jsx)(o.A,{value:"metadata",label:"Transform API Calls",children:(0,r.jsxs)(n.p,{children:["API Calls to external (metadata) services used to ",(0,r.jsx)(n.a,{href:"/configuration/transforms",children:"Enhance Scrobbles"}),", like calls to ",(0,r.jsx)(n.a,{href:"/configuration/transforms/musicbrainz",children:"Musicbrainz"}),", can be cached to avoid duplicate calls and speed up scrobble transformations."]})})]})}),"\n",(0,r.jsxs)(a,{type:"important",summary:"Auth Cache Configuration",children:[(0,r.jsxs)(n.p,{children:["Auth caching defaults to a ",(0,r.jsx)(n.strong,{children:"file"})," that is stored in the ",(0,r.jsx)(n.code,{children:"DATA_DIR"})," directory using the pre-defined file name ",(0,r.jsx)(n.code,{children:"ms-auth.cache"}),"."]}),(0,r.jsxs)(n.p,{children:["This provides automatic persistence across restarts for long-lived auth data/credentials if you have configured a ",(0,r.jsx)(n.a,{href:"/installation?dockerSetting=storage#recommended-settings",children:"persisted volume/bind mount"})," for configuration (",(0,r.jsx)(n.code,{children:"/config"})," is mounted in ",(0,r.jsx)(n.a,{href:"/quickstart#create-docker-compose-file",children:"docker compose"}),")."]}),(0,r.jsxs)(n.p,{children:["If you wish to use the ",(0,r.jsx)(n.a,{href:"#secondary-caching",children:"secondary store"})," for caching Auth you must explicitly configure it. This is because valkey can potentially be ",(0,r.jsx)(n.em,{children:"ephemeral"})," if you do not provide a volume for its data directory."]}),(0,r.jsx)(n.p,{children:"To explicitly configure auth to use the secondary store:"}),(0,r.jsxs)(l.A,{groupId:"configType",queryString:!0,children:[(0,r.jsx)(o.A,{value:"env",label:"ENV",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="compose.yaml"',children:"services:\n multi-scrobbler:\n # ...\n environment:\n // highlight-start\n - CACHE_AUTH=valkey\n // highlight-end\n # ...\n"})})}),(0,r.jsx)(o.A,{value:"aio",label:"AIO",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n "cache": {\n "auth": {\n "provider": "valkey"\n }\n },\n // ...\n}\n'})})})]})]}),"\n",(0,r.jsx)(n.h3,{id:"secondary-caching",children:"Secondary Caching Store"}),"\n",(0,r.jsx)(n.p,{children:"Using a secondary store enables:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"persistence of cached data across restarts"}),"\n",(0,r.jsx)(n.li,{children:"a larger store (more data is saved)"}),"\n",(0,r.jsx)(n.li,{children:"a longer time-to-live in the store (cached data is fetchable for a longer period)"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["These benefits are particularly beneficial when using ",(0,r.jsx)(n.a,{href:"/configuration/transforms",children:"transforms"})," like Musicbrainz and it is ",(0,r.jsx)(n.strong,{children:"strongly recommended"})," for these scenarios."]}),"\n",(0,r.jsxs)(n.p,{children:["Currently, Multi-scrobbler only supports ",(0,r.jsx)(n.a,{href:"https://valkey.io/",children:(0,r.jsx)(n.strong,{children:"Valkey"})}),", an open-source fork of Redis, as a secondary store."]}),"\n",(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Adding Valkey container to multi-scrobbler"}),(0,r.jsxs)(n.p,{children:["A valkey container can be added to the ",(0,r.jsx)(n.a,{href:"/installation?runType=docker-compose#docker",children:"multi-scrobbler docker compose stack"}),":"]}),(0,r.jsxs)(l.A,{children:[(0,r.jsx)(o.A,{value:"named",label:"Named Volume",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n # ...\n // highlight-start\n valkey:\n image: valkey/valkey\n volumes:\n - valkeydata:/data\n\nvolumes:\n valkeydata:\n driver: local\n // highlight-end\n"})})}),(0,r.jsx)(o.A,{value:"bind",label:"Bind Mount",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n # ...\n // highlight-start\n valkey:\n image: valkey/valkey\n volumes:\n - ./valkeyData:/data\n // highlight-end\n"})})})]}),(0,r.jsxs)(n.p,{children:["Use ",(0,r.jsx)(n.code,{children:"redis://valkey:6379"})," as the connection string in the configurations below."]})]}),"\n",(0,r.jsx)(n.p,{children:"The connection string used by multi-scrobbler to connect to your Valkey instance must be in the form:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"redis://HOST_IP:HOST_PORT\n"})}),"\n",(0,r.jsxs)(l.A,{groupId:"configType",queryString:!0,children:[(0,r.jsx)(o.A,{value:"env",label:"ENV",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="compose.yaml"',children:"services:\n multi-scrobbler:\n # ...\n environment:\n // highlight-start\n - CACHE_VALKEY=redis://192.168.0.120:6379\n // highlight-end\n # ...\n"})})}),(0,r.jsxs)(o.A,{value:"aio",label:"AIO",children:[(0,r.jsx)(n.p,{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n "cache": {\n "valkey": "redis://192.168.0.120:6379"\n },\n // ...\n}\n'})})]})]}),"\n",(0,r.jsx)(n.h2,{id:"database",children:"Database"}),"\n",(0,r.jsxs)(n.p,{children:["Multi-scrobbler depends on a SQLite database (",(0,r.jsx)(n.code,{children:"ms.db"}),") that is created on first run and stored in the ",(0,r.jsx)(n.a,{href:"/installation/?dockerSetting=storage#recommended-settings",children:(0,r.jsx)(n.code,{children:"CONFIG_DIR"})}),". When upgrading Multi-scrobbler version, if there are any required database changes than this database is ",(0,r.jsx)(n.a,{href:"/updating#database",children:"automatically backed up and migrated."})]}),"\n",(0,r.jsxs)(n.p,{children:["The database stores ",(0,r.jsx)(n.em,{children:"all"})," Plays for your Sources/Clients as well as metadata and debugging information to help troubleshoot issues. Each Play is associated with a Source/Config in the database based on your configuration."]}),"\n",(0,r.jsxs)(n.p,{children:["You ",(0,r.jsx)(n.strong,{children:"must set IDs for each Source/Client"})," so that the database can identify these even when the configuration is changed."]}),"\n",(0,r.jsx)(n.h3,{id:"retention",children:"Retention"}),"\n",(0,r.jsxs)(a,{status:"note",summary:"How much storage does the database use?",children:[(0,r.jsx)(n.p,{children:"The amount of data stored for each Play can widely vary based on a few factors:"}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"how much data the Source service exposes"}),"\n",(0,r.jsx)(n.li,{children:"how many clients each Source is scrobble to (Plays are duplicated for each Client a Source sends a scrobble to)"}),"\n",(0,r.jsxs)(n.li,{children:["if you are using any ",(0,r.jsx)(n.a,{href:"/configuration/transforms",children:"Transforms"})," the diff of each step is stored, along with any external request/response data (like ",(0,r.jsx)(n.a,{href:"/configuration/transforms/musicbrainz",children:"Musicbrainz queries"}),")"]}),"\n"]}),(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/tests/database/drizzle.test.ts#L551",children:"MS repository contains a benchmark"})," to measure an average database size in a few common scenarios."]}),(0,r.jsxs)(l.A,{groupId:"dbSize",children:[(0,r.jsxs)(o.A,{label:"Minimal",value:"minimal",children:[(0,r.jsx)(n.p,{children:"Assuming your Sources send a minimal amount of data or you have compacted all plays:"}),(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Play Count"}),(0,r.jsx)(n.th,{children:"DB Size"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"100"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"160kb"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"1000"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"1MB"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"10k"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"10.2MB"})})]})]})]})]}),(0,r.jsxs)(o.A,{label:"With Original Input",value:"input",children:[(0,r.jsx)(n.p,{children:"Assuming your Sources have a non-trivial amount of input data (like Spotify or Listenbrainz) that is not compacted:"}),(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Play Count"}),(0,r.jsx)(n.th,{children:"DB Size"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"100"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"356kb"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"1000"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"3MB"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"10k"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"29.6MB"})})]})]})]})]}),(0,r.jsxs)(o.A,{label:"All Debug Deta",value:"all",children:[(0,r.jsx)(n.p,{children:"Assuming your Sources/Clients:"}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"have a non-trivial amount of input data (like Spotify or Listenbrainz)"}),"\n",(0,r.jsxs)(n.li,{children:["and has many ",(0,r.jsx)(n.a,{href:"/configuration/transforms",children:"Transforms"})," steps that include requests"]}),"\n",(0,r.jsx)(n.li,{children:"nothing is compacted"}),"\n"]}),(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Play Count"}),(0,r.jsx)(n.th,{children:"DB Size"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"100"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"684kb"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"1000"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"6.28MB"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"10k"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"61.3MB"})})]})]})]})]})]})]}),"\n",(0,r.jsx)(n.p,{children:"A retention policy can be configured to delete Plays, or unused debug data, from the database after a certain amount of time. If no configuration is provided then a default policy is used that should be reasonable for most users."}),"\n",(0,r.jsxs)(l.A,{groupId:"policy",queryString:!0,children:[(0,r.jsxs)(o.A,{label:"Compaction",value:"compaction",children:[(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.strong,{children:"Compaction"})," Retention Policy is used to delete different types of debug data from your stored Plays."]}),(0,r.jsx)(n.p,{children:"This is a useful way to reduce used storage space when you are not having problems with your Plays, or iterating on a configuration, that requires referencing all this extra data."}),(0,r.jsx)(n.p,{children:"There are two types of data that can be compacted (deleted from the Play):"}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"input"})," - this is the untouched data retrieved by Multi-scrobbler, from a Source, and used to generate a Play/scrobble. This can be used to reconstruct and replay a Play, when used from troubleshooting or reporting an issue"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"transform"})," - this is all of the steps generated by ",(0,r.jsx)(n.a,{href:"/configuration/transforms",children:"transforms"}),", the diff of the Play resulting from the step, and any request/responses used to complete the step"]}),"\n"]}),(0,r.jsxs)(n.admonition,{title:"Defaults",type:"note",children:[(0,r.jsx)(n.p,{children:"When no Compact configuration is provided, Multi-scrobbler uses this policy:"}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Compact (delete) ",(0,r.jsx)(n.code,{children:"input"})," and ",(0,r.jsx)(n.code,{children:"transform"})," data on all Plays after 3 days"]}),"\n"]})]}),(0,r.jsx)(n.h4,{id:"configuring-compaction-policy",children:"Configuring Compaction Policy"}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Details"}),(0,r.jsx)(n.p,{children:"Each value in the configuration properties below can be either"}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["a number of seconds EX ",(0,r.jsx)(n.code,{children:"3600"})," = 10 minutes"]}),"\n",(0,r.jsxs)(n.li,{children:["a unit of a common duration with the pattern ",(0,r.jsx)(n.code,{children:"X unit"})," EX","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"30 minutes"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"5 hours"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"2 days"})}),"\n"]}),"\n"]}),"\n"]}),(0,r.jsxs)(l.A,{groupId:"configType",queryString:!0,children:[(0,r.jsxs)(o.A,{value:"env",label:"ENV",children:[(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"COMPACT_PROPERTIES"})," - which properties to compact"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"RETENTION_COMPACT_AFTER"})," - Default to use for all Plays"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"RETENTION_COMPACT_COMPLETED_AFTER"})," - Compact only completed Plays after..."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"RETENTION_COMPACT_FAILED_AFTER"})," - Compact only failed Plays after..."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"RETENTION_COMPACT_DUPED_AFTER"})," - Compact only duped/discard Plays after..."]}),"\n"]}),(0,r.jsx)(n.p,{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ini",children:"# only delete input when compacting\nCOMPACT_PROPERTIES=input\n# compact all plays after 3 days \nRETENTION_COMPACT_AFTER=3 days\n# specifically compact completed plays after 30 minutes\nRETENTION_COMPACT_COMPLETED_AFTER=30 minutes\n"})})]}),(0,r.jsxs)(o.A,{value:"aio",label:"AIO",children:[(0,r.jsx)(n.p,{children:"Compacting all Play types and deleting both input and transform:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "database": {\n "retention": {\n "compactAfter": "3 days",\n "compact": [\n "input",\n "transform"\n ]\n }\n }\n}\n'})}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Delete only input during compacting"}),"\n",(0,r.jsx)(n.li,{children:"Compact all after 3 days except completed which compacts after 30 minutes"}),"\n"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "database": {\n "retention": {\n "compactAfter": {\n "completed": "30 minutes",\n "duped": "3 days",\n "failed": "3 days"\n },\n "compact": [\n "input"\n ]\n }\n }\n}\n'})})]})]})]})]}),(0,r.jsxs)(o.A,{label:"Deletion",value:"deletion",children:[(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.strong,{children:"Deletion"})," Retention Policy is used to delete different types of stored Plays from Multi-scrobbler database."]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"This does not delete Plays from your Clients."}),' It\'s only deleting the "in-flight" data MS used to create the scrobble that was eventually sent to your clients.']}),(0,r.jsxs)(n.admonition,{title:"Plays Should be Ephemeral",type:"warning",children:[(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Multi-Scrobbler is not designed to store Plays/Scrobbles indefinitely."})}),(0,r.jsx)(n.p,{children:"It should scale fine for thousands of scrobbles but it not meant to store 10's of thousands of scrobbles forever. It is not a scrobbler server."}),(0,r.jsxs)(n.p,{children:["You ",(0,r.jsx)(n.strong,{children:"should"})," set a reasonable deletion policy so that MS stores less than 1000 scrobbles at a time, ideally less."]})]}),(0,r.jsxs)(n.admonition,{title:"Defaults",type:"note",children:[(0,r.jsx)(n.p,{children:"When no Deletion policy configuration is provided, Multi-scrobbler uses this policy:"}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Delete all Plays after 7 days"}),"\n"]})]}),(0,r.jsx)(n.h4,{id:"configuring-deletion-policy",children:"Configuring Deletion Policy"}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Details"}),(0,r.jsx)(n.p,{children:"Each value in the configuration properties below can be either"}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["a number of seconds EX ",(0,r.jsx)(n.code,{children:"3600"})," = 10 minutes"]}),"\n",(0,r.jsxs)(n.li,{children:["a unit of a common duration with the pattern ",(0,r.jsx)(n.code,{children:"X unit"})," EX","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"30 minutes"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"5 hours"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"2 days"})}),"\n"]}),"\n"]}),"\n"]}),(0,r.jsxs)(l.A,{groupId:"configType",queryString:!0,children:[(0,r.jsxs)(o.A,{value:"env",label:"ENV",children:[(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"RETENTION_DELETE_AFTER"})," - Default to use for all Plays"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"RETENTION_DELETE_COMPLETED_AFTER"})," - Delete only completed Plays after..."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"RETENTION_DELETE_FAILED_AFTER"})," - Delete only failed Plays after..."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"RETENTION_DELETE_DUPED_AFTER"})," - Delete only duped/discard Plays after..."]}),"\n"]}),(0,r.jsx)(n.p,{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ini",children:"# delete all plays after 3 days \nRETENTION_DELETE_AFTER=3 days\n# specifically, delete completed plays after 30 minutes\nRETENTIOND_DELETE_COMPLETED_AFTER=30 minutes\n"})})]}),(0,r.jsxs)(o.A,{value:"aio",label:"AIO",children:[(0,r.jsx)(n.p,{children:"Deleting all Play types after 3 days:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "database": {\n "retention": {\n "deleteAfter": "3 days"\n }\n }\n}\n'})}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Delete all after 3 days except completed which are deleted after 30 minutes"}),"\n"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "database": {\n "retention": {\n "deleteAfter": {\n "completed": "30 minutes",\n "duped": "3 days",\n "failed": "3 days"\n },\n }\n }\n}\n'})})]})]})]})]})]}),"\n",(0,r.jsx)(n.h2,{id:"debug-mode",children:"Debug Mode"}),"\n",(0,r.jsx)(n.p,{children:"Turning on Debug Mode will"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:'override and enable all optional "debugging" options found in configuration'}),"\n",(0,r.jsx)(n.li,{children:"set log output to DEBUG"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"Use this as a shortcut for enabling output that can be used for troubleshooting and issue reporting. Be aware that logs will likely be VERY noisy while Debug Mode is on. You should only have this mode on while gathering logs for troubleshooting and then turn it off afterwards."}),"\n",(0,r.jsxs)(n.p,{children:["To set debug mode either add it to ",(0,r.jsxs)(n.a,{href:"./?configType=aio#configuration-types",children:["AIO ",(0,r.jsx)(n.code,{children:"config.json"})]})]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n "debugMode": true,\n "sources": [...],\n // ...\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["or set the ",(0,r.jsx)(n.a,{href:"./?configType=env#configuration-types",children:"ENV"})," ",(0,r.jsx)(n.code,{children:"DEBUG_MODE=true"})]}),"\n",(0,r.jsx)(n.h2,{id:"disable-web",children:"Disable Web"}),"\n",(0,r.jsx)(n.p,{children:"If you do not need the dashboard and/or ingress sources, or have security concerns about ingress and cannot control your hosting environment, the web server and API can be disabled."}),"\n",(0,r.jsx)(n.admonition,{type:"warning",children:(0,r.jsxs)(n.p,{children:["Any ",(0,r.jsxs)(n.strong,{children:[(0,r.jsx)(n.a,{href:"/configuration/sources?sourceComm=ingress#by-communication-method",children:"ingress-based sources"})," will be unusable"]})," (Webscrobbler, etc...) if this is disabled."]})}),"\n",(0,r.jsx)(n.p,{children:"Disable using either:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["ENV ",(0,r.jsx)(n.code,{children:"DISABLE_WEB=true"})]}),"\n",(0,r.jsxs)(n.li,{children:["In ",(0,r.jsx)(n.a,{href:"./?configType=aio#configuration-types",children:"All-in-One File"})," use the top-level property ",(0,r.jsx)(n.code,{children:'"disableWeb": true'})]}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"monitoring",children:"Monitoring"}),"\n",(0,r.jsx)(n.h3,{id:"upstream-services-status",children:"Upstream Services Status"}),"\n",(0,r.jsx)(n.p,{children:"A status page for monitoring the reachability of public services used by some Sources/Clients is available at"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:(0,r.jsx)(n.a,{href:"https://status.multi-scrobbler.app",children:"https://status.multi-scrobbler.app"})})}),"\n",(0,r.jsxs)(n.p,{children:["This monitor runs on a VPS and checks the uptime of ",(0,r.jsx)(n.em,{children:"actual"})," APIs for these services, not just landing pages. On a ~minute interval it checks:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Listenbrainz API"}),"\n",(0,r.jsx)(n.li,{children:"Musicbrainz API"}),"\n",(0,r.jsx)(n.li,{children:"CoverArtArchive API"}),"\n",(0,r.jsx)(n.li,{children:"Last.fm API"}),"\n",(0,r.jsx)(n.li,{children:"A few MS-hosted support services"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"webhook-configurations",children:"Webhook Configurations"}),"\n",(0,r.jsxs)(n.p,{children:["Webhooks will ",(0,r.jsx)(n.strong,{children:"push"})," a notification to your configured servers on these events:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Source polling started"}),"\n",(0,r.jsx)(n.li,{children:"Source polling retry"}),"\n",(0,r.jsx)(n.li,{children:"Source polling stopped on error"}),"\n",(0,r.jsx)(n.li,{children:"Scrobble client scrobble failure"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Webhooks are configured in the AIO ",(0,r.jsx)(n.a,{href:"#configuration-types",children:"config.json"})," file under the ",(0,r.jsx)(n.code,{children:"webhook"})," top-level property. Multiple webhooks may be configured for each webhook type."]}),"\n",(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n "sources": [\n //...\n ],\n "clients": [\n //...\n ],\n "webhooks": [\n {\n "name": "FirstGotifyServer",\n "type": "gotify",\n "url": "http://192.168.0.100:8070",\n "token": "abcd"\n },\n {\n "name": "SecondGotifyServer",\n "type": "gotify",\n //...\n },\n {\n "name": "NtfyServerOne",\n "type": "ntfy",\n //...\n },\n //...\n ]\n}\n'})})]}),"\n",(0,r.jsx)(n.h4,{id:"gotify",children:(0,r.jsx)(n.a,{href:"https://gotify.net/",children:"Gotify"})}),"\n",(0,r.jsxs)(n.p,{children:["Refer to the ",(0,r.jsx)(n.a,{href:"/playground",children:"config schema for GotifyConfig"})]}),"\n",(0,r.jsxs)(n.p,{children:["multi-scrobbler optionally supports setting message notification priority via ",(0,r.jsx)(n.code,{children:"info"})," ",(0,r.jsx)(n.code,{children:"warn"})," and ",(0,r.jsx)(n.code,{children:"error"})," mappings."]}),"\n",(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "type": "gotify",\n "name": "MyGotifyFriendlyNameForLogs",\n "url": "http://192.168.0.100:8070",\n "token": "AQZI58fA.rfSZbm",\n "priorities": {\n "info": 5,\n "warn": 7,\n "error": 10\n }\n}\n'})})]}),"\n",(0,r.jsx)(n.h4,{id:"ntfy",children:(0,r.jsx)(n.a,{href:"https://ntfy.sh/",children:"Ntfy"})}),"\n",(0,r.jsxs)(n.p,{children:["Refer to the ",(0,r.jsx)(n.a,{href:"/playground",children:"config schema for NtfyConfig"})]}),"\n",(0,r.jsxs)(n.p,{children:["multi-scrobbler optionally supports setting message notification priority via ",(0,r.jsx)(n.code,{children:"info"})," ",(0,r.jsx)(n.code,{children:"warn"})," and ",(0,r.jsx)(n.code,{children:"error"})," mappings."]}),"\n",(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "type": "ntfy",\n "name": "MyNtfyFriendlyNameForLogs",\n "url": "http://192.168.0.100:9991",\n "topic": "RvOwKJ1XtIVMXGLR",\n "username": "Optional",\n "password": "Optional",\n "priorities": {\n "info": 3,\n "warn": 4,\n "error": 5\n }\n}\n'})})]}),"\n",(0,r.jsx)(n.h4,{id:"apprise",children:(0,r.jsx)(n.a,{href:"https://github.com/caronc/apprise-api",children:"Apprise"})}),"\n",(0,r.jsxs)(n.p,{children:["Refer to the ",(0,r.jsx)(n.a,{href:"/playground",children:"config schema for AppriseConfig"})]}),"\n",(0,r.jsxs)(n.p,{children:["multi-scrobbler supports ",(0,r.jsx)(n.a,{href:"https://github.com/caronc/apprise-api?tab=readme-ov-file#stateless-solution",children:"stateless"})," and ",(0,r.jsx)(n.a,{href:"https://github.com/caronc/apprise-api?tab=readme-ov-file#persistent-storage-solution",children:"persistent storage"})," endpoints as well as ",(0,r.jsx)(n.a,{href:"https://github.com/caronc/apprise-api?tab=readme-ov-file#tagging",children:"tags"}),"/"]}),"\n",(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n "type": "apprise",\n "name": "MyAppriseFriendlyNameForLogs",\n "host": "http://192.168.0.100:8080",\n "urls": ["gotify://192.168.0.101:8070/MyToken"], // stateless endpoints\n "keys": ["e90b20526808373353afad7fb98a201198c0c3e0555bea19f182df3388af7b17"], //persistent storage endpoints\n "tags": ["my","optional","tags"]\n}\n'})})]}),"\n",(0,r.jsx)(n.h3,{id:"health-endpoint",children:"Health Endpoint"}),"\n",(0,r.jsxs)(n.p,{children:["An endpoint for monitoring the health of sources/clients is available at GET ",(0,r.jsx)(n.code,{children:"http://YourMultiScrobblerDomain/api/health"})]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Returns ",(0,r.jsx)(n.code,{children:"200 OK"})," when ",(0,r.jsx)(n.strong,{children:"everything"})," is working or ",(0,r.jsx)(n.code,{children:"500 Internal Server Error"})," if ",(0,r.jsx)(n.strong,{children:"anything"})," is not"]}),"\n",(0,r.jsxs)(n.li,{children:["The plain url (",(0,r.jsx)(n.code,{children:"/api/health"}),") aggregates status of ",(0,r.jsx)(n.strong,{children:"all clients/sources"})," -- so any failing client/source will make status return 500","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Use query params ",(0,r.jsx)(n.code,{children:"type"})," or ",(0,r.jsx)(n.code,{children:"name"})," to restrict client/sources aggregated IE ",(0,r.jsx)(n.code,{children:"/api/health?type=spotify"})," or ",(0,r.jsx)(n.code,{children:"/api/health?name=MyMaloja"})]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["On 500 the response returns a JSON payload with ",(0,r.jsx)(n.code,{children:"messages"})," array that describes any issues","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["For any clients/sources that require authentication ",(0,r.jsx)(n.code,{children:"/api/health"})," will return 500 if they are ",(0,r.jsx)(n.strong,{children:"not authenticated"})]}),"\n",(0,r.jsxs)(n.li,{children:["For sources that poll (spotify, yt music, subsonic) ",(0,r.jsx)(n.code,{children:"/api/health"})," will 500 if they are ",(0,r.jsx)(n.strong,{children:"not polling"})]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"prometheus",children:"Prometheus"}),"\n",(0,r.jsxs)(n.p,{children:["A ",(0,r.jsx)(n.a,{href:"https://prometheus.io/",children:"Prometheus"})," export endpoint is available at GET ",(0,r.jsx)(n.code,{children:"http://YourMultiScrobblerDomain/api/metrics"})]}),"\n",(0,r.jsx)(n.p,{children:"It includes metrics for:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Count of discovered plays per ",(0,r.jsx)(n.a,{href:"/configuration/sources",children:"Source"})]}),"\n",(0,r.jsxs)(n.li,{children:["Count of Queued/Scrobbled/Deadletter scrobbles per ",(0,r.jsx)(n.a,{href:"/configuration/clients",children:"Client"})]}),"\n",(0,r.jsxs)(n.li,{children:["Number of issues per Source/Client","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"If any of these metrics is > 0 it means your Source/Client is not operating normally"}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.li,{children:"Caching metrics"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Additionally, general process metrics like cpu and memory usage can be enabled with the env ",(0,r.jsx)(n.code,{children:"PROMETHEUS_FULL=true"})]}),"\n",(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Example Usage"}),(0,r.jsxs)(n.p,{children:["Update your ",(0,r.jsx)(n.a,{href:"https://prometheus.io/docs/prometheus/latest/configuration/configuration/#configuration-file",children:"Prometheus Configuration file"})," (usually at ",(0,r.jsx)(n.code,{children:"/etc/prometheus.yml"}),") to add a job to the ",(0,r.jsx)(n.code,{children:"scrape_configs"}),(0,r.jsx)(n.a,{href:"https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config",children:"section"})," like this:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"scrape_configs:\n #\n # ...your other jobs are here\n # \n - job_name: multi-scrobbler\n scrape_interval: 30s\n metrics_path: '/api/metrics'\n static_configs:\n - targets: ['myMultiscrobblerIp:9078']\n"})})]}),"\n",(0,r.jsxs)(a,{status:"tip",summary:"Grafana Dashboard",children:[(0,r.jsxs)(n.p,{children:["Github user ",(0,r.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/issues/507#issuecomment-4144982295",children:"4rft5"})," created a Grafana dashboard that uses the Prometheus metrics from above."]}),(0,r.jsx)("img",{src:i(6434).A,height:"800"}),(0,r.jsxs)(n.p,{children:["Download the json dashboard here: ",(0,r.jsx)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:i(3096).A+"",children:(0,r.jsx)(n.code,{children:"multiscrobbler-dashboard.json"})})]})]})]})}function f(e={}){let{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(j,{...e})}):j(e)}function p(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}},9461(e,n,i){i.d(n,{A:()=>h}),i(6540);var s=i(2170),r=i.n(s),t=i(5548),l=i(349),o=i(1219),a=i.n(o),c=i(4848);let d=e=>{let n,{data:i,name:s,client:l=!1}=e;try{n=a().parse(i)}catch(e){return console.error(e),(0,c.jsxs)(t.A,{type:"danger",title:"Unexpected Error",children:[(0,c.jsx)("p",{children:"Example component crashed because of error!"}),(0,c.jsx)(r(),{children:e.message})]})}n=(n=n.filter(e=>void 0===e.configureAs||e.configureAs===(l?"client":"source"))).map(e=>({...e,type:s}));let o={[l?"clients":"sources"]:n};return(0,c.jsx)(r(),{title:"CONFIG_DIR/config.json",language:"json5",children:JSON.stringify(o,null,2)})},h=e=>(0,c.jsx)(l.A,{fallback:({error:e})=>(0,c.jsx)("div",{children:(0,c.jsxs)("p",{children:["Example component crashed because of error: ",e.message,"."]})}),children:(0,c.jsx)(d,{...e})})},1672(e,n,i){i.d(n,{A:()=>l}),i(6540);var s=i(4164),r=i(8774),t=i(4848);function l({size:e=null,outline:n=!1,variant:i="primary",block:o=!1,disabled:a=!1,className:c,style:d={},link:h,label:u,onClick:x=()=>null,...j}){let f=e?({sm:"sm",small:"sm",lg:"lg",large:"lg",medium:null})[e]:"",p=f?`button--${f}`:"",m=i?`button--${i}`:"";return(0,t.jsx)(r.A,{to:a?null:h,children:(0,t.jsx)("button",{className:(0,s.A)("button",p,n?"button--outline":"",m,o?"button--block":"",a?"disabled":"",c),style:d,role:"button","aria-disabled":a,onClick:e=>{e.preventDefault(),x()},...j,children:u})})}},9643(e,n,i){i.d(n,{A:()=>a}),i(6540);var s=i(1672),r=i(4848);let t={borderRadius:"var(--ifm-button-border-radius) 0 0 var(--ifm-button-border-radius)"},l={borderRadius:"0 var(--ifm-button-border-radius) var(--ifm-button-border-radius) 0"},o={borderRadius:"0"};function a({size:e=null,variant:n="primary",block:i=!1,disabled:c=!1,className:d,style:h,options:u,value:x,defaultValue:j,onChange:f=()=>null}){if(void 0===u||0===u.length)return null;let p=u.map((i,a)=>{let c={style:o,outline:!0,size:e,variant:n,label:i[1],link:"#",onClick:()=>f(i[0])};return 0===a?c.style=t:a===u.length-1&&(c.style=l),(x??j)===i[0]&&(c.outline=!1),(0,r.jsx)(s.A,{...c},a)});return(0,r.jsx)("span",{children:p})}},8675(e,n,i){i.d(n,{A:()=>j});var s=i(6540),r=i(2170),t=i.n(r),l=i(349),o=i(8193);let a={};var c=i(9643),d=i(1922),h=i(4581),u=i(4848);let x=e=>{let{data:n,interactive:i=!0,defaultDisplay:r="interactive",schemaName:l,filePathName:x,readOnlyAtWidth:j=1,...f}=e,[p,m]=((e,n,i)=>{let r;return e in a?a[e]():o.A.canUseDOM?(a[e]=(r=[],()=>{let[t,l]=((e,n,i=!0)=>{let[r,t]=(0,s.useState)(()=>{let s=window.localStorage.getItem(e);if(i)try{return s?JSON.parse(s):n}catch(e){return console.error(e),n}try{let e;if(null===s)return n;if("true"===s)return!0;if("false"===s)return!1;if(e=s,("number"==typeof e||"string"==typeof e&&""!==e.trim())&&!isNaN(e)){if(s.includes("."))return parseFloat(s);return parseInt(s)}return s}catch(e){return console.error(e),n}}),l=(0,s.useCallback)(n=>{t(()=>(i?localStorage.setItem(e,JSON.stringify(n)):localStorage.setItem(e,n),n))},[e]);return(0,s.useEffect)(()=>{let i=i=>{i.storageArea===localStorage&&i.key===e&&t(null===i.newValue?n:i.newValue)};return window.addEventListener("storage",i),()=>window.removeEventListener("storage",i)}),[r,l]})(e,n,i),o=(0,s.useCallback)(e=>{for(let n of r)n(e)},[]);return(0,s.useEffect)(()=>(r.push(l),()=>{r.splice(r.indexOf(l),1)}),[l]),[t,o]}),a[e]()):[n,()=>null]})("docusaurus.tab.fileExampleShow",r,!1),g=(0,h.l)({desktopBreakpoint:j}),b=null,y=i&&"desktop"===g;y&&(b=(0,u.jsx)(c.A,{options:[["plain","Plain"],["interactive","Interactive"]],defaultValue:r,value:p,variant:"primary",size:"medium",onChange:e=>m(e)}));let v=null;void 0!==x&&y&&"interactive"===p&&(v=(0,u.jsx)("code",{style:{float:"right",paddingRight:"0.5em",paddingLeft:"0.5em"},children:x}));let E=null;(null!==b||null!==v)&&(E=(0,u.jsxs)("div",{style:{marginBottom:"1em"},children:[b,v]}));let A=(0,s.useMemo)(()=>y&&"interactive"===p?null:"string"!=typeof n?JSON.stringify(n,null,2):n,[i,p,n]);return(0,u.jsxs)(s.Fragment,{children:[E,y&&"plain"!==p?null:(0,u.jsx)(t(),{title:x,language:"json5",children:A}),y&&"interactive"===p?(0,u.jsx)(d.A,{configContent:n,schemaName:l}):null]})},j=e=>(0,u.jsx)(l.A,{fallback:({error:e})=>(0,u.jsx)("div",{children:(0,u.jsxs)("p",{children:["Example component crashed because of error: ",e.message,"."]})}),children:(0,u.jsx)(x,{...e})})},1922(e,n,i){i.d(n,{A:()=>f});var s=i(6540),r=i(6025),t=i(8478),l=i(5293),o=i(4020),a=i(8182),c=i.n(a),d=i(1219),h=i.n(d),u=i(4848);function x(e){let n=(0,s.useMemo)(()=>{if(void 0!==e.schema)return(0,o.IM)({schema:e.schema,ajvOptions:{strict:"log",strictSchema:"log",allowUnionTypes:!0},onCreateAjv:e=>(e.addKeyword("deprecationMessage"),c().default(e),e)})},[e.schema]),i=(0,s.useMemo)(()=>({json:"string"==typeof e.config?h().parse(e.config):e.config}),[e.config]),{colorMode:r}=(0,l.G)();return(0,u.jsx)(o.t$,{validator:n,theme:r,mode:"text",content:i,style:{height:"initial"}})}function j(e){return(0,u.jsx)(t.default,{fallback:(0,u.jsx)("div",{children:"Loading..."}),children:()=>(0,u.jsx)(x,{...e})})}function f(e){let{schemaName:n}=e,[i,t]=(0,s.useState)(void 0),[l,o]=(0,s.useState)(void 0),a=void 0===n?void 0:(0,r.Ay)(`schemas/${e.schemaName}.json`);return(0,s.useEffect)(()=>{void 0===a?t(!1):fetch(a,{headers:{Accept:"application/json"}}).then(e=>e.json()).then(e=>t(e)).catch(e=>t(e))},[t,a]),(0,u.jsxs)(s.Fragment,{children:[void 0===i&&(0,u.jsx)("div",{children:"Loading ..."}),void 0!==i&&i instanceof Error&&(0,u.jsxs)("div",{children:["Houston we have a problem : ",i.message]}),void 0!==i&&!(i instanceof Error)&&(0,u.jsx)(j,{schema:!1===i?void 0:i,config:e.configContent})]})}},2709(e,n,i){i.d(n,{A:()=>t});var s=i(6540),r=i(4848);let t=e=>{let{children:n,lower:i,client:t=!1}=e,l=n;void 0===l&&(l=(0,r.jsxs)(s.Fragment,{children:[i?"explore":"Explore"," the schema with an example and live editor/validator"]}));let o=`https://json-schema.app/view/%23/%23%2Fdefinitions%2F${e.objectName}`;return(0,r.jsx)("a",{target:"_blank",href:`${o}?url=${t?"https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fclient.json":"https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json"}`,children:l})}},3096(e,n,i){i.d(n,{A:()=>s});let s=i.p+"assets/files/multiscrobbler-dashboard-814b740dbbd83aba08598320999b9f67.json"},6434(e,n,i){i.d(n,{A:()=>s});let s=i.p+"assets/images/grafana-004a8d8d2ec395ad949e9f5b79406a32.png"}}]); \ No newline at end of file diff --git a/assets/js/9618.0168aa7c.js b/assets/js/9618.fc560b69.js similarity index 95% rename from assets/js/9618.0168aa7c.js rename to assets/js/9618.fc560b69.js index 4f42ce1d..b4d897f6 100644 --- a/assets/js/9618.0168aa7c.js +++ b/assets/js/9618.fc560b69.js @@ -1 +1 @@ -(()=>{"use strict";var r,e,t,s,o,n={4809(r,e,t){t.d(e,{BH:()=>o,Ho:()=>i,IH:()=>n,sx:()=>s}),t(8291);let s=[],o=["en"],n="search-index{dir}.json?_=b0837099",i=1}},i={};function u(r){var e=i[r];if(void 0!==e)return e.exports;var t=i[r]={exports:{}};return n[r](t,t.exports,u),t.exports}u.m=n,u.x=()=>{var r=u.O(void 0,["998"],()=>u(5893));return u.O(r)},u.n=r=>{var e=r&&r.__esModule?()=>r.default:()=>r;return u.d(e,{a:e}),e},u.d=(r,e)=>{for(var t in e)u.o(e,t)&&!u.o(r,t)&&Object.defineProperty(r,t,{enumerable:!0,get:e[t]})},u.f={},u.e=r=>Promise.all(Object.keys(u.f).reduce((e,t)=>(u.f[t](r,e),e),[])),u.u=r=>"assets/js/"+r+".f3efabc4.js",u.miniCssF=r=>""+r+".css",u.o=(r,e)=>Object.prototype.hasOwnProperty.call(r,e),r=[],u.O=(e,t,s,o)=>{if(t){o=o||0;for(var n=r.length;n>0&&r[n-1][2]>o;n--)r[n]=r[n-1];r[n]=[t,s,o];return}for(var i=1/0,n=0;n=o)&&Object.keys(u.O).every(r=>u.O[r](t[c]))?t.splice(c--,1):(a=!1,o"1.7.11",e=u.x,u.x=()=>u.e("998").then(e),u.gca=function(r){return r=({})[r]||r,u.p+u.u(r)},t={9618:1},u.f.i=(r,e)=>{t[r]||importScripts(u.p+u.u(r))},o=(s=self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push.bind(s),s.push=r=>{var e=r[0],s=r[1],n=r[2];for(var i in s)u.o(s,i)&&(u.m[i]=s[i]);for(n&&n(u);e.length;)t[e.pop()]=1;o(r)},u.ruid="bundler=rspack@1.7.11",u.x()})(); \ No newline at end of file +(()=>{"use strict";var r,e,t,s,o,n={4809(r,e,t){t.d(e,{BH:()=>o,Ho:()=>i,IH:()=>n,sx:()=>s}),t(8291);let s=[],o=["en"],n="search-index{dir}.json?_=45c4bfd5",i=1}},i={};function u(r){var e=i[r];if(void 0!==e)return e.exports;var t=i[r]={exports:{}};return n[r](t,t.exports,u),t.exports}u.m=n,u.x=()=>{var r=u.O(void 0,["998"],()=>u(5893));return u.O(r)},u.n=r=>{var e=r&&r.__esModule?()=>r.default:()=>r;return u.d(e,{a:e}),e},u.d=(r,e)=>{for(var t in e)u.o(e,t)&&!u.o(r,t)&&Object.defineProperty(r,t,{enumerable:!0,get:e[t]})},u.f={},u.e=r=>Promise.all(Object.keys(u.f).reduce((e,t)=>(u.f[t](r,e),e),[])),u.u=r=>"assets/js/"+r+".f3efabc4.js",u.miniCssF=r=>""+r+".css",u.o=(r,e)=>Object.prototype.hasOwnProperty.call(r,e),r=[],u.O=(e,t,s,o)=>{if(t){o=o||0;for(var n=r.length;n>0&&r[n-1][2]>o;n--)r[n]=r[n-1];r[n]=[t,s,o];return}for(var i=1/0,n=0;n=o)&&Object.keys(u.O).every(r=>u.O[r](t[c]))?t.splice(c--,1):(a=!1,o"1.7.11",e=u.x,u.x=()=>u.e("998").then(e),u.gca=function(r){return r=({})[r]||r,u.p+u.u(r)},t={9618:1},u.f.i=(r,e)=>{t[r]||importScripts(u.p+u.u(r))},o=(s=self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push.bind(s),s.push=r=>{var e=r[0],s=r[1],n=r[2];for(var i in s)u.o(s,i)&&(u.m[i]=s[i]);for(n&&n(u);e.length;)t[e.pop()]=1;o(r)},u.ruid="bundler=rspack@1.7.11",u.x()})(); \ No newline at end of file diff --git a/assets/js/runtime~main.c1425198.js b/assets/js/runtime~main.dd061ccb.js similarity index 69% rename from assets/js/runtime~main.c1425198.js rename to assets/js/runtime~main.dd061ccb.js index 2df0050f..04769e87 100644 --- a/assets/js/runtime~main.c1425198.js +++ b/assets/js/runtime~main.dd061ccb.js @@ -1 +1 @@ -(()=>{"use strict";var e,a,d,c,f,b,t,r={},o={};function n(e){var a=o[e];if(void 0!==a)return a.exports;var d=o[e]={exports:{}};return r[e].call(d.exports,d,d.exports,n),d.exports}n.m=r,n.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return n.d(a,{a:a}),a},a=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,n.t=function(d,c){if(1&c&&(d=this(d)),8&c||"object"==typeof d&&d&&(4&c&&d.__esModule||16&c&&"function"==typeof d.then))return d;var f=Object.create(null);n.r(f);var b={};e=e||[null,a({}),a([]),a(a)];for(var t=2&c&&d;("object"==typeof t||"function"==typeof t)&&!~e.indexOf(t);t=a(t))Object.getOwnPropertyNames(t).forEach(e=>{b[e]=()=>d[e]});return b.default=()=>d,n.d(f,b),f},n.d=(e,a)=>{for(var d in a)n.o(a,d)&&!n.o(e,d)&&Object.defineProperty(e,d,{enumerable:!0,get:a[d]})},n.f={},n.e=e=>Promise.all(Object.keys(n.f).reduce((a,d)=>(n.f[d](e,a),a),[])),n.u=e=>"assets/js/"+(({106:"17896441",1184:"e6ac3f64",1234:"138e0e15",1309:"153c3ceb",1353:"e7a53bbd",1499:"24a5b6e6",1668:"5e95c892",1703:"e82a470f",1785:"c2e935db",1920:"294f31e2",2068:"202c4968",2072:"425b8dd3",2076:"91a78c9f",2077:"d53edd15",232:"af9480c3",2351:"f18fd350",2609:"1bb2dc41",2613:"778b0fa6",2677:"4edc808e",2929:"61856a46",3056:"a7456010",3747:"aba21aa0",3763:"cdda7d53",3841:"41cd626a",3895:"64837553",3922:"5a378701",3963:"8b39be93",4123:"925abbc7",429:"33c4ed3f",4461:"04d926a5",4479:"490e20a0",4787:"7d837a1d",490:"9bcab3db",5005:"common",5077:"30829cd8",5093:"1fb61236",539:"dfa6d5b1",5515:"a16703cd",5575:"a7bd4aaa",5614:"fa357871",5847:"1a4e3797",6412:"3827a595",6473:"2ecbbad4",6691:"b46f7c12",676:"49117d77",6785:"a94703ab",6942:"44b2647e",7211:"5a6ebb5c",7212:"a93ae359",7220:"d50915e5",7227:"72d905a9",7234:"14eb3368",7314:"74876495",7417:"08938dd7",7430:"498c77bd",7736:"bf69edfb",8037:"d3a18f08",8123:"73287945",8160:"ad895e75",8270:"094000d3",8406:"0ce74438",8968:"ea69cd8d",899:"2a24ad03",9077:"5930ad03",9212:"9fd2a592",9249:"b49ba48c",941:"493c00ca",9494:"7b4f75c2",9515:"4f7d60c5",9536:"7ebd3339",9627:"fee59995",9698:"97d34c1a",9818:"5f7452e4",9991:"812e048c"})[e]||e)+"."+({106:"366a20db",1184:"065ee538",1234:"270f5136",1309:"8bccd2a2",1353:"caba3573",1499:"a9742041",1649:"9badff1a",1668:"dbe123fe",1703:"24cf7fbc",1785:"ed551359",1920:"fad893ae",2068:"da650089",2072:"cea8b1a2",2076:"055787b3",2077:"6f64cded",232:"1ae1b28b",2351:"891e8d11",2609:"11f59e56",2613:"f419d76e",2677:"01dc77ad",2929:"bc75c3ee",3056:"728c2f65",3747:"34582f97",3763:"5b8b0a9c",3841:"990a1161",3895:"d76c7c42",3922:"f804e785",3963:"eb6d9d58",4123:"c3c17436",429:"96b3612f",434:"3522035b",4461:"6d08a85e",4479:"2f2af6ce",4494:"30732283",4787:"874db7a3",490:"4ec17ca4",5005:"83d91827",5077:"9087dd14",5093:"1db46805",5314:"0d8b1cbe",5387:"a22894e6",539:"bed44827",5515:"a3295a26",5575:"97114100",5614:"b16bfd0d",5730:"53f66bf8",5847:"3d615296",6412:"3cd1fc75",6473:"b0403b88",6691:"3100fbe6",676:"ecb7b006",6785:"97949813",6942:"35013e97",7211:"0af53e22",7212:"171f1231",7220:"3f036d92",7227:"6c664fe1",7234:"050b12c6",7314:"3bfa055b",7417:"47711a14",7430:"493fa519",7736:"c50ccff0",8037:"ea51fc4e",8123:"9295af0b",8160:"13e7b34f",8270:"256efb93",8406:"e2243fa5",8968:"fbc19d6c",899:"f7468792",9077:"bc7160d6",92:"e59cb17d",9212:"3a67aa4f",9249:"e798877e",9311:"f9221018",9313:"dc290399",941:"d188969b",9494:"2b8c9d89",9515:"96d59325",9536:"c3c05102",9618:"0168aa7c",9627:"19421a19",9698:"ed4d7083",9818:"26462546",9991:"b5db2814",9999:"64bb008b"})[e]+".js",n.miniCssF=e=>""+e+".css",n.g=(()=>{if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}})(),n.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),d={},n.l=function(e,a,c,f){if(d[e])return void d[e].push(a);if(void 0!==c)for(var b,t,r=document.getElementsByTagName("script"),o=0;o{"u">typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},c=[],n.O=(e,a,d,f)=>{if(a){f=f||0;for(var b=c.length;b>0&&c[b-1][2]>f;b--)c[b]=c[b-1];c[b]=[a,d,f];return}for(var t=1/0,b=0;b=f)&&Object.keys(n.O).every(e=>n.O[e](a[o]))?a.splice(o--,1):(r=!1,f"1.7.11",n.gca=function(e){return e=({0x11113f9:"106",0x3dd57b1:"3895",0x45e4909:"8123",0x476864f:"7314",af9480c3:"232","33c4ed3f":"429","9bcab3db":"490",dfa6d5b1:"539","49117d77":"676","2a24ad03":"899","493c00ca":"941",e6ac3f64:"1184","138e0e15":"1234","153c3ceb":"1309",e7a53bbd:"1353","24a5b6e6":"1499","5e95c892":"1668",e82a470f:"1703",c2e935db:"1785","294f31e2":"1920","202c4968":"2068","425b8dd3":"2072","91a78c9f":"2076",d53edd15:"2077",f18fd350:"2351","1bb2dc41":"2609","778b0fa6":"2613","4edc808e":"2677","61856a46":"2929",a7456010:"3056",aba21aa0:"3747",cdda7d53:"3763","41cd626a":"3841","5a378701":"3922","8b39be93":"3963","925abbc7":"4123","04d926a5":"4461","490e20a0":"4479","7d837a1d":"4787",common:"5005","30829cd8":"5077","1fb61236":"5093",a16703cd:"5515",a7bd4aaa:"5575",fa357871:"5614","1a4e3797":"5847","3827a595":"6412","2ecbbad4":"6473",b46f7c12:"6691",a94703ab:"6785","44b2647e":"6942","5a6ebb5c":"7211",a93ae359:"7212",d50915e5:"7220","72d905a9":"7227","14eb3368":"7234","08938dd7":"7417","498c77bd":"7430",bf69edfb:"7736",d3a18f08:"8037",ad895e75:"8160","094000d3":"8270","0ce74438":"8406",ea69cd8d:"8968","5930ad03":"9077","9fd2a592":"9212",b49ba48c:"9249","7b4f75c2":"9494","4f7d60c5":"9515","7ebd3339":"9536",fee59995:"9627","97d34c1a":"9698","5f7452e4":"9818","812e048c":"9991"})[e]||e,n.p+n.u(e)},n.b=document.baseURI||self.location.href,f={4014:0,9783:0},n.f.j=function(e,a){var d=n.o(f,e)?f[e]:void 0;if(0!==d)if(d)a.push(d[2]);else if(/^(4014|9783)$/.test(e))f[e]=0;else{var c=new Promise((a,c)=>d=f[e]=[a,c]);a.push(d[2]=c);var b=n.p+n.u(e),t=Error();n.l(b,function(a){if(n.o(f,e)&&(0!==(d=f[e])&&(f[e]=void 0),d)){var c=a&&("load"===a.type?"missing":a.type),b=a&&a.target&&a.target.src;t.message="Loading chunk "+e+" failed.\n("+c+": "+b+")",t.name="ChunkLoadError",t.type=c,t.request=b,d[1](t)}},"chunk-"+e,e)}},n.O.j=e=>0===f[e],b=(e,a)=>{var d,c,b=a[0],t=a[1],r=a[2],o=0;if(b.some(e=>0!==f[e])){for(d in t)n.o(t,d)&&(n.m[d]=t[d]);if(r)var i=r(n)}for(e&&e(a);o{"use strict";var e,a,d,c,f,t,r,b={},o={};function n(e){var a=o[e];if(void 0!==a)return a.exports;var d=o[e]={exports:{}};return b[e].call(d.exports,d,d.exports,n),d.exports}n.m=b,n.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return n.d(a,{a:a}),a},a=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,n.t=function(d,c){if(1&c&&(d=this(d)),8&c||"object"==typeof d&&d&&(4&c&&d.__esModule||16&c&&"function"==typeof d.then))return d;var f=Object.create(null);n.r(f);var t={};e=e||[null,a({}),a([]),a(a)];for(var r=2&c&&d;("object"==typeof r||"function"==typeof r)&&!~e.indexOf(r);r=a(r))Object.getOwnPropertyNames(r).forEach(e=>{t[e]=()=>d[e]});return t.default=()=>d,n.d(f,t),f},n.d=(e,a)=>{for(var d in a)n.o(a,d)&&!n.o(e,d)&&Object.defineProperty(e,d,{enumerable:!0,get:a[d]})},n.f={},n.e=e=>Promise.all(Object.keys(n.f).reduce((a,d)=>(n.f[d](e,a),a),[])),n.u=e=>"assets/js/"+(({106:"17896441",1184:"e6ac3f64",1234:"138e0e15",1309:"153c3ceb",1353:"e7a53bbd",1499:"24a5b6e6",1668:"5e95c892",1703:"e82a470f",1785:"c2e935db",1920:"294f31e2",2068:"202c4968",2072:"425b8dd3",2076:"91a78c9f",2077:"d53edd15",232:"af9480c3",2351:"f18fd350",2609:"1bb2dc41",2613:"778b0fa6",2677:"4edc808e",2929:"61856a46",3056:"a7456010",3747:"aba21aa0",3763:"cdda7d53",3841:"41cd626a",3895:"64837553",3922:"5a378701",3963:"8b39be93",4123:"925abbc7",429:"33c4ed3f",4461:"04d926a5",4479:"490e20a0",4787:"7d837a1d",490:"9bcab3db",5005:"common",5077:"30829cd8",5093:"1fb61236",539:"dfa6d5b1",5515:"a16703cd",5575:"a7bd4aaa",5614:"fa357871",5847:"1a4e3797",6412:"3827a595",6473:"2ecbbad4",6691:"b46f7c12",676:"49117d77",6785:"a94703ab",6942:"44b2647e",7211:"5a6ebb5c",7212:"a93ae359",7220:"d50915e5",7227:"72d905a9",7234:"14eb3368",7314:"74876495",7417:"08938dd7",7430:"498c77bd",7736:"bf69edfb",8037:"d3a18f08",8123:"73287945",8160:"ad895e75",8270:"094000d3",8406:"0ce74438",8968:"ea69cd8d",899:"2a24ad03",9077:"5930ad03",9212:"9fd2a592",9249:"b49ba48c",941:"493c00ca",9494:"7b4f75c2",9515:"4f7d60c5",9536:"7ebd3339",9627:"fee59995",9698:"97d34c1a",9818:"5f7452e4",9991:"812e048c"})[e]||e)+"."+({106:"366a20db",1184:"065ee538",1234:"270f5136",1309:"8bccd2a2",1353:"caba3573",1499:"a9742041",1649:"9badff1a",1668:"dbe123fe",1703:"24cf7fbc",1785:"ed551359",1920:"fad893ae",2068:"da650089",2072:"cea8b1a2",2076:"055787b3",2077:"6f64cded",232:"1ae1b28b",2351:"891e8d11",2609:"11f59e56",2613:"f419d76e",2677:"01dc77ad",2929:"bc75c3ee",3056:"728c2f65",3747:"34582f97",3763:"5b8b0a9c",3841:"990a1161",3895:"d76c7c42",3922:"f804e785",3963:"eb6d9d58",4123:"c3c17436",429:"96b3612f",434:"3522035b",4461:"8f53c6aa",4479:"2f2af6ce",4494:"30732283",4787:"874db7a3",490:"4ec17ca4",5005:"83d91827",5077:"9087dd14",5093:"1db46805",5314:"0d8b1cbe",5387:"a22894e6",539:"bed44827",5515:"a3295a26",5575:"97114100",5614:"b16bfd0d",5730:"53f66bf8",5847:"3d615296",6412:"3cd1fc75",6473:"b0403b88",6691:"3100fbe6",676:"ecb7b006",6785:"97949813",6942:"35013e97",7211:"0af53e22",7212:"171f1231",7220:"3f036d92",7227:"6c664fe1",7234:"050b12c6",7314:"3bfa055b",7417:"47711a14",7430:"493fa519",7736:"c50ccff0",8037:"ea51fc4e",8123:"9295af0b",8160:"13e7b34f",8270:"256efb93",8406:"e2243fa5",8968:"fbc19d6c",899:"f7468792",9077:"bc7160d6",92:"e59cb17d",9212:"3a67aa4f",9249:"e798877e",9311:"f9221018",9313:"dc290399",941:"d188969b",9494:"2b8c9d89",9515:"96d59325",9536:"c3c05102",9618:"fc560b69",9627:"19421a19",9698:"ed4d7083",9818:"26462546",9991:"b5db2814",9999:"64bb008b"})[e]+".js",n.miniCssF=e=>""+e+".css",n.g=(()=>{if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}})(),n.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),d={},n.l=function(e,a,c,f){if(d[e])return void d[e].push(a);if(void 0!==c)for(var t,r,b=document.getElementsByTagName("script"),o=0;o{"u">typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},c=[],n.O=(e,a,d,f)=>{if(a){f=f||0;for(var t=c.length;t>0&&c[t-1][2]>f;t--)c[t]=c[t-1];c[t]=[a,d,f];return}for(var r=1/0,t=0;t=f)&&Object.keys(n.O).every(e=>n.O[e](a[o]))?a.splice(o--,1):(b=!1,f"1.7.11",n.gca=function(e){return e=({0x11113f9:"106",0x3dd57b1:"3895",0x45e4909:"8123",0x476864f:"7314",af9480c3:"232","33c4ed3f":"429","9bcab3db":"490",dfa6d5b1:"539","49117d77":"676","2a24ad03":"899","493c00ca":"941",e6ac3f64:"1184","138e0e15":"1234","153c3ceb":"1309",e7a53bbd:"1353","24a5b6e6":"1499","5e95c892":"1668",e82a470f:"1703",c2e935db:"1785","294f31e2":"1920","202c4968":"2068","425b8dd3":"2072","91a78c9f":"2076",d53edd15:"2077",f18fd350:"2351","1bb2dc41":"2609","778b0fa6":"2613","4edc808e":"2677","61856a46":"2929",a7456010:"3056",aba21aa0:"3747",cdda7d53:"3763","41cd626a":"3841","5a378701":"3922","8b39be93":"3963","925abbc7":"4123","04d926a5":"4461","490e20a0":"4479","7d837a1d":"4787",common:"5005","30829cd8":"5077","1fb61236":"5093",a16703cd:"5515",a7bd4aaa:"5575",fa357871:"5614","1a4e3797":"5847","3827a595":"6412","2ecbbad4":"6473",b46f7c12:"6691",a94703ab:"6785","44b2647e":"6942","5a6ebb5c":"7211",a93ae359:"7212",d50915e5:"7220","72d905a9":"7227","14eb3368":"7234","08938dd7":"7417","498c77bd":"7430",bf69edfb:"7736",d3a18f08:"8037",ad895e75:"8160","094000d3":"8270","0ce74438":"8406",ea69cd8d:"8968","5930ad03":"9077","9fd2a592":"9212",b49ba48c:"9249","7b4f75c2":"9494","4f7d60c5":"9515","7ebd3339":"9536",fee59995:"9627","97d34c1a":"9698","5f7452e4":"9818","812e048c":"9991"})[e]||e,n.p+n.u(e)},n.b=document.baseURI||self.location.href,f={4014:0,9783:0},n.f.j=function(e,a){var d=n.o(f,e)?f[e]:void 0;if(0!==d)if(d)a.push(d[2]);else if(/^(4014|9783)$/.test(e))f[e]=0;else{var c=new Promise((a,c)=>d=f[e]=[a,c]);a.push(d[2]=c);var t=n.p+n.u(e),r=Error();n.l(t,function(a){if(n.o(f,e)&&(0!==(d=f[e])&&(f[e]=void 0),d)){var c=a&&("load"===a.type?"missing":a.type),t=a&&a.target&&a.target.src;r.message="Loading chunk "+e+" failed.\n("+c+": "+t+")",r.name="ChunkLoadError",r.type=c,r.request=t,d[1](r)}},"chunk-"+e,e)}},n.O.j=e=>0===f[e],t=(e,a)=>{var d,c,t=a[0],r=a[1],b=a[2],o=0;if(t.some(e=>0!==f[e])){for(d in r)n.o(r,d)&&(n.m[d]=r[d]);if(b)var i=b(n)}for(e&&e(a);o Development | Multi-Scrobbler - + diff --git a/configuration/clients/discord/discord-app/index.html b/configuration/clients/discord/discord-app/index.html index 2f95612d..9d9003fc 100644 --- a/configuration/clients/discord/discord-app/index.html +++ b/configuration/clients/discord/discord-app/index.html @@ -3,11 +3,11 @@ -Discord (App) | Multi-Scrobbler +Discord (App) | Multi-Scrobbler - + - + diff --git a/configuration/clients/discord/discord-headless/index.html b/configuration/clients/discord/discord-headless/index.html index e534f4c2..b50a0163 100644 --- a/configuration/clients/discord/discord-headless/index.html +++ b/configuration/clients/discord/discord-headless/index.html @@ -3,11 +3,11 @@ -Discord (Headless) | Multi-Scrobbler +Discord (Headless) | Multi-Scrobbler - + - + diff --git a/configuration/clients/discord/index.html b/configuration/clients/discord/index.html index 797a5ed2..9b5f6755 100644 --- a/configuration/clients/discord/index.html +++ b/configuration/clients/discord/index.html @@ -3,11 +3,11 @@ -Discord | Multi-Scrobbler +Discord | Multi-Scrobbler - + - + diff --git a/configuration/clients/index.html b/configuration/clients/index.html index f724c215..9c57df39 100644 --- a/configuration/clients/index.html +++ b/configuration/clients/index.html @@ -3,11 +3,11 @@ -Overview | Multi-Scrobbler +Overview | Multi-Scrobbler - + - + diff --git a/configuration/clients/koito/index.html b/configuration/clients/koito/index.html index dc3c9b98..7df42bcd 100644 --- a/configuration/clients/koito/index.html +++ b/configuration/clients/koito/index.html @@ -3,11 +3,11 @@ -Koito | Multi-Scrobbler +Koito | Multi-Scrobbler - + - + diff --git a/configuration/clients/lastfm/index.html b/configuration/clients/lastfm/index.html index e262a31e..c25294ef 100644 --- a/configuration/clients/lastfm/index.html +++ b/configuration/clients/lastfm/index.html @@ -3,11 +3,11 @@ -Last.fm | Multi-Scrobbler +Last.fm | Multi-Scrobbler - + - + diff --git a/configuration/clients/librefm/index.html b/configuration/clients/librefm/index.html index 57548e32..4bb1e290 100644 --- a/configuration/clients/librefm/index.html +++ b/configuration/clients/librefm/index.html @@ -3,11 +3,11 @@ -Libre.fm | Multi-Scrobbler +Libre.fm | Multi-Scrobbler - + - + diff --git a/configuration/clients/listenbrainz/index.html b/configuration/clients/listenbrainz/index.html index b4c29ee3..753bea6f 100644 --- a/configuration/clients/listenbrainz/index.html +++ b/configuration/clients/listenbrainz/index.html @@ -3,11 +3,11 @@ -Listenbrainz | Multi-Scrobbler +Listenbrainz | Multi-Scrobbler - + - + diff --git a/configuration/clients/maloja/index.html b/configuration/clients/maloja/index.html index f8e2258f..575d5fcc 100644 --- a/configuration/clients/maloja/index.html +++ b/configuration/clients/maloja/index.html @@ -3,11 +3,11 @@ -Maloja | Multi-Scrobbler +Maloja | Multi-Scrobbler - + - + diff --git a/configuration/clients/rocksky/index.html b/configuration/clients/rocksky/index.html index cc524c0d..99d5455f 100644 --- a/configuration/clients/rocksky/index.html +++ b/configuration/clients/rocksky/index.html @@ -3,11 +3,11 @@ -Rocksky | Multi-Scrobbler +Rocksky | Multi-Scrobbler - + - + diff --git a/configuration/clients/tealfm/index.html b/configuration/clients/tealfm/index.html index def96d3b..8132217c 100644 --- a/configuration/clients/tealfm/index.html +++ b/configuration/clients/tealfm/index.html @@ -3,11 +3,11 @@ -Teal.fm | Multi-Scrobbler +Teal.fm | Multi-Scrobbler - + - + diff --git a/configuration/duplicates/index.html b/configuration/duplicates/index.html index 3fe6edae..906312f4 100644 --- a/configuration/duplicates/index.html +++ b/configuration/duplicates/index.html @@ -3,11 +3,11 @@ -Duplicate Scrobble Guidance | Multi-Scrobbler +Duplicate Scrobble Guidance | Multi-Scrobbler - + - + diff --git a/configuration/index.html b/configuration/index.html index 30a3582f..aaa63311 100644 --- a/configuration/index.html +++ b/configuration/index.html @@ -3,11 +3,11 @@ -Overview | Multi-Scrobbler +Overview | Multi-Scrobbler - + - + @@ -44,7 +44,7 @@
  • There is also a kitchensink example that provides examples of using all sources/clients in a complex configuration.
  • -

    Each config file is named after the type of the Client/Source. Within each file, a JSON array defines all of the instances for that type of Client/Source.

    Example directory structure:

    /CONFIG_DIR
    jellyfin.json
    lastfm.json
    koito.json
    Config Example
    jellyfin.json
    [
    {
    "name": "Foxx JF Server",
    "id": "foxxJf",
    "data": {
    "url": "http://localhost:8096",
    "user": "FoxxMD",
    "apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
    }
    }
    ]
    Multiple of Same Config Example
    jellyfin.json
    [
    {
    "name": "Foxx JF Server",
    "id": "foxxJf",
    "data": {
    "url": "http://localhost:8096",
    "user": "FoxxMD",
    "apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
    }
    },
    {
    "name": "Foo's JF Server",
    "id": "fooJf",
    "data": {
    "url": "http://192.168.0.150:8096",
    "user": "foo",
    "apiKey": "9c5bb56bd6540c756fbf481ebd9c5c40"
    }
    }
    ]

    Each config file is named after the type of the Client/Source. Within each file, a JSON array defines all of the instances for that type of Client/Source.

    Example directory structure:

    /CONFIG_DIR
    jellyfin.json
    lastfm.json
    koito.json
    Config Example
    jellyfin.json
    [
    {
    "name": "Foxx JF Server",
    "id": "foxxJf",
    "data": {
    "url": "http://localhost:8096",
    "user": "FoxxMD",
    "apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
    }
    }
    ]
    Multiple of Same Config Example
    jellyfin.json
    [
    {
    "name": "Foxx JF Server",
    "id": "foxxJf",
    "data": {
    "url": "http://localhost:8096",
    "user": "FoxxMD",
    "apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
    }
    },
    {
    "name": "Foo's JF Server",
    "id": "fooJf",
    "data": {
    "url": "http://192.168.0.150:8096",
    "user": "foo",
    "apiKey": "9c5bb56bd6540c756fbf481ebd9c5c40"
    }
    }
    ]

    You can find an example of the file name that should be used under the File Configuration for each Source/Client. Note: this is the same value that is used for type in AIO configs.

    +
    Config Example
    CONFIG_DIR/config.json
    {
    "debugMode": false,
    "disableWeb": false,
    "sourceDefaults": {
    "logPayload": false,
    "logFilterFailure": "warn",
    "logPlayerState": false,
    "scrobbleThresholds": {
    "duration": 30,
    "percent": 50
    },
    "maxPollRetries": 1,
    "maxRequestRetries": 1,
    "retryMultiplier": 1.5
    },
    "clientDefaults": {
    "maxRequestRetries": 1,
    "retryMultiplier": 1.5
    },
    "sources": [
    {
    "type": "spotify",
    "enable": true,
    "clients": ["myConfig"],
    "name": "Cool Spotify Name",
    "id": "mySpotifyId",
    "data": {
    "clientId": "a89cba1569901a0671d5a9875fed4be1",
    "clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
    "redirectUri": "http://localhost:9078/callback"
    }
    }
    ],
    "clients": [
    {
    "type": "maloja",
    "enable": true,
    "name": "Foxx Maloja",
    "id": "myMalojaId",
    "data": {
    "url": "http://localhost:42010",
    "apiKey": "myMalojaKey"
    }
    }
    ],
    "webhooks": [
    {
    "name": "FirstGotifyServer",
    "type": "gotify",
    "url": "http://localhost:8070",
    "token": "MyGotifyToken",
    "priorities": {
    "info": 5,
    "warn": 7,
    "error": 10
    }
    },
    {
    "type": "ntfy",
    "name": "MyNtfyFriendlyNameForLogs",
    "url": "http://localhost:9991",
    "topic": "MyMultiScrobblerTopic",
    "username": "Optional",
    "password": "Optional",
    "priorities": {
    "info": 3,
    "warn": 4,
    "error": 5
    }
    }
    ]
    }
    Source/Client AIO Configs require a 'type' property

    Compared to their File equivalent, Sources/Clients configured using AIO require an additional type property.

    config.json
    {
    "sources": [
    {
    "name": "Foxx JF Server",
    "type: "jellyfin",
    "id": "foxxJf",
    "data": {
    "url": "http://localhost:8096",
    "user": "FoxxMD",
    "apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
    }
    }
    ]
    }

    You can find an example of the required type value by referring to the AIO tab in each Source/Client's Configuration section example. This is the same value that is used as the name of individual File config files.

    Setup Sources and Clients​

    See the Configuration Types above for your options for creating Source and Client configurations.