FAQOn this pageFAQConnection Issues Webscrobbler/Listenbrainz Endpoint doesn't connect @@ -37,7 +37,7 @@ Ensure any client id or secrets are correct in your configuration. The callback/redirect URL for these services must be: -the same address you would use to access the multi-scrobbler web interface +the same address you would use to access the multi-scrobbler web interface the web-interface must be accessible from the browser you are completing authentication from. @@ -110,7 +110,7 @@ Provide a detailed account of how you were using YTM when the issue occurred, including things like: the platform listening on (desktop, mobile, 3rd party client, etc...) -any changes in platform +any changes in platform @@ -119,7 +119,7 @@ -how you were listening to music +how you were listening to music diff --git a/assets/js/04d926a5.cffa3cc9.js b/assets/js/04d926a5.cffa3cc9.js deleted file mode 100644 index 41c6df77..00000000 --- a/assets/js/04d926a5.cffa3cc9.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:()=>a,RM:()=>o});var s=i(4848),r=i(8453),t=i(2170),l=i.n(t);let o=[];function c(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 active listening 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 a(e={}){let{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},9625(e,n,i){i.r(n),i.d(n,{metadata:()=>s,default:()=>p,frontMatter:()=>h,contentTitle:()=>u,toc:()=>j,assets:()=>x});var s=JSON.parse('{"id":"configuration/configuration","title":"Overview","description":"Check the FAQ if you have any issues after configuration!","source":"@site/docs/configuration/configuration.mdx","sourceDirName":"configuration","slug":"/configuration/","permalink":"/multi-scrobbler/configuration/","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/configuration.mdx","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"sidebar_position":2,"title":"Overview","toc_max_heading_level":3},"sidebar":"tutorialSidebar","previous":{"title":"Overview","permalink":"/multi-scrobbler/updating/"},"next":{"title":"Overview","permalink":"/multi-scrobbler/configuration/clients/"}}'),r=i(4848),t=i(8453),l=i(8194),o=i(698),c=i(2170),a=i.n(c);i(2709),i(9461),i(8675),i(7265);var d=i(3169);let h={sidebar_position:2,title:"Overview",toc_max_heading_level:3},u,x={},j=[{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:"Application Options",id:"application-options",level:2},{value:"Base URL",id:"base-url",level:3},{value:"Caching",id:"caching",level:3},{value:"Configuration",id:"configuration",level:5},{value:"Configuration",id:"configuration-1",level:5},{value:"Configuration",id:"configuration-2",level:5},{value:"Secondary Caching Configuration",id:"secondary-caching-configuration",level:4},{value:"Debug Mode",id:"debug-mode",level:3},{value:"Disable Web",id:"disable-web",level:3},{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 f(e){let n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",h5:"h5",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:c}=n;return s||g("Details",!0),c||g("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 or the other."]}),"\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.p,{children:["MS will parse environmental variables present in the OS/container when it is run. ",(0,r.jsx)(n.strong,{children:"This method means MS does not require files to run."})]}),(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"}),"\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 Plex -> Maloja"]}),"\n"]})]}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Config Example"}),(0,r.jsx)(n.p,{children:"For Docker container..."}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-shell",children:'docker run -e "SPOTIFY_CLIENT_ID=yourId" -e "SPOTIFY_CLIENT_SECRET=yourSecret" ...\n'})}),(0,r.jsx)(n.p,{children:"For Docker Compose"}),(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 - SPOTIFY_CLIENT_ID=yourId\n - SPOTIFY_CLIENT_SECRET=yourSecret\n - MALOJA_URL=http://domain.tld:42010\n - MALOJA_API_KEY=1234\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 -> ",(0,r.jsx)(n.code,{children:"PROJECT_DIR/config"})]}),"\n",(0,r.jsxs)(n.li,{children:["Docker -> ",(0,r.jsx)(n.code,{children:"/config"})," (in the container) -- see the ",(0,r.jsx)(n.a,{href:"/multi-scrobbler/installation/#docker",children:"install 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.jsxs)(n.li,{children:["You have many ",(0,r.jsx)(n.a,{href:"/configuration/sources",children:"Sources"})]}),"\n",(0,r.jsxs)(n.li,{children:["You have many of each type of ",(0,r.jsx)(n.strong,{children:"Source"})," you want to scrobble from IE 2x Plex accounts, 3x Spotify accounts, 1x\nFunkwhale..."]}),"\n",(0,r.jsxs)(n.li,{children:["You have more than one scrobble ",(0,r.jsx)(n.strong,{children:"Client"})," you want to scrobble to IE multiple Maloja servers"]}),"\n",(0,r.jsxs)(n.li,{children:["You want only 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, all configuration is possible using File/AIO"}),"\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.jsx)(n.li,{children:"For docker installations these examples are copied to your configuration directory on first-time use."}),"\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 file is named by the ",(0,r.jsx)(n.strong,{children:"type"})," of the Client/Source found in below sections. Each file as an ",(0,r.jsx)(n.strong,{children:"array"})," of 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 plex.json\n spotify.json\n maloja.json\n"})}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Config Example"}),(0,r.jsx)(a(),{title:"CONFIG_DIR/spotify.json",language:"json5",children:d.A})]})]}),(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 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 -> ",(0,r.jsx)(n.code,{children:"PROJECT_DIR/config/config.json"})]}),"\n",(0,r.jsxs)(n.li,{children:["Docker -> ",(0,r.jsx)(n.code,{children:"/config/config.json"})," (in the container) -- see the ",(0,r.jsx)(n.a,{href:"/multi-scrobbler/installation/#docker",children:"install 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:["You have many ",(0,r.jsx)(n.a,{href:"/configuration/sources",children:"Sources"})]}),"\n",(0,r.jsxs)(n.li,{children:["You have many of each type of ",(0,r.jsx)(n.strong,{children:"Source"})," you want to scrobble from IE 2x Plex accounts, 3x Spotify accounts, 1x\nFunkwhale..."]}),"\n",(0,r.jsxs)(n.li,{children:["You have more than one scrobble ",(0,r.jsx)(n.strong,{children:"Client"})," you want to scrobble to IE multiple Maloja servers"]}),"\n",(0,r.jsxs)(n.li,{children:["You want only to scrobble to specific ",(0,r.jsx)(n.strong,{children:"Clients"})]}),"\n",(0,r.jsxs)(n.li,{children:["You need to setup ",(0,r.jsx)(n.a,{href:"#monitoring",children:"monitoring/webhooks"})]}),"\n",(0,r.jsx)(n.li,{children:"You want to setup defaults for all Sources/Clients"}),"\n"]})]}),(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"The AIO config also enables setting default options for sources/clients as well as global options for MS itself."})}),(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["An example AIO config files can be found in the project 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"})})]}),"\n",(0,r.jsx)(n.li,{children:"For docker installations this example is copied to your configuration directory on first-time use."}),"\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.jsx)(n.p,{children:(0,r.jsx)(n.a,{href:"https://json-schema.app/view/%23?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json",children:(0,r.jsx)(n.strong,{children:"Explore the schema for this configuration, along with an example generator and validator, here"})})}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Config Example"}),(0,r.jsx)(a(),{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": "mySpotifySource",\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": "myConfig",\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'})]})]})]}),"\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)(c,{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)(c,{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)(c,{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:"application-options",children:"Application Options"}),"\n",(0,r.jsx)(n.p,{children:"These options affect multi-scrobbler's behavior and are not specific to any source/client."}),"\n",(0,r.jsx)(n.h3,{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 on ",(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.h3,{id:"caching",children:"Caching"}),"\n",(0,r.jsx)(n.p,{children:"Multi-scrobbler caches some activities to persist important data across restarts, reduce external API calls, and make some actions faster."}),"\n",(0,r.jsxs)(n.p,{children:["All of the activities below are ",(0,r.jsx)(n.strong,{children:"always"})," cached ",(0,r.jsx)(n.strong,{children:"in-memory"})," with an optional, configurable ",(0,r.jsxs)(n.a,{href:"#secondary-caching-configuration",children:[(0,r.jsx)(n.strong,{children:"secondary"})," store"]})," for persistence."]}),"\n",(0,r.jsxs)(l.A,{groupId:"cachedThings",queryString:!0,children:[(0,r.jsxs)(o.A,{value:"scrobbles",label:"Scrobbles",children:[(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Queued"})," and ",(0,r.jsx)(n.strong,{children:"Failed"})," Scrobbles are cached so that any un-scrobbled data you have is persisted across restarts of multi-scrobbler."]}),(0,r.jsxs)(n.admonition,{type:"tip",children:[(0,r.jsxs)(n.p,{children:["By default, this data use a ",(0,r.jsx)(n.a,{href:"#secondary-caching-configuration",children:"Secondary"})," ",(0,r.jsx)(n.a,{href:"./?cacheType=file#secondary-caching-configuration",children:"File"})," store, configured for you automatically."]}),(0,r.jsxs)(n.p,{children:["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"}),") then you are already done. If you are not persisting this directory then you should consider setting up ",(0,r.jsx)(n.a,{href:"./?cacheType=valkey#secondary-caching-configuration",children:"Valkey Cache"})," for this."]})]}),(0,r.jsx)(n.h5,{id:"configuration",children:"Configuration"}),(0,r.jsxs)(n.p,{children:["Use any ",(0,r.jsx)(n.a,{href:"#secondary-caching-configuration",children:"Secondary Cache"}),", the config examples below show the default values:"]}),(0,r.jsxs)(l.A,{children:[(0,r.jsx)(o.A,{value:"env",label:"ENV",children:(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,r.jsx)(n.th,{children:"Required?"}),(0,r.jsx)(n.th,{children:"Default"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_SCROBBLE"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"file"})}),(0,r.jsx)(n.td,{children:"The cache type to use"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_SCROBBLE_CONN"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"/config"})}),(0,r.jsx)(n.td,{})]})]})]})}),(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 "scrobble": {\n "provider": "file",\n "connection": "/config"\n }\n },\n // ...\n}\n'})})})]})]}),(0,r.jsxs)(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.jsxs)(n.admonition,{type:"tip",children:[(0,r.jsxs)(n.p,{children:["By default, this data use a ",(0,r.jsx)(n.a,{href:"#secondary-caching-configuration",children:"Secondary"})," ",(0,r.jsx)(n.a,{href:"./?cacheType=file#secondary-caching-configuration",children:"File"})," store, configured for you automatically."]}),(0,r.jsxs)(n.p,{children:["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"}),") then you are already done. If you are not persisting this directory then you should consider setting up ",(0,r.jsx)(n.a,{href:"./?cacheType=valkey#secondary-caching-configuration",children:"Valkey Cache"})," for this."]})]}),(0,r.jsx)(n.h5,{id:"configuration-1",children:"Configuration"}),(0,r.jsxs)(n.p,{children:["Use any ",(0,r.jsx)(n.a,{href:"#secondary-caching-configuration",children:"Secondary Cache"}),", the config examples below show the default values:"]}),(0,r.jsxs)(l.A,{children:[(0,r.jsx)(o.A,{value:"env",label:"ENV",children:(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,r.jsx)(n.th,{children:"Required?"}),(0,r.jsx)(n.th,{children:"Default"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_AUTH"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"file"})}),(0,r.jsx)(n.td,{children:"The cache type to use"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_AUTH_CONN"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"/config"})}),(0,r.jsx)(n.td,{})]})]})]})}),(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": "file",\n "connection": "/config"\n }\n },\n // ...\n}\n'})})})]})]}),(0,r.jsxs)(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."]}),(0,r.jsxs)(n.p,{children:["By default, these calls are only cached in memory. If you wish for cached calls to be persisted across restarts then setup ",(0,r.jsx)(n.a,{href:"./?cacheType=valkey#secondary-caching-configuration",children:"Valkey Cache"}),"."]}),(0,r.jsx)(n.h5,{id:"configuration-2",children:"Configuration"}),(0,r.jsxs)(n.p,{children:["Use any ",(0,r.jsx)(n.a,{href:"#secondary-caching-configuration",children:"Secondary Cache"}),", the config examples below show the default values:"]}),(0,r.jsxs)(l.A,{children:[(0,r.jsx)(o.A,{value:"env",label:"ENV",children:(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,r.jsx)(n.th,{children:"Required?"}),(0,r.jsx)(n.th,{children:"Default"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_METADATA"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{}),(0,r.jsx)(n.td,{children:"The cache type to use"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_METADATA_CONN"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{}),(0,r.jsx)(n.td,{})]})]})]})}),(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 "metadata": {\n "provider": "valkey",\n "connection": "yourConnectionStringHere"\n }\n },\n // ...\n}\n'})})})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"secondary-caching-configuration",children:"Secondary Caching Configuration"}),"\n",(0,r.jsxs)(n.p,{children:["The type of cache used, and its connection properties, can be configured through ",(0,r.jsx)(n.strong,{children:"ENV"})," or ",(0,r.jsx)(n.strong,{children:"AIO"})," config."]}),"\n",(0,r.jsxs)(l.A,{groupId:"cacheType",queryString:!0,children:[(0,r.jsxs)(o.A,{value:"file",label:"File",children:[(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"File"})," cache is stored in the ",(0,r.jsx)(n.code,{children:"CONFIG_DIR"})," directory using the pre-defined file name ",(0,r.jsx)(n.code,{children:"ms-[cacheName].cache"}),"."]}),(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:"Example"}),(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,r.jsx)(n.th,{children:"Required?"}),(0,r.jsx)(n.th,{children:"Default"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_SCROBBLE"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"file"})}),(0,r.jsx)(n.td,{children:"The cache type to use"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_SCROBBLE_CONN"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"/config"})}),(0,r.jsx)(n.td,{children:"The directory, within the container, to store the cache file"})]})]})]})]}),(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 "scrobble": {\n "provider": "file",\n "connection": "/config"\n }\n },\n // ...\n}\n'})})]})]})]}),(0,r.jsxs)(o.A,{value:"valkey",label:"Valkey",children:[(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"https://valkey.io/",children:(0,r.jsx)(n.strong,{children:"Valkey"})})," is an open-source fork of Redis."]}),(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.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n # ...\n # adding everything below\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.jsxs)(n.p,{children:["Use ",(0,r.jsx)(n.code,{children:"redis://valkey:6379"})," as the connection string in the configurations below."]})]}),(0,r.jsx)(n.p,{children:"The connection string used by multi-scrobbler to connect to your Valkey instance must be in the form:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"redis://HOST_IP:HOST_PORT\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:"Example"}),(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,r.jsx)(n.th,{children:"Required?"}),(0,r.jsx)(n.th,{children:"Default"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_METADATA"})}),(0,r.jsx)(n.td,{children:"Yes"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"valkey"})}),(0,r.jsx)(n.td,{children:"The cache type to use"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_METADATA_CONN"})}),(0,r.jsx)(n.td,{children:"Yes"}),(0,r.jsx)(n.td,{}),(0,r.jsxs)(n.td,{children:["The host/IP and port to connect to EX: ",(0,r.jsx)(n.code,{children:"redis://192.168.0.120:6379"})]})]})]})]})]}),(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 "metadata": {\n "provider": "valkey",\n "connection": "redis://192.168.0.120:6379"\n }\n },\n // ...\n}\n'})})]})]})]})]}),"\n",(0,r.jsx)(n.h3,{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.h3,{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:"https://json-schema.app/view/%23/%23%2Fdefinitions%2FGotifyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json",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:"https://json-schema.app/view/%23/%23%2Fdefinitions%2FNtfyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json",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:"https://json-schema.app/view/%23/%23%2Fdefinitions%2FAppriseConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json",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)(c,{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 p(e={}){let{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(f,{...e})}):f(e)}function g(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);i(5161);var o=i(1219),c=i.n(o),a=i(4848);let d=e=>{let n,{data:i,name:s,client:l=!1}=e;try{n=c().parse(i)}catch(e){return console.error(e),(0,a.jsxs)(t.A,{type:"danger",title:"Unexpected Error",children:[(0,a.jsx)("p",{children:"Example component crashed because of error!"}),(0,a.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,a.jsx)(r(),{title:"CONFIG_DIR/config.json",language:"json5",children:JSON.stringify(o,null,2)})},h=e=>(0,a.jsx)(l.A,{fallback:({error:e})=>(0,a.jsx)("div",{children:(0,a.jsxs)("p",{children:["Example component crashed because of error: ",e.message,"."]})}),children:(0,a.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:c=!1,className:a,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}`:"",g=i?`button--${i}`:"";return(0,t.jsx)(r.A,{to:c?null:h,children:(0,t.jsx)("button",{className:(0,s.A)("button",p,n?"button--outline":"",g,o?"button--block":"",c?"disabled":"",a),style:d,role:"button","aria-disabled":c,onClick:e=>{e.preventDefault(),x()},...j,children:u})})}},9643(e,n,i){i.d(n,{A:()=>c}),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 c({size:e=null,variant:n="primary",block:i=!1,disabled:a=!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,c)=>{let a={style:o,outline:!0,size:e,variant:n,label:i[1],link:"#",onClick:()=>f(i[0])};return 0===c?a.style=t:c===u.length-1&&(a.style=l),(x??j)===i[0]&&(a.outline=!1),(0,r.jsx)(s.A,{...a},c)});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 c={};var a=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,g]=((e,n,i)=>{let r;return e in c?c[e]():o.A.canUseDOM?(c[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]}),c[e]()):[n,()=>null]})("docusaurus.tab.fileExampleShow",r,!1),m=(0,h.l)({desktopBreakpoint:j}),b=null,y=i&&"desktop"===m;y&&(b=(0,u.jsx)(a.A,{options:[["plain","Plain"],["interactive","Interactive"]],defaultValue:r,value:p,variant:"primary",size:"medium",onChange:e=>g(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 A=null;(null!==b||null!==v)&&(A=(0,u.jsxs)("div",{style:{marginBottom:"1em"},children:[b,v]}));let k=(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:[A,y&&"plain"!==p?null:(0,u.jsx)(t(),{title:x,language:"json5",children:k}),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),c=i(8182),a=i.n(c),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"),a().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),c=void 0===n?void 0:(0,r.Ay)(`schemas/${e.schemaName}.json`);return(0,s.useEffect)(()=>{void 0===c?t(!1):fetch(c,{headers:{Accept:"application/json"}}).then(e=>e.json()).then(e=>t(e)).catch(e=>t(e))},[t,c]),(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"},3169(e,n,i){i.d(n,{A:()=>s});let s='[\n {\n "name": "MySpotify",\n "enable": true,\n "clients": [],\n "data": {\n "clientId": "a89cba1569901a0671d5a9875fed4be1",\n "clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",\n "redirectUri": "http://localhost:9078/callback",\n "interval": 60\n }\n }\n]\n'},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.ed6faa74.js b/assets/js/04d926a5.ed6faa74.js new file mode 100644 index 00000000..971089ec --- /dev/null +++ b/assets/js/04d926a5.ed6faa74.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:()=>a,RM:()=>o});var s=i(4848),r=i(8453),t=i(2170),l=i.n(t);let o=[];function c(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 active listening 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 a(e={}){let{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},9625(e,n,i){i.r(n),i.d(n,{metadata:()=>s,default:()=>p,frontMatter:()=>h,contentTitle:()=>u,toc:()=>j,assets:()=>x});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/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/configuration.mdx","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"sidebar_position":2,"title":"Overview","description":"Configuration","toc_max_heading_level":3},"sidebar":"tutorialSidebar","previous":{"title":"Overview","permalink":"/multi-scrobbler/updating/"},"next":{"title":"Overview","permalink":"/multi-scrobbler/configuration/clients/"}}'),r=i(4848),t=i(8453),l=i(8194),o=i(698),c=i(2170),a=i.n(c);i(2709),i(9461),i(8675),i(7265);var d=i(3169);let h={sidebar_position:2,title:"Overview",description:"Configuration",toc_max_heading_level:3},u,x={},j=[{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:"Application Options",id:"application-options",level:2},{value:"Base URL",id:"base-url",level:3},{value:"Caching",id:"caching",level:3},{value:"Configuration",id:"configuration",level:5},{value:"Configuration",id:"configuration-1",level:5},{value:"Configuration",id:"configuration-2",level:5},{value:"Secondary Caching Configuration",id:"secondary-caching-configuration",level:4},{value:"Debug Mode",id:"debug-mode",level:3},{value:"Disable Web",id:"disable-web",level:3},{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 f(e){let n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",h5:"h5",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:c}=n;return s||g("Details",!0),c||g("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 or the other."]}),"\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.p,{children:["MS will parse environmental variables present in the OS/container when it is run. ",(0,r.jsx)(n.strong,{children:"This method means MS does not require files to run."})]}),(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"}),"\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 Plex -> Maloja"]}),"\n"]})]}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Config Example"}),(0,r.jsx)(n.p,{children:"For Docker container..."}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-shell",children:'docker run -e "SPOTIFY_CLIENT_ID=yourId" -e "SPOTIFY_CLIENT_SECRET=yourSecret" ...\n'})}),(0,r.jsx)(n.p,{children:"For Docker Compose"}),(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 - SPOTIFY_CLIENT_ID=yourId\n - SPOTIFY_CLIENT_SECRET=yourSecret\n - MALOJA_URL=http://domain.tld:42010\n - MALOJA_API_KEY=1234\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 -> ",(0,r.jsx)(n.code,{children:"PROJECT_DIR/config"})]}),"\n",(0,r.jsxs)(n.li,{children:["Docker -> ",(0,r.jsx)(n.code,{children:"/config"})," (in the container) -- see the ",(0,r.jsx)(n.a,{href:"/multi-scrobbler/installation/#docker",children:"install 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.jsxs)(n.li,{children:["You have many ",(0,r.jsx)(n.a,{href:"/configuration/sources",children:"Sources"})]}),"\n",(0,r.jsxs)(n.li,{children:["You have many of each type of ",(0,r.jsx)(n.strong,{children:"Source"})," you want to scrobble from IE 2x Plex accounts, 3x Spotify accounts, 1x\nFunkwhale..."]}),"\n",(0,r.jsxs)(n.li,{children:["You have more than one scrobble ",(0,r.jsx)(n.strong,{children:"Client"})," you want to scrobble to IE multiple Maloja servers"]}),"\n",(0,r.jsxs)(n.li,{children:["You want only 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, all configuration is possible using File/AIO"}),"\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.jsx)(n.li,{children:"For docker installations these examples are copied to your configuration directory on first-time use."}),"\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 file is named by the ",(0,r.jsx)(n.strong,{children:"type"})," of the Client/Source found in below sections. Each file as an ",(0,r.jsx)(n.strong,{children:"array"})," of 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 plex.json\n spotify.json\n maloja.json\n"})}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Config Example"}),(0,r.jsx)(a(),{title:"CONFIG_DIR/spotify.json",language:"json5",children:d.A})]})]}),(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 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 -> ",(0,r.jsx)(n.code,{children:"PROJECT_DIR/config/config.json"})]}),"\n",(0,r.jsxs)(n.li,{children:["Docker -> ",(0,r.jsx)(n.code,{children:"/config/config.json"})," (in the container) -- see the ",(0,r.jsx)(n.a,{href:"/multi-scrobbler/installation/#docker",children:"install 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:["You have many ",(0,r.jsx)(n.a,{href:"/configuration/sources",children:"Sources"})]}),"\n",(0,r.jsxs)(n.li,{children:["You have many of each type of ",(0,r.jsx)(n.strong,{children:"Source"})," you want to scrobble from IE 2x Plex accounts, 3x Spotify accounts, 1x\nFunkwhale..."]}),"\n",(0,r.jsxs)(n.li,{children:["You have more than one scrobble ",(0,r.jsx)(n.strong,{children:"Client"})," you want to scrobble to IE multiple Maloja servers"]}),"\n",(0,r.jsxs)(n.li,{children:["You want only to scrobble to specific ",(0,r.jsx)(n.strong,{children:"Clients"})]}),"\n",(0,r.jsxs)(n.li,{children:["You need to setup ",(0,r.jsx)(n.a,{href:"#monitoring",children:"monitoring/webhooks"})]}),"\n",(0,r.jsx)(n.li,{children:"You want to setup defaults for all Sources/Clients"}),"\n"]})]}),(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"The AIO config also enables setting default options for sources/clients as well as global options for MS itself."})}),(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["An example AIO config files can be found in the project 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"})})]}),"\n",(0,r.jsx)(n.li,{children:"For docker installations this example is copied to your configuration directory on first-time use."}),"\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.jsx)(n.p,{children:(0,r.jsx)(n.a,{href:"https://json-schema.app/view/%23?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json",children:(0,r.jsx)(n.strong,{children:"Explore the schema for this configuration, along with an example generator and validator, here"})})}),(0,r.jsxs)(s,{children:[(0,r.jsx)("summary",{children:"Config Example"}),(0,r.jsx)(a(),{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": "mySpotifySource",\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": "myConfig",\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'})]})]})]}),"\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)(c,{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)(c,{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)(c,{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:"application-options",children:"Application Options"}),"\n",(0,r.jsx)(n.p,{children:"These options affect multi-scrobbler's behavior and are not specific to any source/client."}),"\n",(0,r.jsx)(n.h3,{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 on ",(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.h3,{id:"caching",children:"Caching"}),"\n",(0,r.jsx)(n.p,{children:"Multi-scrobbler caches some activities to persist important data across restarts, reduce external API calls, and make some actions faster."}),"\n",(0,r.jsxs)(n.p,{children:["All of the activities below are ",(0,r.jsx)(n.strong,{children:"always"})," cached ",(0,r.jsx)(n.strong,{children:"in-memory"})," with an optional, configurable ",(0,r.jsxs)(n.a,{href:"#secondary-caching-configuration",children:[(0,r.jsx)(n.strong,{children:"secondary"})," store"]})," for persistence."]}),"\n",(0,r.jsxs)(l.A,{groupId:"cachedThings",queryString:!0,children:[(0,r.jsxs)(o.A,{value:"scrobbles",label:"Scrobbles",children:[(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Queued"})," and ",(0,r.jsx)(n.strong,{children:"Failed"})," Scrobbles are cached so that any un-scrobbled data you have is persisted across restarts of multi-scrobbler."]}),(0,r.jsxs)(n.admonition,{type:"tip",children:[(0,r.jsxs)(n.p,{children:["By default, this data use a ",(0,r.jsx)(n.a,{href:"#secondary-caching-configuration",children:"Secondary"})," ",(0,r.jsx)(n.a,{href:"./?cacheType=file#secondary-caching-configuration",children:"File"})," store, configured for you automatically."]}),(0,r.jsxs)(n.p,{children:["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"}),") then you are already done. If you are not persisting this directory then you should consider setting up ",(0,r.jsx)(n.a,{href:"./?cacheType=valkey#secondary-caching-configuration",children:"Valkey Cache"})," for this."]})]}),(0,r.jsx)(n.h5,{id:"configuration",children:"Configuration"}),(0,r.jsxs)(n.p,{children:["Use any ",(0,r.jsx)(n.a,{href:"#secondary-caching-configuration",children:"Secondary Cache"}),", the config examples below show the default values:"]}),(0,r.jsxs)(l.A,{children:[(0,r.jsx)(o.A,{value:"env",label:"ENV",children:(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,r.jsx)(n.th,{children:"Required?"}),(0,r.jsx)(n.th,{children:"Default"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_SCROBBLE"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"file"})}),(0,r.jsx)(n.td,{children:"The cache type to use"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_SCROBBLE_CONN"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"/config"})}),(0,r.jsx)(n.td,{})]})]})]})}),(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 "scrobble": {\n "provider": "file",\n "connection": "/config"\n }\n },\n // ...\n}\n'})})})]})]}),(0,r.jsxs)(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.jsxs)(n.admonition,{type:"tip",children:[(0,r.jsxs)(n.p,{children:["By default, this data use a ",(0,r.jsx)(n.a,{href:"#secondary-caching-configuration",children:"Secondary"})," ",(0,r.jsx)(n.a,{href:"./?cacheType=file#secondary-caching-configuration",children:"File"})," store, configured for you automatically."]}),(0,r.jsxs)(n.p,{children:["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"}),") then you are already done. If you are not persisting this directory then you should consider setting up ",(0,r.jsx)(n.a,{href:"./?cacheType=valkey#secondary-caching-configuration",children:"Valkey Cache"})," for this."]})]}),(0,r.jsx)(n.h5,{id:"configuration-1",children:"Configuration"}),(0,r.jsxs)(n.p,{children:["Use any ",(0,r.jsx)(n.a,{href:"#secondary-caching-configuration",children:"Secondary Cache"}),", the config examples below show the default values:"]}),(0,r.jsxs)(l.A,{children:[(0,r.jsx)(o.A,{value:"env",label:"ENV",children:(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,r.jsx)(n.th,{children:"Required?"}),(0,r.jsx)(n.th,{children:"Default"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_AUTH"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"file"})}),(0,r.jsx)(n.td,{children:"The cache type to use"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_AUTH_CONN"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"/config"})}),(0,r.jsx)(n.td,{})]})]})]})}),(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": "file",\n "connection": "/config"\n }\n },\n // ...\n}\n'})})})]})]}),(0,r.jsxs)(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."]}),(0,r.jsxs)(n.p,{children:["By default, these calls are only cached in memory. If you wish for cached calls to be persisted across restarts then setup ",(0,r.jsx)(n.a,{href:"./?cacheType=valkey#secondary-caching-configuration",children:"Valkey Cache"}),"."]}),(0,r.jsx)(n.h5,{id:"configuration-2",children:"Configuration"}),(0,r.jsxs)(n.p,{children:["Use any ",(0,r.jsx)(n.a,{href:"#secondary-caching-configuration",children:"Secondary Cache"}),", the config examples below show the default values:"]}),(0,r.jsxs)(l.A,{children:[(0,r.jsx)(o.A,{value:"env",label:"ENV",children:(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,r.jsx)(n.th,{children:"Required?"}),(0,r.jsx)(n.th,{children:"Default"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_METADATA"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{}),(0,r.jsx)(n.td,{children:"The cache type to use"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_METADATA_CONN"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{}),(0,r.jsx)(n.td,{})]})]})]})}),(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 "metadata": {\n "provider": "valkey",\n "connection": "yourConnectionStringHere"\n }\n },\n // ...\n}\n'})})})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"secondary-caching-configuration",children:"Secondary Caching Configuration"}),"\n",(0,r.jsxs)(n.p,{children:["The type of cache used, and its connection properties, can be configured through ",(0,r.jsx)(n.strong,{children:"ENV"})," or ",(0,r.jsx)(n.strong,{children:"AIO"})," config."]}),"\n",(0,r.jsxs)(l.A,{groupId:"cacheType",queryString:!0,children:[(0,r.jsxs)(o.A,{value:"file",label:"File",children:[(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"File"})," cache is stored in the ",(0,r.jsx)(n.code,{children:"CONFIG_DIR"})," directory using the pre-defined file name ",(0,r.jsx)(n.code,{children:"ms-[cacheName].cache"}),"."]}),(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:"Example"}),(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,r.jsx)(n.th,{children:"Required?"}),(0,r.jsx)(n.th,{children:"Default"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_SCROBBLE"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"file"})}),(0,r.jsx)(n.td,{children:"The cache type to use"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_SCROBBLE_CONN"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"/config"})}),(0,r.jsx)(n.td,{children:"The directory, within the container, to store the cache file"})]})]})]})]}),(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 "scrobble": {\n "provider": "file",\n "connection": "/config"\n }\n },\n // ...\n}\n'})})]})]})]}),(0,r.jsxs)(o.A,{value:"valkey",label:"Valkey",children:[(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"https://valkey.io/",children:(0,r.jsx)(n.strong,{children:"Valkey"})})," is an open-source fork of Redis."]}),(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.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n # ...\n # adding everything below\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.jsxs)(n.p,{children:["Use ",(0,r.jsx)(n.code,{children:"redis://valkey:6379"})," as the connection string in the configurations below."]})]}),(0,r.jsx)(n.p,{children:"The connection string used by multi-scrobbler to connect to your Valkey instance must be in the form:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"redis://HOST_IP:HOST_PORT\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:"Example"}),(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,r.jsx)(n.th,{children:"Required?"}),(0,r.jsx)(n.th,{children:"Default"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_METADATA"})}),(0,r.jsx)(n.td,{children:"Yes"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"valkey"})}),(0,r.jsx)(n.td,{children:"The cache type to use"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{style:{textAlign:"left"},children:(0,r.jsx)(n.code,{children:"CACHE_METADATA_CONN"})}),(0,r.jsx)(n.td,{children:"Yes"}),(0,r.jsx)(n.td,{}),(0,r.jsxs)(n.td,{children:["The host/IP and port to connect to EX: ",(0,r.jsx)(n.code,{children:"redis://192.168.0.120:6379"})]})]})]})]})]}),(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 "metadata": {\n "provider": "valkey",\n "connection": "redis://192.168.0.120:6379"\n }\n },\n // ...\n}\n'})})]})]})]})]}),"\n",(0,r.jsx)(n.h3,{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.h3,{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:"https://json-schema.app/view/%23/%23%2Fdefinitions%2FGotifyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json",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:"https://json-schema.app/view/%23/%23%2Fdefinitions%2FNtfyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json",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:"https://json-schema.app/view/%23/%23%2Fdefinitions%2FAppriseConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json",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)(c,{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 p(e={}){let{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(f,{...e})}):f(e)}function g(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);i(5161);var o=i(1219),c=i.n(o),a=i(4848);let d=e=>{let n,{data:i,name:s,client:l=!1}=e;try{n=c().parse(i)}catch(e){return console.error(e),(0,a.jsxs)(t.A,{type:"danger",title:"Unexpected Error",children:[(0,a.jsx)("p",{children:"Example component crashed because of error!"}),(0,a.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,a.jsx)(r(),{title:"CONFIG_DIR/config.json",language:"json5",children:JSON.stringify(o,null,2)})},h=e=>(0,a.jsx)(l.A,{fallback:({error:e})=>(0,a.jsx)("div",{children:(0,a.jsxs)("p",{children:["Example component crashed because of error: ",e.message,"."]})}),children:(0,a.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:c=!1,className:a,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}`:"",g=i?`button--${i}`:"";return(0,t.jsx)(r.A,{to:c?null:h,children:(0,t.jsx)("button",{className:(0,s.A)("button",p,n?"button--outline":"",g,o?"button--block":"",c?"disabled":"",a),style:d,role:"button","aria-disabled":c,onClick:e=>{e.preventDefault(),x()},...j,children:u})})}},9643(e,n,i){i.d(n,{A:()=>c}),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 c({size:e=null,variant:n="primary",block:i=!1,disabled:a=!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,c)=>{let a={style:o,outline:!0,size:e,variant:n,label:i[1],link:"#",onClick:()=>f(i[0])};return 0===c?a.style=t:c===u.length-1&&(a.style=l),(x??j)===i[0]&&(a.outline=!1),(0,r.jsx)(s.A,{...a},c)});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 c={};var a=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,g]=((e,n,i)=>{let r;return e in c?c[e]():o.A.canUseDOM?(c[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]}),c[e]()):[n,()=>null]})("docusaurus.tab.fileExampleShow",r,!1),m=(0,h.l)({desktopBreakpoint:j}),b=null,y=i&&"desktop"===m;y&&(b=(0,u.jsx)(a.A,{options:[["plain","Plain"],["interactive","Interactive"]],defaultValue:r,value:p,variant:"primary",size:"medium",onChange:e=>g(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 A=null;(null!==b||null!==v)&&(A=(0,u.jsxs)("div",{style:{marginBottom:"1em"},children:[b,v]}));let k=(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:[A,y&&"plain"!==p?null:(0,u.jsx)(t(),{title:x,language:"json5",children:k}),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),c=i(8182),a=i.n(c),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"),a().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),c=void 0===n?void 0:(0,r.Ay)(`schemas/${e.schemaName}.json`);return(0,s.useEffect)(()=>{void 0===c?t(!1):fetch(c,{headers:{Accept:"application/json"}}).then(e=>e.json()).then(e=>t(e)).catch(e=>t(e))},[t,c]),(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"},3169(e,n,i){i.d(n,{A:()=>s});let s='[\n {\n "name": "MySpotify",\n "enable": true,\n "clients": [],\n "data": {\n "clientId": "a89cba1569901a0671d5a9875fed4be1",\n "clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",\n "redirectUri": "http://localhost:9078/callback",\n "interval": 60\n }\n }\n]\n'},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/07b3b246.f7430fc0.js b/assets/js/07b3b246.f7430fc0.js deleted file mode 100644 index fbdda4ba..00000000 --- a/assets/js/07b3b246.f7430fc0.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["7340"],{4122(e,t,l){l.r(t),l.d(t,{metadata:()=>o,default:()=>p,frontMatter:()=>r,contentTitle:()=>s,toc:()=>d,assets:()=>a});var o=JSON.parse('{"id":"development/dev-client","title":"Client Development/Tutorial","description":"To do...","source":"@site/docs/development/dev-client.md","sourceDirName":"development","slug":"/development/dev-client","permalink":"/multi-scrobbler/development/dev-client","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/development/dev-client.md","tags":[],"version":"current","sidebarPosition":3,"frontMatter":{"toc_min_heading_level":2,"toc_max_heading_level":5,"sidebar_position":3,"title":"Client Development/Tutorial"},"sidebar":"tutorialSidebar","previous":{"title":"Source Development/Tutorial","permalink":"/multi-scrobbler/development/dev-source"},"next":{"title":"Flatpak","permalink":"/multi-scrobbler/development/flatpak"}}'),i=l(4848),n=l(8453);let r={toc_min_heading_level:2,toc_max_heading_level:5,sidebar_position:3,title:"Client Development/Tutorial"},s,a={},d=[];function c(e){let t={p:"p",...(0,n.R)(),...e.components};return(0,i.jsx)(t.p,{children:"To do..."})}function p(e={}){let{wrapper:t}={...(0,n.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}}}]); \ No newline at end of file diff --git a/assets/js/08938dd7.591dee72.js b/assets/js/08938dd7.591dee72.js deleted file mode 100644 index 029a7562..00000000 --- a/assets/js/08938dd7.591dee72.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["7417"],{1469(e,s,n){n.r(s),n.d(s,{metadata:()=>o,default:()=>h,frontMatter:()=>c,contentTitle:()=>l,toc:()=>a,assets:()=>d});var o=JSON.parse('{"id":"configuration/sources/rocksky-source","title":"Rocksky","description":"To scrobble to* Rocksky, create a Rocksky (Client)","source":"@site/docs/configuration/sources/rocksky-source.mdx","sourceDirName":"configuration/sources","slug":"/configuration/sources/rocksky-source","permalink":"/multi-scrobbler/configuration/sources/rocksky-source","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/sources/rocksky-source.mdx","tags":[],"version":"current","frontMatter":{"title":"Rocksky","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Plex","permalink":"/multi-scrobbler/configuration/sources/plex"},"next":{"title":"Sonos","permalink":"/multi-scrobbler/configuration/sources/sonos"}}'),r=n(4848),t=n(8453);n(8194),n(698),n(2170);var i=n(8165);let c={title:"Rocksky",toc_min_heading_level:2,toc_max_heading_level:5},l,d={},a=[{value:"Setup",id:"setup",level:2},{value:"Configuration",id:"configuration",level:2}];function u(e){let s={a:"a",admonition:"admonition",code:"code",h2:"h2",li:"li",p:"p",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components},{Config:n}=s;return n||function(e,s){throw Error("Expected "+(s?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Config",!0),(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(s.admonition,{title:"Other Uses",type:"tip",children:(0,r.jsxs)(s.ul,{children:["\n",(0,r.jsxs)(s.li,{children:["To ",(0,r.jsx)(s.strong,{children:"scrobble to"})," Rocksky, create a ",(0,r.jsx)(s.a,{href:"/configuration/clients/rocksky",children:"Rocksky (Client)"})]}),"\n"]})}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.a,{href:"https://rocksky.app/",children:(0,r.jsx)(s.strong,{children:"Rocksky"})})," is music tracking and discovery platform built on the AT Protocol (Bluesky)."]}),"\n",(0,r.jsx)(s.p,{children:"This Source monitors the Rocksky scrobbles in your PDS and then re-scrobbles discovered tracks to configured clients."}),"\n",(0,r.jsxs)(s.p,{children:["Learn more about how Rocksky scrobbles work in the ",(0,r.jsx)(s.a,{href:"/configuration/clients/rocksky",children:"Rocksky (Client)"})," docs."]}),"\n",(0,r.jsx)(s.h2,{id:"setup",children:"Setup"}),"\n",(0,r.jsxs)(s.p,{children:["Follow the ",(0,r.jsx)(s.strong,{children:"Setup"})," instructions in the ",(0,r.jsx)(s.a,{href:"/configuration/clients/rocksky#setup",children:"Rocksky (Client)"})," docs to get access to your Bluesky PDS repository and scrobble data."]}),"\n",(0,r.jsx)(s.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsxs)(s.p,{children:["See the ",(0,r.jsx)(s.a,{href:"/configuration/clients/rocksky#setup",children:"Rocksky (Client)"})," configuration for general setup. The only difference for ",(0,r.jsx)(s.strong,{children:"Source"})," configuration:"]}),"\n",(0,r.jsxs)(s.ul,{children:["\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.a,{href:"/configuration?configType=file#configuration-types",children:"File/AIO config"})," must include ",(0,r.jsx)(s.code,{children:'"configureAs": "source"'})]}),"\n"]}),"\n",(0,r.jsx)(n,{config:"RockskySourceConfig",fileContent:i.A,name:"rocksky",children:(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,r.jsx)(s.th,{children:"Required?"}),(0,r.jsx)(s.th,{children:"Default"}),(0,r.jsx)(s.th,{children:"Description"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{style:{textAlign:"left"},children:(0,r.jsx)(s.code,{children:"SOURCE_ROCKSKY_HANDLE"})}),(0,r.jsx)(s.td,{children:"Yes"}),(0,r.jsx)(s.td,{}),(0,r.jsxs)(s.td,{children:["Your ",(0,r.jsx)(s.strong,{children:"full"})," ATProto handle. For Bluesky account this is like ",(0,r.jsx)(s.code,{children:"myUser.bsky.social"})]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{style:{textAlign:"left"},children:(0,r.jsx)(s.code,{children:"SOURCE_ROCKSKY_KEY"})}),(0,r.jsx)(s.td,{children:"Yes"}),(0,r.jsx)(s.td,{}),(0,r.jsx)(s.td,{children:"API Key obtained from Rocksky developer dashboard"})]})]})]})})]})}function h(e={}){let{wrapper:s}={...(0,t.R)(),...e.components};return s?(0,r.jsx)(s,{...e,children:(0,r.jsx)(u,{...e})}):u(e)}},8165(e,s,n){n.d(s,{A:()=>o});let o='[\n {\n "name": "myRockskyClient",\n "configureAs": "client",\n "data": {\n "key": "7cdr86vis3mpq5b1pi01md0hkm7ykzzxjy81r",\n "handle": "alice.bsky.social"\n }\n },\n {\n "name": "myRockskySource",\n "configureAs": "source",\n "data": {\n "key": "e4xwktm7jxm607x8el1fcda30eu14dzb64h3j",\n "handle": "alice.bsky.social"\n }\n }\n]'}}]); \ No newline at end of file diff --git a/assets/js/08938dd7.7bd722ce.js b/assets/js/08938dd7.7bd722ce.js new file mode 100644 index 00000000..f7338ba5 --- /dev/null +++ b/assets/js/08938dd7.7bd722ce.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["7417"],{1469(e,s,o){o.r(s),o.d(s,{metadata:()=>n,default:()=>h,frontMatter:()=>c,contentTitle:()=>l,toc:()=>a,assets:()=>d});var n=JSON.parse('{"id":"configuration/sources/rocksky-source","title":"Rocksky","description":"Rocksky (Source)","source":"@site/docs/configuration/sources/rocksky-source.mdx","sourceDirName":"configuration/sources","slug":"/configuration/sources/rocksky-source","permalink":"/multi-scrobbler/configuration/sources/rocksky-source","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/sources/rocksky-source.mdx","tags":[],"version":"current","frontMatter":{"title":"Rocksky","description":"Rocksky (Source)","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Plex","permalink":"/multi-scrobbler/configuration/sources/plex"},"next":{"title":"Sonos","permalink":"/multi-scrobbler/configuration/sources/sonos"}}'),r=o(4848),t=o(8453);o(8194),o(698),o(2170);var i=o(8165);let c={title:"Rocksky",description:"Rocksky (Source)",toc_min_heading_level:2,toc_max_heading_level:5},l,d={},a=[{value:"Setup",id:"setup",level:2},{value:"Configuration",id:"configuration",level:2}];function u(e){let s={a:"a",admonition:"admonition",code:"code",h2:"h2",li:"li",p:"p",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components},{Config:o}=s;return o||function(e,s){throw Error("Expected "+(s?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Config",!0),(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(s.admonition,{title:"Other Uses",type:"tip",children:(0,r.jsxs)(s.ul,{children:["\n",(0,r.jsxs)(s.li,{children:["To ",(0,r.jsx)(s.strong,{children:"scrobble to"})," Rocksky, create a ",(0,r.jsx)(s.a,{href:"/configuration/clients/rocksky",children:"Rocksky (Client)"})]}),"\n"]})}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.a,{href:"https://rocksky.app/",children:(0,r.jsx)(s.strong,{children:"Rocksky"})})," is music tracking and discovery platform built on the AT Protocol (Bluesky)."]}),"\n",(0,r.jsx)(s.p,{children:"This Source monitors the Rocksky scrobbles in your PDS and then re-scrobbles discovered tracks to configured clients."}),"\n",(0,r.jsxs)(s.p,{children:["Learn more about how Rocksky scrobbles work in the ",(0,r.jsx)(s.a,{href:"/configuration/clients/rocksky",children:"Rocksky (Client)"})," docs."]}),"\n",(0,r.jsx)(s.h2,{id:"setup",children:"Setup"}),"\n",(0,r.jsxs)(s.p,{children:["Follow the ",(0,r.jsx)(s.strong,{children:"Setup"})," instructions in the ",(0,r.jsx)(s.a,{href:"/configuration/clients/rocksky#setup",children:"Rocksky (Client)"})," docs to get access to your Bluesky PDS repository and scrobble data."]}),"\n",(0,r.jsx)(s.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsxs)(s.p,{children:["See the ",(0,r.jsx)(s.a,{href:"/configuration/clients/rocksky#setup",children:"Rocksky (Client)"})," configuration for general setup. The only difference for ",(0,r.jsx)(s.strong,{children:"Source"})," configuration:"]}),"\n",(0,r.jsxs)(s.ul,{children:["\n",(0,r.jsxs)(s.li,{children:[(0,r.jsx)(s.a,{href:"/configuration?configType=file#configuration-types",children:"File/AIO config"})," must include ",(0,r.jsx)(s.code,{children:'"configureAs": "source"'})]}),"\n"]}),"\n",(0,r.jsx)(o,{config:"RockskySourceConfig",fileContent:i.A,name:"rocksky",children:(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,r.jsx)(s.th,{children:"Required?"}),(0,r.jsx)(s.th,{children:"Default"}),(0,r.jsx)(s.th,{children:"Description"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{style:{textAlign:"left"},children:(0,r.jsx)(s.code,{children:"SOURCE_ROCKSKY_HANDLE"})}),(0,r.jsx)(s.td,{children:"Yes"}),(0,r.jsx)(s.td,{}),(0,r.jsxs)(s.td,{children:["Your ",(0,r.jsx)(s.strong,{children:"full"})," ATProto handle. For Bluesky account this is like ",(0,r.jsx)(s.code,{children:"myUser.bsky.social"})]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{style:{textAlign:"left"},children:(0,r.jsx)(s.code,{children:"SOURCE_ROCKSKY_KEY"})}),(0,r.jsx)(s.td,{children:"Yes"}),(0,r.jsx)(s.td,{}),(0,r.jsx)(s.td,{children:"API Key obtained from Rocksky developer dashboard"})]})]})]})})]})}function h(e={}){let{wrapper:s}={...(0,t.R)(),...e.components};return s?(0,r.jsx)(s,{...e,children:(0,r.jsx)(u,{...e})}):u(e)}},8165(e,s,o){o.d(s,{A:()=>n});let n='[\n {\n "name": "myRockskyClient",\n "configureAs": "client",\n "data": {\n "key": "7cdr86vis3mpq5b1pi01md0hkm7ykzzxjy81r",\n "handle": "alice.bsky.social"\n }\n },\n {\n "name": "myRockskySource",\n "configureAs": "source",\n "data": {\n "key": "e4xwktm7jxm607x8el1fcda30eu14dzb64h3j",\n "handle": "alice.bsky.social"\n }\n }\n]'}}]); \ No newline at end of file diff --git a/assets/js/0ce74438.ca447096.js b/assets/js/0ce74438.1d21e72e.js similarity index 98% rename from assets/js/0ce74438.ca447096.js rename to assets/js/0ce74438.1d21e72e.js index d3cc52db..565c6255 100644 --- a/assets/js/0ce74438.ca447096.js +++ b/assets/js/0ce74438.1d21e72e.js @@ -1 +1 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["8406"],{3936(e,t,i){i.d(t,{A:()=>d});var s=i(6540),n=i(4164),r=i(7988),o=i(2303),a=i(1422),l=i(4848);function c(e){return!!e&&("SUMMARY"===e.tagName||c(e.parentElement))}function p({summary:e,children:t,...i}){(0,r.A)().collectAnchor(i.id);let d=(0,o.A)(),u=(0,s.useRef)(null),{collapsed:f,setCollapsed:h}=(0,a.u)({initialState:!i.open}),[y,m]=(0,s.useState)(i.open),b=s.isValidElement(e)?e:(0,l.jsx)("summary",{children:e??"Details"});return(0,l.jsxs)("details",{...i,ref:u,open:y,"data-collapsed":f,className:(0,n.A)("details_lb9f",d&&"isBrowser_bmU9",i.className),onMouseDown:e=>{c(e.target)&&e.detail>1&&e.preventDefault()},onClick:e=>{e.stopPropagation();let t=e.target;c(t)&&function e(t,i){return!!t&&(t===i||e(t.parentElement,i))}(t,u.current)&&(e.preventDefault(),f?(h(!1),m(!0)):h(!0))},children:[b,(0,l.jsx)(a.N,{lazy:!1,collapsed:f,onCollapseTransitionEnd:e=>{h(e),m(!e)},children:(0,l.jsx)("div",{className:"collapsibleContent_i85q",children:t})})]})}function d({...e}){return(0,l.jsx)(p,{...e,className:(0,n.A)("alert alert--info","details_b_Ee",e.className)})}},4254(e,t,i){i.d(t,{A:()=>c});var s=i(3936),n=i(804),r=i(633),o=i(3069),a=i(3550),l=i(4848);let c=e=>{let t,i="";switch(e.type){case"warning":i="alert--warning",t=(0,l.jsx)(n.A,{});break;case"danger":i="alert--danger",t=(0,l.jsx)(r.A,{});break;case"note":i="alert--secondary",t=(0,l.jsx)(a.A,{});break;case"tip":i="alert--success",t=(0,l.jsx)(o.A,{})}let c=void 0===t?null:(0,l.jsx)("span",{className:"admoIcon",children:t}),p=(0,l.jsxs)("summary",{children:[c,e.summary]});return(0,l.jsx)(s.A,{className:i,summary:p,children:e.children})}},5543(e,t,i){i.d(t,{A:()=>S});var s=i(6540),n=i(8478),r=i(5293),o=i(4581),a=i(2170),l=i.n(a),c=JSON.parse('{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"sourceDefaults":{"$ref":"#/definitions/SourceDefaults"},"clientDefaults":{"$ref":"#/definitions/ClientDefaults"},"sources":{"type":"array","items":{"$ref":"#/definitions/SourceAIOConfig"}},"clients":{"type":"array","items":{"$ref":"#/definitions/ClientAIOConfig"}},"webhooks":{"type":"array","items":{"$ref":"#/definitions/WebhookConfig"}},"port":{"type":"number","description":"Set the port the multi-scrobbler UI will be served from","default":9078,"examples":[9078]},"baseUrl":{"type":"string","description":"Set the Base URL the application should assume the UI is served from.\\n\\nThis will affect how default redirect URLs are generated (spotify, lastfm, deezer) and some logging messages.\\n\\nIt will NOT set the actual interface/IP that the application is listening on.\\n\\nThis can also be set using the BASE_URL environmental variable.","default":"http://localhost","examples":["http://localhost","http://192.168.0.101","https://ms.myDomain.tld"]},"logging":{"$ref":"#/definitions/LogOptions"},"disableWeb":{"type":"boolean","description":"Disable web server from running/listening on port.\\n\\nThis will also make any ingress sources (Plex, Jellyfin, Tautulli, etc...) unusable"},"debugMode":{"type":"boolean","description":"Enables ALL relevant logging and debug options for all sources/clients, when none are defined.\\n\\nThis is a convenience shortcut for enabling all output needed to troubleshoot an issue and does not need to be on for normal operation.\\n\\nIt can also be enabled with the environmental variable DEBUG_MODE=true","default":false,"examples":[false]},"cache":{"$ref":"#/definitions/CacheConfigOptions"},"transformers":{"type":"array","items":{"$ref":"#/definitions/TransformerCommonConfig"}}},"definitions":{"SourceDefaults":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"SourceDefaults"},"LogLevel":{"type":"string","enum":["silent","fatal","error","warn","info","log","verbose","debug","trace"],"description":"Names of log levels that can be invoked on the logger\\n\\nFrom lowest to highest:\\n\\n* `trace`\\n* `debug`\\n* `verbose`\\n* `log`\\n* `info`\\n* `warn`\\n* `error`\\n* `fatal`\\n* `silent` (will never output anything)\\n\\nWhen used in `LogOptions` specifies the **minimum** level the output should log at.","title":"LogLevel"},"FileLogOptions":{"type":"object","properties":{"timestamp":{"type":"string","enum":["unix","iso","auto"],"description":"For rolling log files\\n\\nWhen\\n* value passed to rolling destination is a string (`path` from LogOptions is a string) and\\n* `frequency` is defined\\n\\nThis determines the format of the datetime inserted into the log file name:\\n\\n* `unix` - unix epoch timestamp in milliseconds\\n* `iso` - Full [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) datetime IE \'2024-03-07T20:11:34Z\'\\n* `auto`\\n * When frequency is `daily` only inserts date IE YYYY-MM-DD\\n * Otherwise inserts full ISO8601 datetime","default":"auto"},"size":{"type":["number","string"],"description":"The maximum size of a given rolling log file.\\n\\nCan be combined with frequency. Use k, m and g to express values in KB, MB or GB.\\n\\nNumerical values will be considered as MB.","default":"10MB"},"frequency":{"anyOf":[{"type":"string","enum":["daily"]},{"type":"string","enum":["hourly"]},{"type":"number"}],"description":"The amount of time a given rolling log file is used. Can be combined with size.\\n\\nUse `daily` or `hourly` to rotate file every day (or every hour). Existing file within the current day (or hour) will be re-used.\\n\\nNumerical values will be considered as a number of milliseconds. Using a numerical value will always create a new file upon startup.","default":"daily"},"path":{"anyOf":[{"type":"string"},{"$comment":"() => string"}],"description":"The path and filename to use for log files.\\n\\nIf using rolling files the filename will be appended with `.N` (a number) BEFORE the extension based on rolling status.\\n\\nMay also be specified using env LOG_PATH or a function that returns a string.\\n\\nIf path is relative the absolute path will be derived from `logBaseDir` (in `LoggerAppExtras`) which defaults to CWD","default":"./logs/app.log"},"level":{"anyOf":[{"$ref":"#/definitions/LogLevel"},{"type":"boolean","const":false}],"description":"Specify the minimum log level to output to rotating files. If `false` no log files will be created."}},"title":"FileLogOptions"},"ScrobbleThresholds":{"type":"object","properties":{"duration":{"type":["number","null"],"description":"The number of seconds a track has been listened to before it should be considered scrobbled.\\n\\nSet to null to disable.","default":240,"examples":[240]},"percent":{"type":["number","null"],"description":"The percentage (as an integer) of a track that should have been seen played before it should be scrobbled. Only used if the Source provides information about how long the track is.\\n\\nSet to null to disable.\\n\\nNOTE: This should be used with care when the Source is a \\"polling\\" type (has an \'interval\' property). If the track is short and the interval is too high MS may ignore the track if percentage is high because it had not \\"seen\\" the track for long enough from first discovery, even if you have been playing the track for longer.","default":50,"examples":[50]}},"title":"ScrobbleThresholds"},"PlayTransformOptions":{"type":"object","properties":{"log":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["all"]}]},"preCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-5069%5B%5D%7CExternalMetadataTerm)%3E"},"compare":{"type":"object","properties":{"candidate":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-5069%5B%5D%7CExternalMetadataTerm)%3E"},"existing":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-5069%5B%5D%7CExternalMetadataTerm)%3E"}}},"postCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-5069%5B%5D%7CExternalMetadataTerm)%3E"}},"title":"PlayTransformOptions"},"PlayTransformPartsConfig<(def-alias-617285649-262-339-617285649-0-5069[]|ExternalMetadataTerm)>":{"anyOf":[{"$ref":"#/definitions/PlayTransformPartsArray%3C(def-alias-617285649-262-339-617285649-0-5069%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"},{"$ref":"#/definitions/PlayTransformParts%3C(def-alias-617285649-262-339-617285649-0-5069%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"}],"description":"Represents the weakly-defined user config. May be an array of parts or one parts object","title":"PlayTransformPartsConfig<(def-alias-617285649-262-339-617285649-0-5069[]|ExternalMetadataTerm)>"},"PlayTransformPartsArray<(def-alias-617285649-262-339-617285649-0-5069[]|ExternalMetadataTerm),MaybeStageTyped>":{"type":"array","items":{"$ref":"#/definitions/PlayTransformParts%3C(def-alias-617285649-262-339-617285649-0-5069%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"},"title":"PlayTransformPartsArray<(def-alias-617285649-262-339-617285649-0-5069[]|ExternalMetadataTerm),MaybeStageTyped>"},"PlayTransformParts<(def-alias-617285649-262-339-617285649-0-5069[]|ExternalMetadataTerm),MaybeStageTyped>":{"anyOf":[{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"artists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"albumArtists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"album":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"duration":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"meta":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"$ref":"#/definitions/StageTypeMetadata"},"name":{"type":"string"},"stageHash":{"type":"string"},"score":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"artists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"albumArtists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"album":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"duration":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"meta":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"$ref":"#/definitions/StageTypeUser"},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"artists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"albumArtists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"album":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"duration":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"meta":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"type":"string","enum":["native"]},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"artists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"albumArtists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"album":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"duration":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"meta":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"name":{"type":"string"}}},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{},"artists":{},"albumArtists":{},"album":{},"duration":{},"meta":{},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"type":"string"},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]}],"title":"PlayTransformParts<(def-alias-617285649-262-339-617285649-0-5069[]|ExternalMetadataTerm),MaybeStageTyped>"},"WhenConditionsConfig":{"$ref":"#/definitions/WhenConditions%3Cstring%3E","title":"WhenConditionsConfig"},"WhenConditions":{"type":"array","items":{"$ref":"#/definitions/WhenParts%3Cstring%3E"},"title":"WhenConditions"},"WhenParts":{"$ref":"#/definitions/PlayTransformPartsAtomic%3Cstring%3E","title":"WhenParts"},"PlayTransformPartsAtomic":{"type":"object","properties":{"title":{"type":"string"},"artists":{"type":"string"},"albumArtists":{"type":"string"},"album":{"type":"string"},"duration":{"type":"string"},"meta":{"type":"string"}},"title":"PlayTransformPartsAtomic"},"Whennable":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"}},"title":"Whennable"},"FlowControlTerm":{"type":"string","enum":["continue","stop"],"title":"FlowControlTerm"},"StageTypeMetadata":{"type":"string","enum":["spotify","musicbrainz","native"],"title":"StageTypeMetadata"},"SearchAndReplaceTerm":{"anyOf":[{"type":"string"},{"$ref":"#/definitions/ConditionalSearchAndReplaceTerm"}],"title":"SearchAndReplaceTerm"},"ConditionalSearchAndReplaceTerm":{"$ref":"#/definitions/ConditionalSearchAndReplaceRegExp","title":"ConditionalSearchAndReplaceTerm"},"ConditionalSearchAndReplaceRegExp":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"search":{"anyOf":[{"type":"string"},{"type":"string","format":"regex"}],"description":"The search value to test for\\n\\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object","examples":[["find this string","/some string*/ig"]]},"replace":{"type":"string","description":"The replacement string/value to use when search is found\\n\\nThis can be a literal string like `\'replace with this`, an empty string to remove the search value (`\'\'`), or a special regex value\\n\\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace"},"test":{"$comment":"(obj: SearchAndReplaceRegExp) => boolean","type":"object","properties":{"namedArgs":{"type":"object","properties":{"obj":{"$ref":"#/definitions/SearchAndReplaceRegExp"}},"required":["obj"],"additionalProperties":false}},"description":"An optional function to execute before running this search-and-replace that decides if it should run at all"}},"required":["replace","search"],"title":"ConditionalSearchAndReplaceRegExp"},"SearchAndReplaceRegExp":{"type":"object","properties":{"search":{"anyOf":[{"type":"string"},{"type":"string","format":"regex"}],"description":"The search value to test for\\n\\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object","examples":[["find this string","/some string*/ig"]]},"replace":{"type":"string","description":"The replacement string/value to use when search is found\\n\\nThis can be a literal string like `\'replace with this`, an empty string to remove the search value (`\'\'`), or a special regex value\\n\\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace"},"test":{"$comment":"(obj: SearchAndReplaceRegExp) => boolean","type":"object","properties":{"namedArgs":{"type":"object","properties":{"obj":{"$ref":"#/definitions/SearchAndReplaceRegExp"}},"required":["obj"],"additionalProperties":false}},"description":"An optional function to execute before running this search-and-replace that decides if it should run at all"}},"required":["search","replace"],"title":"SearchAndReplaceRegExp"},"ExternalMetadataTerm":{"anyOf":[{"type":"boolean"},{"not":{}},{"$ref":"#/definitions/Whennable"}],"title":"ExternalMetadataTerm"},"StageTypeUser":{"type":"string","enum":["user"],"title":"StageTypeUser"},"ClientDefaults":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\\n\\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\\n\\nThis setting should only be changed in specific scenarios where MS is handling multiple \\"relaying\\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\\n\\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \\"new\\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":3,"examples":[3]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"ClientDefaults"},"MatchLoggingOptions":{"type":"object","properties":{"onNoMatch":{"type":"boolean","description":"Log to DEBUG when a new track does NOT match an existing scrobble","default":false,"examples":[false]},"onMatch":{"type":"boolean","description":"Log to DEBUG when a new track DOES match an existing scrobble","default":false,"examples":[false]},"confidenceBreakdown":{"type":"boolean","description":"Include confidence breakdowns in track match logging, if applicable","default":false,"examples":[false]}},"description":"Scrobble matching (between new source track and existing client scrobbles) logging options. Used for debugging.","title":"MatchLoggingOptions"},"SourceAIOConfig":{"anyOf":[{"$ref":"#/definitions/SpotifySourceAIOConfig"},{"$ref":"#/definitions/PlexApiSourceAIOConfig"},{"$ref":"#/definitions/DeezerAIOCompatConfig"},{"$ref":"#/definitions/ListenbrainzEndpointSourceAIOConfig"},{"$ref":"#/definitions/LastFMEndpointSourceAIOConfig"},{"$ref":"#/definitions/SubsonicSourceAIOConfig"},{"$ref":"#/definitions/JellyApiSourceAIOConfig"},{"$ref":"#/definitions/LastFmSouceAIOConfig"},{"$ref":"#/definitions/LibrefmSouceAIOConfig"},{"$ref":"#/definitions/YTMusicSourceAIOConfig"},{"$ref":"#/definitions/YandexMusicBridgeSourceAIOConfig"},{"$ref":"#/definitions/MPRISSourceAIOConfig"},{"$ref":"#/definitions/MopidySourceAIOConfig"},{"$ref":"#/definitions/ListenBrainzSourceAIOConfig"},{"$ref":"#/definitions/JRiverSourceAIOConfig"},{"$ref":"#/definitions/KodiSourceAIOConfig"},{"$ref":"#/definitions/WebScrobblerSourceAIOConfig"},{"$ref":"#/definitions/ChromecastSourceAIOConfig"},{"$ref":"#/definitions/MalojaSourceAIOConfig"},{"$ref":"#/definitions/MusikcubeSourceAIOConfig"},{"$ref":"#/definitions/MusicCastSourceAIOConfig"},{"$ref":"#/definitions/MPDSourceAIOConfig"},{"$ref":"#/definitions/VLCSourceAIOConfig"},{"$ref":"#/definitions/IcecastSourceAIOConfig"},{"$ref":"#/definitions/AzuracastSourceAIOConfig"},{"$ref":"#/definitions/KoitoSourceAIOConfig"},{"$ref":"#/definitions/TealSourceAIOConfig"},{"$ref":"#/definitions/RockskySourceAIOConfig"},{"$ref":"#/definitions/SonosSourceAIOConfig"}],"title":"SourceAIOConfig"},"SpotifySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SpotifySourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["spotify"]}},"required":["data","type"],"title":"SpotifySourceAIOConfig"},"SpotifySourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)\\n\\nIt is unlikely you should need to change this unless you scrobble many very short tracks often\\n\\nReading:\\n* https://developer.spotify.com/documentation/web-api/guides/rate-limits/\\n* https://medium.com/mendix/limiting-your-amount-of-calls-in-mendix-most-of-the-time-rest-835dde55b10e\\n * Rate limit may ~180 req/min\\n* https://community.spotify.com/t5/Spotify-for-Developers/Web-API-ratelimit/m-p/5503150/highlight/true#M7930\\n * Informally indicated as 20 req/sec? Probably for burstiness","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"clientId":{"type":"string","description":"spotify client id","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"clientSecret":{"type":"string","description":"spotify client secret","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"redirectUri":{"type":"string","description":"spotify redirect URI -- required only if not the default shown here. URI must end in \\"callback\\"","default":"http://localhost:9078/callback","examples":["http://localhost:9078/callback"]}},"required":["clientId","clientSecret"],"title":"SpotifySourceData"},"CommonSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"CommonSourceOptions"},"PlexApiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/PlexApiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/PlexApiOptions"},"type":{"type":"string","enum":["plex"]}},"required":["data","type"],"title":"PlexApiSourceAIOConfig"},"PlexApiData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"token":{"type":"string"},"url":{"type":"string","description":"http(s)://HOST:PORT of the Plex server to connect to"},"usersAllow":{"anyOf":[{"type":"string"},{"type":"boolean","enum":[true]},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\\n\\nIf `true` MS will scrobble activity from all users"},"usersBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble for these users (case-insensitive)"},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device or application name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device or application name contains strings from this list (case-insensitive)"},"librariesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if library name contains string from this list (case-insensitive)"},"librariesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if library name contains strings from this list (case-insensitive)"}},"required":["url"],"title":"PlexApiData"},"PlexApiOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"ignoreInvalidCert":{"type":"boolean","description":"Ignore invalid cert errors when connecting to Plex\\n\\nUseful for Plex servers using \\"Required\\" Secure Connections with self-signed certificates\\n\\nDo not enable unless you know you need this.","default":false}},"title":"PlexApiOptions"},"DeezerAIOCompatConfig":{"anyOf":[{"$ref":"#/definitions/DeezerSourceAIOConfig"},{"$ref":"#/definitions/DeezerInternalAIOConfig"}],"title":"DeezerAIOCompatConfig"},"DeezerSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/DeezerData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["deezer"]}},"required":["data","type"],"title":"DeezerSourceAIOConfig"},"DeezerData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"clientId":{"type":"string","description":"deezer client id","examples":["a89cba1569901a0671d5a9875fed4be1"]},"clientSecret":{"type":"string","description":"deezer client secret","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"redirectUri":{"type":"string","description":"deezer redirect URI -- required only if not the default shown here. URI must end in \\"callback\\"","default":"http://localhost:9078/deezer/callback","examples":["http://localhost:9078/deezer/callback"]},"accessToken":{"type":"string"}},"required":["clientId","clientSecret"],"title":"DeezerData"},"DeezerInternalAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/DeezerInternalData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/DeezerInternalSourceOptions"},"type":{"type":"string","enum":["deezer"]}},"required":["data","type"],"title":"DeezerInternalAIOConfig"},"DeezerInternalData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"arl":{"type":"string","description":"ARL retrieved from Deezer response header"},"userAgent":{"type":"string","description":"User agent","default":"Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0"},"accountId":{"type":"string","description":"The ID (USER_ID) of the linked account to monitor. If not set, monitors the main ARL account"}},"required":["arl"],"title":"DeezerInternalData"},"DeezerInternalSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"fuzzyDiscoveryIgnore":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["aggressive"]}]}},"title":"DeezerInternalSourceOptions"},"ListenbrainzEndpointSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ListenbrainzEndpointData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["endpointlz"]}},"required":["type"],"title":"ListenbrainzEndpointSourceAIOConfig"},"ListenbrainzEndpointData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\\n\\nIf you are using multiple Listenbrainz endpoint sources (scrobbles for many users) you can use a slug to match Sources with individual users/origins\\n\\nExample:\\n\\n* slug: \'usera\' => API URL: http://localhost:9078/api/listenbrainz/usera\\n* slug: \'originb\' => API URL: http://localhost:9078/api/listenbrainz/originb\\n\\nIf no slug is found from an extension\'s incoming webhook event the first Listenbrainz source without a slug will be used"},"token":{"type":["string","null"],"description":"If an LZ submission request contains this token in the Authorization Header it will be used to match the submission with this Source\\n\\nSee: https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#add-the-user-token-to-your-requests"}},"title":"ListenbrainzEndpointData"},"LastFMEndpointSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LastFMEndpointData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["endpointlfm"]}},"required":["type"],"title":"LastFMEndpointSourceAIOConfig"},"LastFMEndpointData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\\n\\nIf you are using multiple Last.fm endpoint sources (scrobbles for many users) you can use a slug to match Sources with individual users/origins\\n\\nExample:\\n\\n* slug: \'usera\' => API URL: http://localhost:9078/api/lastfm/usera\\n* slug: \'originb\' => API URL: http://localhost:9078/api/lastfm/originb\\n\\nIf no slug is found from an extension\'s incoming webhook event the first Last.fm source without a slug will be used"}},"title":"LastFMEndpointData"},"SubsonicSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SubsonicData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["subsonic"]}},"required":["data","type"],"title":"SubsonicSourceAIOConfig"},"SubsonicData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the subsonic media server to query","examples":["http://airsonic.local"]},"user":{"type":"string","description":"Username to login to the server with","examples":[["MyUser"]]},"password":{"type":"string","description":"Password for the user to login to the server with","examples":["MyPassword"]},"ignoreTlsErrors":{"type":"boolean","description":"If your subsonic server is using self-signed certs you may need to disable TLS errors in order to get a connection\\n\\nWARNING: This should be used with caution as your traffic may not be encrypted.","default":false},"legacyAuthentication":{"type":"boolean","description":"Older Subsonic versions, and some badly implemented servers (Nextcloud), use legacy authentication which sends your password in CLEAR TEXT. This is less secure than the newer, recommended hashing authentication method but in some cases it is needed. See \\"Authentication\\" section here => https://www.subsonic.org/pages/api.jsp\\n\\nIf this option is not specified it will be turned on if the subsonic server responds with error code 41 \\"Token authentication not supported for LDAP users.\\" -- See Error Handling section => https://www.subsonic.org/pages/api.jsp","default":false},"usersAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\\n\\nIf undefined or an empty string/list MS will scrobble activity from all users"}},"required":["url","user","password"],"title":"SubsonicData"},"JellyApiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JellyApiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/JellyApiOptions"},"type":{"type":"string","enum":["jellyfin"]}},"required":["data","type"],"title":"JellyApiSourceAIOConfig"},"JellyApiData":{"type":"object","properties":{"url":{"type":"string","description":"HOST:PORT of the Jellyfin server to connect to"},"user":{"type":"string","description":"The username of the user to authenticate for or track scrobbles for"},"password":{"type":"string","description":"Password of the username to authenticate for\\n\\nRequired if `apiKey` is not provided."},"apiKey":{"type":"string","description":"API Key to authenticate with.\\n\\nRequired if `password` is not provided."},"usersAllow":{"anyOf":[{"type":"string"},{"type":"boolean","enum":[true]},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\\n\\nIf `true` MS will scrobble activity from all users"},"usersBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble for these users (case-insensitive)"},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device or application name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device or application name contains strings from this list (case-insensitive)"},"librariesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if library name contains string from this list (case-insensitive)"},"librariesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if library name contains strings from this list (case-insensitive)"},"additionalAllowedLibraryTypes":{"type":"array","items":{},"description":"Allow MS to scrobble audio media in libraries classified other than \'music\'\\n\\n`librariesAllow` will achieve the same result as this but this is more convenient if you do not want to explicitly list every library name or are only using `librariesBlock`"},"allowUnknown":{"type":"boolean","description":"Force media with a type of \\"Unknown\\" to be counted as Audio","default":false,"deprecated":"use allowMediaTypes instead"},"allowMediaTypes":{"anyOf":[{"type":"array","items":{}},{"type":"string"}],"description":"Allow these media types to be scrobbled.\\n\\nIf not defined or empty, uses \'Audio\' as default. If non-empty then *only* uses these types (make sure you include Audio!)\\n\\nValues are case-insensitive.\\n\\nSee https://github.com/jellyfin/jellyfin-sdk-typescript/blob/master/src/generated-client/models/media-type.ts#L22 for possible types"},"frontendUrlOverride":{"type":"string","description":"HOST:PORT of the Jellyfin server that your browser will be able to access from the frontend (and thus load images and links from)\\nIf unspecified it will use the normal server HOST and PORT from the `url`\\nNecessary if you are using a reverse proxy or other network configuration that prevents the frontend from accessing the server directly\\n\\nENV: JELLYFIN_FRONTEND_URL_OVERRIDE"}},"required":["url","user"],"title":"JellyApiData"},"JellyApiOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"JellyApiOptions"},"LastFmSouceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LastFmSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `lastfm.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["lastfm"]}},"required":["configureAs","data","type"],"title":"LastFmSouceAIOConfig"},"LastFmSourceData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"API Key generated from Last.fm/Libre.fm account","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"secret":{"type":"string","description":"Secret generated from Last.fm/Libre.fm account","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \\"lastfm/callback\\" in the URL somewhere.","default":"http://localhost:9078/lastfm/callback","examples":["http://localhost:9078/lastfm/callback"]},"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"}},"required":["apiKey","secret"],"title":"LastFmSourceData"},"LibrefmSouceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LibrefmData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `librefm.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["librefm"]}},"required":["data","type"],"title":"LibrefmSouceAIOConfig"},"LibrefmData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"Optional Secret for Libre.fm account\\n\\nThis can be anything, or it will be generated by multi-scrobbler","examples":["anyValue"]},"secret":{"type":"string","description":"Optional Secret for Libre.fm account\\n\\nThis can be anything, or it will be generated by multi-scrobbler","examples":["anyValue"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"path":{"type":"string"},"urlBase":{"type":"string","description":"(Optional) The host and path prefix for your Libre.fm instance","default":"https://libre.fm/2.0/"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \\"librefm/callback\\" in the URL somewhere.","default":"http://localhost:9078/librefm/callback","examples":["http://localhost:9078/librefm/callback"]}},"title":"LibrefmData"},"YTMusicSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/YTMusicData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"type":"object","properties":{"logAuth":{"type":"boolean","description":"When true MS will log to DEBUG all of the credentials data it receives from YTM"},"logDiff":{"type":"boolean","description":"Always log history diff\\n\\nBy default MS will log to `WARN` if history diff is inconsistent but does not log if diff is expected (on new tracks found)\\nSet this to `true` to ALWAYS log diff on new tracks. Expected diffs will log to `DEBUG` and inconsistent diffs will continue to log to `WARN`","default":false},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}}},"type":{"type":"string","enum":["ytmusic"]}},"required":["type"],"title":"YTMusicSourceAIOConfig"},"YTMusicData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"cookie":{"type":"string","description":"The cookie retrieved from the Request Headers of music.youtube.com after logging in.\\n\\nSee https://ytmusicapi.readthedocs.io/en/stable/setup/browser.html#copy-authentication-headers for how to retrieve this value.","examples":["VISITOR_INFO1_LIVE=jMp2xA1Xz2_PbVc; __Secure-3PAPISID=3AxsXpy0M/AkISpjek; ..."]},"clientId":{"type":"string","description":"Google Cloud Console project OAuth Client ID\\n\\nGenerated from a custom OAuth Client, see docs"},"clientSecret":{"type":"string","description":"Google Cloud Console project OAuth Client Secret\\n\\nGenerated from a custom OAuth Client, see docs"},"redirectUri":{"type":"string","description":"Google Cloud Console project OAuth Client Authorized redirect URI\\n\\nGenerated from a custom OAuth Client, see docs. multi-scrobbler will generate a default based on BASE_URL.\\nOnly specify this if the default does not work for you."},"innertubeOptions":{"$ref":"#/definitions/InnertubeOptions","description":"Additional options for authorization and tailoring YTM client"}},"title":"YTMusicData"},"InnertubeOptions":{"type":"object","properties":{"po_token":{"type":"string","description":"Proof of Origin token\\n\\nMay be required if YTM starts returning 403"},"visitor_data":{"type":"string","description":"Visitor ID value found in VISITOR_INFO1_LIVE or visitorData cookie\\n\\nMay be required if YTM starts returning 403"},"account_index":{"type":"number","description":"If account login results in being able to choose multiple account, use a zero-based index to choose which one to monitor","examples":[0,1]},"location":{"type":"string"},"lang":{"type":"string"},"generate_session_locally":{"type":"boolean"},"device_category":{"type":"string"},"client_type":{"type":"string"},"timezone":{"type":"string"}},"title":"InnertubeOptions"},"YandexMusicBridgeSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/YandexMusicBridgeData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["ymbridge"]}},"required":["type"],"title":"YandexMusicBridgeSourceAIOConfig"},"YandexMusicBridgeData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the local Python bridge, for example http://yandex-music-bridge:9980"},"apiKey":{"type":"string","description":"Optional API key sent as X-API-Key to the bridge"}},"required":["url"],"title":"YandexMusicBridgeData"},"MPRISSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MPRISData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["mpris"]}},"required":["data","type"],"title":"MPRISSourceAIOConfig"},"MPRISData":{"type":"object","properties":{"blacklist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any players that START WITH these values, case-insensitive","examples":[["spotify","vlc"]]},"whitelist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY from any players that START WITH these values, case-insensitive\\n\\nIf whitelist is present then blacklist is ignored","examples":[["spotify","vlc"]]}},"title":"MPRISData"},"MopidySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MopidyData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["mopidy"]}},"required":["data","type"],"title":"MopidySourceAIOConfig"},"MopidyData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the Mopidy HTTP server to connect to\\n\\nYou MUST have Mopidy-HTTP extension enabled: https://mopidy.com/ext/http\\n\\nmulti-scrobbler connects to the WebSocket endpoint that ultimately looks like this => `ws://localhost:6680/mopidy/ws/`\\n\\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\\n\\nParts => [default value]\\n\\n* Protocol => `ws://`\\n* Hostname => `localhost`\\n* Port => `6680`\\n* Path => `/mopidy/ws/`","examples":["ws://localhost:6680/mopidy/ws/"],"default":"ws://localhost:6680/mopidy/ws/"},"uriBlacklist":{"type":"array","items":{"type":"string"},"description":"Do not scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive\\n\\nEX: Don\'t scrobble tracks from soundcloud by adding \'soundcloud\' to this list.\\n\\nList is ignored if uriWhitelist is used."},"uriWhitelist":{"type":"array","items":{"type":"string"},"description":"Only scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive\\n\\nEX: Only scrobble tracks from soundcloud by adding \'soundcloud\' to this list."},"albumBlacklist":{"type":"array","items":{"type":"string"},"description":"Remove album data that matches any case-insensitive string from this list when scrobbling,\\n\\nFor certain sources (Soundcloud) Mopidy does not have all track info (Album) and will instead use \\"Soundcloud\\" as the Album name. You can prevent multi-scrobbler from using this bad Album data by adding the fake name to this list. Multi-scrobbler will still scrobble the track, just without the bad data.","examples":[["Soundcloud","Mixcloud"]],"default":["Soundcloud"]}},"title":"MopidyData"},"ListenBrainzSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ListenBrainzSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `listenbrainz.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["listenbrainz"]}},"required":["configureAs","data","type"],"title":"ListenBrainzSourceAIOConfig"},"ListenBrainzSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the ListenBrainz server, if not using the default","examples":["https://api.listenbrainz.org/"],"default":"https://api.listenbrainz.org/"},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","username"],"title":"ListenBrainzSourceData"},"JRiverSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JRiverData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["jriver"]}},"required":["data","type"],"title":"JRiverSourceAIOConfig"},"JRiverData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the JRiver HTTP server to connect to\\n\\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:52199/MCWS/v1/`\\n\\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\\n\\nParts => [default value]\\n\\n* Protocol => `http://`\\n* Hostname => `localhost`\\n* Port => `52199`\\n* Path => `/MCWS/v1/`","examples":["http://localhost:52199/MCWS/v1/"],"default":"http://localhost:52199/MCWS/v1/"},"username":{"type":"string","description":"If you have enabled authentication, the username you set"},"password":{"type":"string","description":"If you have enabled authentication, the password you set"}},"required":["url"],"title":"JRiverData"},"KodiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/KodiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["kodi"]}},"required":["data","type"],"title":"KodiSourceAIOConfig"},"KodiData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the Kodi HTTP server to connect to\\n\\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:8080/jsonrpc`\\n\\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\\n\\nParts => [default value]\\n\\n* Protocol => `http://`\\n* Hostname => `localhost`\\n* Port => `8080`\\n* Path => `/jsonrpc`","examples":["http://localhost:8080/jsonrpc"],"default":"http://localhost:8080/jsonrpc"},"username":{"type":"string","description":"The username set for Remote Control via Web Sever"},"password":{"type":"string","description":"The password set for Remote Control via Web Sever"}},"required":["url","username","password"],"title":"KodiData"},"WebScrobblerSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/WebScrobblerData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["webscrobbler"]}},"required":["type"],"title":"WebScrobblerSourceAIOConfig"},"WebScrobblerData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\\n\\nIn WebScrobbler\'s Webhook you must set an \'API URL\'. All MS WebScrobbler sources must start like:\\n\\nhttp://localhost:9078/api/webscrobbler\\n\\nIf you are using multiple WebScrobbler sources (scrobbles for many users) you must use a slug to match Sources with each users extension.\\n\\nExample:\\n\\n* slug: \'usera\' => API URL: http://localhost:9078/api/webscrobbler/usera\\n* slug: \'userb\' => API URL: http://localhost:9078/api/webscrobbler/userb\\n\\nIf no slug is found from an extension\'s incoming webhook event the first WebScrobbler source without a slug will be used"},"blacklist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Block scrobbling from specific WebScrobbler Connectors","examples":[["youtube"]]},"whitelist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only allow scrobbling from specific WebScrobbler Connectors","examples":[["mixcloud","soundcloud","bandcamp"]]}},"title":"WebScrobblerData"},"ChromecastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ChromecastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["chromecast"]}},"required":["data","type"],"title":"ChromecastSourceAIOConfig"},"ChromecastData":{"type":"object","properties":{"blacklistDevices":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any cast devices that START WITH these values, case-insensitive\\n\\nUseful when used with auto discovery","examples":[["home-mini","family-tv"]]},"whitelistDevices":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY scrobble from any cast device that START WITH these values, case-insensitive\\n\\nIf whitelist is present then blacklist is ignored\\n\\nUseful when used with auto discovery","examples":[["home-mini","family-tv"]]},"blacklistApps":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any application that START WITH these values, case-insensitive","examples":[["spotify","pandora"]]},"whitelistApps":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY scrobble from any application that START WITH these values, case-insensitive\\n\\nIf whitelist is present then blacklist is ignored","examples":[["spotify","pandora"]]},"useAvahi":{"type":"boolean","description":"Try to use Avahi and avahi-browse to resolve mDNS devices instead of native mDNS querying\\n\\nUseful for docker (alpine) container where mDNS resolution is not yet supported. Avahi socket must be exposed to the container and avahi-tools must be installed.","default":false},"useAutoDiscovery":{"type":"boolean","description":"Use mDNS to discovery Google Cast devices on your next automatically?\\n\\nIf not explicitly set then it is TRUE if `devices` is not set"},"devices":{"type":"array","items":{"$ref":"#/definitions/ChromecastDeviceInfo"},"description":"A list of Google Cast devices to monitor\\n\\nIf this is used then `useAutoDiscovery` is set to FALSE, if not explicitly set"},"allowUnknownMedia":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Chromecast Apps report a \\"media type\\" in the status info returned for whatever is currently playing\\n\\n* If set to TRUE then Music AND Generic/Unknown media will be tracked for ALL APPS\\n* If set to FALSE then only media explicitly typed as Music will be tracked for ALL APPS\\n* If set to a list then only Apps whose name contain one of these values, case-insensitive, will have Music AND Generic/Unknown tracked\\n\\nSee https://developers.google.com/cast/docs/media/messages#MediaInformation \\"metadata\\" property","default":false},"forceMediaRecognitionOn":{"type":"array","items":{"type":"string"},"description":"Media provided by any App whose name is listed here will ALWAYS be tracked, regardless of the \\"media type\\" reported\\n\\nApps will be recognized if they CONTAIN any of these values, case-insensitive"}},"title":"ChromecastData"},"ChromecastDeviceInfo":{"type":"object","properties":{"name":{"type":"string","description":"A friendly name to identify this device","examples":["MySmartTV"]},"address":{"type":"string","description":"The IP address of the device","examples":["192.168.0.115"]}},"required":["name","address"],"title":"ChromecastDeviceInfo"},"MalojaSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MalojaSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `maloja.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["maloja"]}},"required":["configureAs","data","type"],"title":"MalojaSourceAIOConfig"},"MalojaSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for maloja server","examples":["http://localhost:42010"]},"apiKey":{"type":"string","description":"API Key for Maloja server","examples":["myApiKey"]}},"required":["apiKey","url"],"title":"MalojaSourceData"},"MusikcubeSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MusikcubeData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["musikcube"]}},"required":["data","type"],"title":"MusikcubeSourceAIOConfig"},"MusikcubeData":{"type":"object","properties":{"url":{"type":"string","description":"URL of the Musikcube Websocket (Metadata) server to connect to\\n\\nYou MUST have enabled \'metadata\' server and set a password: https://github.com/clangen/musikcube/wiki/remote-api-documentation\\n * musikcube -> settings -> server setup\\n\\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\\n\\nParts => [default value]\\n\\n* Protocol => `ws://`\\n* Hostname => `localhost`\\n* Port => `7905`","examples":["ws://localhost:7905"],"default":"ws://localhost:7905"},"password":{"type":"string","description":"Password set in Musikcube https://github.com/clangen/musikcube/wiki/remote-api-documentation\\n\\n* musikcube -> settings -> server setup -> password"},"device_id":{"type":"string"}},"required":["password"],"title":"MusikcubeData"},"MusicCastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MusicCastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["musiccast"]}},"required":["data","type"],"title":"MusicCastSourceAIOConfig"},"MusicCastData":{"type":"object","properties":{"url":{"type":"string","description":"The host or URL of the YamahaExtendedControl endpoint to use","examples":[["192.168.0.101","http://192.168.0.101/YamahaExtendedControl"]]}},"required":["url"],"title":"MusicCastData"},"MPDSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MPDData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/MPDSourceOptions"},"type":{"type":"string","enum":["mpd"]}},"required":["data","options","type"],"title":"MPDSourceAIOConfig"},"MPDData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL:PORT of the MPD server to connect to\\n\\nTo use this you must have TCP connections enabled for your MPD server https://mpd.readthedocs.io/en/stable/user.html#client-connections","examples":["localhost:6600"],"default":"localhost:6600"},"path":{"type":"string","description":"If using socket specify the path instead of url.\\n\\ntrailing `~` is replaced by your home directory"},"password":{"type":"string","description":"Password for the server, if set https://mpd.readthedocs.io/en/stable/user.html#permissions-and-passwords"}},"title":"MPDData"},"MPDSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"MPDSourceOptions"},"VLCSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/VLCData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/VLCSourceOptions"},"type":{"type":"string","enum":["vlc"]}},"required":["data","type"],"title":"VLCSourceAIOConfig"},"VLCData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL:PORT of the VLC server to connect to\\n\\nTo use this you must have the Web (http) interface module enabled and a password set https://foxxmd.github.io/multi-scrobbler/docs/configuration#vlc","examples":["localhost:8080"],"default":"localhost:8080"},"password":{"type":"string","description":"Password for the server"}},"required":["password"],"title":"VLCData"},"VLCSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"filenamePatterns":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"A list of regular expressions to use to extract metadata (title, album, artist) from a filename\\n\\nUsed when VLC reports only the filename for the current audio track"},"logFilenamePatterns":{"type":"boolean","description":"Log to DEBUG when a filename-only track is matched or not matched by filenamePatterns","default":false},"dumpVlcMetadata":{"type":"boolean","description":"Dump all the metadata VLC reports for an audio track to DEBUG.\\n\\nUse this if reporting an issue with VLC not correctly capturing metadata for a track.","default":false}},"title":"VLCSourceOptions"},"IcecastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/IcecastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/IcecastSourceOptions"},"type":{"type":"string","enum":["icecast"]}},"required":["data","type"],"title":"IcecastSourceAIOConfig"},"IcecastData":{"type":"object","properties":{"sources":{"type":"array","items":{"$ref":"#/definitions/IcecastSource"}},"icestatsEndpoint":{"type":"string"},"statsEndpoint":{"type":"string"},"nextsongsEndpoint":{"type":"string"},"sevenhtmlEndpoint":{"type":"string"},"icyMetaInt":{"type":"number"},"url":{"type":"string","description":"The Icecast stream URL"}},"required":["url"],"title":"IcecastData"},"IcecastSource":{"type":"string","enum":["icy","ogg","icestats","stats","sevenhtml","nextsongs"],"title":"IcecastSource"},"IcecastSourceOptions":{"type":"object","properties":{"systemScrobble":{"type":"boolean","description":"For Sources that support manual listening, should MS default to scrobbling when no user interaction has occurred?\\n\\nIf not specified MS will use a Source\'s specific behavior, see Source\'s documentation."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"IcecastSourceOptions"},"AzuracastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/AzuracastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["azuracast"]}},"required":["data","type"],"title":"AzuracastSourceAIOConfig"},"AzuracastData":{"type":"object","properties":{"url":{"type":"string","description":"Base URL of the Azuracast instance\\n\\nThis does NOT include the station. If a station is included it will be ignored. Use `station` field to specify station, if necessary","examples":["https://radio.mydomain.tld","http://localhost:80"]},"station":{"type":"string","description":"The specific station to monitor\\n\\nScrobbling will only occur if any of the monitor conditions are met AND the station is ONLINE.\\n\\nTo monitor multiple stations create a Source for each station.","examples":["my-station-1"]},"monitorWhenListeners":{"type":["boolean","number"],"description":"Only activate scrobble monitoring if station\\n\\n* `true` => has any current listeners\\n* `number` => has EQUAL TO or MORE THAN X number of listeners"},"monitorWhenLive":{"type":"boolean","description":"Only activate scrobble monitoring if station has a live DJ/Streamer","default":true},"apiKey":{"type":"string","description":"API Key used to access data about private streams\\n\\nhttps://www.azuracast.com/docs/developers/apis/#api-authentication"}},"required":["url","station"],"title":"AzuracastData"},"KoitoSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/KoitoSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `koito.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["koito"]}},"required":["configureAs","data","type"],"title":"KoitoSourceAIOConfig"},"KoitoSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the Koito server","examples":["http://192.168.0.100:4110"]},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","url","username"],"title":"KoitoSourceData"},"TealSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/TealSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/TealSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"Should always be `souce` when using Tealfm as a Source","default":"source","examples":["source"]},"type":{"type":"string","enum":["tealfm"]}},"required":["configureAs","data","type"],"title":"TealSourceAIOConfig"},"TealSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"baseUri":{"type":"string","description":"The base URI of the Multi-Scrobbler to use for ATProto OAuth\\n\\nOnly include this if you want to use OAuth. The URI must be a non-IP/non-local domain using https: protocol."},"identifier":{"type":"string","description":"Identify the account to login as\\n\\n* For **App Password** Auth - your email\\n* For **Oauth** - your handle minus the @"},"appPassword":{"type":"string","description":"The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account\\n\\nThis is created under https://bsky.app/settings/app-passwords\\n\\n**Use this if you are self-hosting Multi-Scrobbler on localhost or accessed like http://IP:PORT**"},"serviceAllow":{"type":"array","items":{"type":"string"}},"serviceDeny":{"type":"array","items":{"type":"string"}}},"required":["identifier"],"title":"TealSourceData"},"TealSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"TealSourceOptions"},"RockskySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/RockskySourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/RockskySourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `rocksky.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["rocksky"]}},"required":["configureAs","data","type"],"title":"RockskySourceAIOConfig"},"RockskySourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"key":{"type":"string","description":"API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"handle":{"type":"string","description":"The **fully-qualified** handle for your ATPRoto/Bluesky account, like:\\n\\n* alice.bsky.social\\n* foxxmd.com\\n* mysuer.blacksky.app"}},"required":["handle","key"],"title":"RockskySourceData"},"RockskySourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"audioScrobblerUrl":{"type":"string","description":"URL for the Rocksky *Listenbrainz* endpoint, if not using the default","examples":["https://audioscrobbler.rocksky.app"],"default":"https://audioscrobbler.rocksky.app"},"apiUrl":{"type":"string","description":"URL for the Rocksky *API* endpoint, if not using the default","examples":["https://api.rocksky.app"],"default":"https://api.rocksky.app"}},"title":"RockskySourceOptions"},"SonosSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SonosData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/SonosSourceOptions"},"type":{"type":"string","enum":["sonos"]}},"required":["data","type"],"title":"SonosSourceAIOConfig"},"SonosData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"host":{"type":"string","description":"IP address of any connected Sonos speaker or device","examples":["192.168.0.170"]},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device name contains strings from this list (case-insensitive)"},"groupsAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive)"},"groupsBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive)"}},"required":["host"],"title":"SonosData"},"SonosSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"logEmptyPlayer":{"type":"boolean"}},"title":"SonosSourceOptions"},"ClientAIOConfig":{"anyOf":[{"$ref":"#/definitions/MalojaClientAIOConfig"},{"$ref":"#/definitions/LastfmClientAIOConfig"},{"$ref":"#/definitions/LibrefmClientAIOConfig"},{"$ref":"#/definitions/ListenBrainzClientAIOConfig"},{"$ref":"#/definitions/KoitoClientAIOConfig"},{"$ref":"#/definitions/TealClientAIOConfig"},{"$ref":"#/definitions/RockSkyClientAIOConfig"},{"$ref":"#/definitions/DiscordClientAIOConfig"}],"title":"ClientAIOConfig"},"MalojaClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/MalojaClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Maloja as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["maloja"]}},"required":["data","name","type"],"title":"MalojaClientAIOConfig"},"MalojaClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for maloja server","examples":["http://localhost:42010"]},"apiKey":{"type":"string","description":"API Key for Maloja server","examples":["myApiKey"]}},"required":["apiKey","url"],"title":"MalojaClientData"},"CommonClientOptions":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\\n\\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\\n\\nThis setting should only be changed in specific scenarios where MS is handling multiple \\"relaying\\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\\n\\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \\"new\\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":3,"examples":[3]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"CommonClientOptions"},"LastfmClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/LastfmData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/LastfmClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using LastFM as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["lastfm"]}},"required":["data","name","type"],"title":"LastfmClientAIOConfig"},"LastfmData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"API Key generated from Last.fm/Libre.fm account","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"secret":{"type":"string","description":"Secret generated from Last.fm/Libre.fm account","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \\"lastfm/callback\\" in the URL somewhere.","default":"http://localhost:9078/lastfm/callback","examples":["http://localhost:9078/lastfm/callback"]}},"required":["apiKey","secret"],"title":"LastfmData"},"LastfmClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\\n\\n* `true` (default) => Report Now Playing from any eligible Source. \\n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\\n* `false` => Do not report Now Playing\\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\\n\\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\\n\\nThis setting should only be changed in specific scenarios where MS is handling multiple \\"relaying\\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\\n\\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \\"new\\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":3,"examples":[3]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"LastfmClientOptions"},"LibrefmClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/LibrefmData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/LibrefmClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Librefm as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["librefm"]}},"required":["data","name","type"],"title":"LibrefmClientAIOConfig"},"LibrefmClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\\n\\n* `true` (default) => Report Now Playing from any eligible Source. \\n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\\n* `false` => Do not report Now Playing\\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\\n\\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\\n\\nThis setting should only be changed in specific scenarios where MS is handling multiple \\"relaying\\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\\n\\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \\"new\\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":3,"examples":[3]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"LibrefmClientOptions"},"ListenBrainzClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/ListenBrainzClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Listenbrainz as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["listenbrainz"]}},"required":["data","name","type"],"title":"ListenBrainzClientAIOConfig"},"ListenBrainzClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the ListenBrainz server, if not using the default","examples":["https://api.listenbrainz.org/"],"default":"https://api.listenbrainz.org/"},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","username"],"title":"ListenBrainzClientData"},"KoitoClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/KoitoClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Koito as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["koito"]}},"required":["data","name","type"],"title":"KoitoClientAIOConfig"},"KoitoClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the Koito server","examples":["http://192.168.0.100:4110"]},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","url","username"],"title":"KoitoClientData"},"TealClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/TealClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/TealClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Tealfm as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["tealfm"]}},"required":["data","name","type"],"title":"TealClientAIOConfig"},"TealClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"baseUri":{"type":"string","description":"The base URI of the Multi-Scrobbler to use for ATProto OAuth\\n\\nOnly include this if you want to use OAuth. The URI must be a non-IP/non-local domain using https: protocol."},"identifier":{"type":"string","description":"Identify the account to login as\\n\\n* For **App Password** Auth - your email\\n* For **Oauth** - your handle minus the @"},"appPassword":{"type":"string","description":"The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account\\n\\nThis is created under https://bsky.app/settings/app-passwords\\n\\n**Use this if you are self-hosting Multi-Scrobbler on localhost or accessed like http://IP:PORT**"}},"required":["identifier"],"title":"TealClientData"},"TealClientOptions":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\\n\\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\\n\\nThis setting should only be changed in specific scenarios where MS is handling multiple \\"relaying\\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\\n\\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \\"new\\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":3,"examples":[3]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"TealClientOptions"},"RockSkyClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/RockSkyClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/RockSkyClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using RockSky as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["rocksky"]}},"required":["data","name","type"],"title":"RockSkyClientAIOConfig"},"RockSkyClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"key":{"type":"string","description":"API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"handle":{"type":"string","description":"The **fully-qualified** handle for your ATPRoto/Bluesky account, like:\\n\\n* alice.bsky.social\\n* foxxmd.com\\n* mysuer.blacksky.app"}},"required":["handle","key"],"title":"RockSkyClientData"},"RockSkyClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\\n\\n* `true` (default) => Report Now Playing from any eligible Source. \\n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\\n* `false` => Do not report Now Playing\\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\\n\\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\\n\\nThis setting should only be changed in specific scenarios where MS is handling multiple \\"relaying\\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\\n\\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \\"new\\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":3,"examples":[3]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"audioScrobblerUrl":{"type":"string","description":"URL for the Rocksky *Listenbrainz* endpoint, if not using the default","examples":["https://audioscrobbler.rocksky.app"],"default":"https://audioscrobbler.rocksky.app"},"apiUrl":{"type":"string","description":"URL for the Rocksky *API* endpoint, if not using the default","examples":["https://api.rocksky.app"],"default":"https://api.rocksky.app"}},"title":"RockSkyClientOptions"},"DiscordClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/DiscordClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Koito as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["discord"]}},"required":["data","name","type"],"title":"DiscordClientAIOConfig"},"DiscordClientData":{"type":"object","properties":{"token":{"type":"string"},"applicationId":{"type":"string"},"artwork":{"anyOf":[{"type":"boolean"},{"type":"string"},{"type":"array","items":{"type":"string"}}]},"artworkDefaultUrl":{"type":"string"},"statusOverrideAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"$ref":"#/definitions/StatusType"}}]},"listeningActivityAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"ipcLocations":{"anyOf":[{"type":"string"},{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"array","minItems":2,"items":[{"type":"number"},{"type":"string"}],"maxItems":2}]}}]}},"title":"DiscordClientData"},"StatusType":{"type":"string","enum":["online","idle","dnd","invisible"],"title":"StatusType"},"WebhookConfig":{"anyOf":[{"$ref":"#/definitions/GotifyConfig"},{"$ref":"#/definitions/NtfyConfig"},{"$ref":"#/definitions/AppriseConfig"}],"title":"WebhookConfig"},"GotifyConfig":{"type":"object","properties":{"type":{"type":"string","enum":["gotify","ntfy","apprise"],"description":"Webhook type. Valid values are:\\n\\n* gotify\\n* ntfy","examples":["gotify"]},"name":{"type":"string","description":"A friendly name used to identify webhook config in logs"},"url":{"type":"string","description":"The URL of the Gotify server. Same URL that would be used to reach the Gotify UI","examples":["http://192.168.0.100:8078"]},"token":{"type":"string","description":"The token created for this Application in Gotify","examples":["AQZI58fA.rfSZbm"]},"priorities":{"$ref":"#/definitions/PrioritiesConfig","description":"Priority of messages\\n\\n* Info -> 5\\n* Warn -> 7\\n* Error -> 10"}},"required":["token","type","url"],"title":"GotifyConfig"},"PrioritiesConfig":{"type":"object","properties":{"info":{"type":"number","examples":[5]},"warn":{"type":"number","examples":[7]},"error":{"type":"number","examples":[10]}},"required":["info","warn","error"],"title":"PrioritiesConfig"},"NtfyConfig":{"type":"object","properties":{"type":{"type":"string","enum":["gotify","ntfy","apprise"],"description":"Webhook type. Valid values are:\\n\\n* gotify\\n* ntfy","examples":["gotify"]},"name":{"type":"string","description":"A friendly name used to identify webhook config in logs"},"url":{"type":"string","description":"The URL of the Ntfy server","examples":["http://192.168.0.100:8078"]},"topic":{"type":"string","description":"The topic mutli-scrobbler should POST to"},"username":{"type":"string","description":"Required if topic is protected"},"password":{"type":"string","description":"Required if topic is protected"},"token":{"type":"string","description":"Use instead of username/password, required if topic is protected"},"priorities":{"$ref":"#/definitions/PrioritiesConfig","description":"Priority of messages\\n\\n* Info -> 3\\n* Warn -> 4\\n* Error -> 5"}},"required":["topic","type","url"],"title":"NtfyConfig"},"AppriseConfig":{"type":"object","properties":{"type":{"type":"string","enum":["gotify","ntfy","apprise"],"description":"Webhook type. Valid values are:\\n\\n* gotify\\n* ntfy","examples":["gotify"]},"name":{"type":"string","description":"A friendly name used to identify webhook config in logs"},"host":{"type":"string","description":"The URL of the apprise-api server","examples":["http://192.168.0.100:8078"]},"urls":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"If using [Stateless Endpoints](https://github.com/caronc/apprise-api?tab=readme-ov-file#stateless-solution) the Apprise config URL(s) to send"},"keys":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"If using [Persistent Store Endpoints](https://github.com/caronc/apprise-api?tab=readme-ov-file#persistent-storage-solution) the Configuration ID(s) to send to\\n\\nNote: If multiple keys are defined then MS will attempt to POST to each one individually"},"tags":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Optional [tag(s)](https://github.com/caronc/apprise-api?tab=readme-ov-file#tagging) to send in the notification payload"}},"required":["host","type"],"title":"AppriseConfig"},"LogOptions":{"type":"object","properties":{"level":{"$ref":"#/definitions/LogLevel","description":"Specify the minimum log level for all log outputs without their own level specified.\\n\\nDefaults to env `LOG_LEVEL` or `info` if not specified.","default":"info"},"file":{"anyOf":[{"$ref":"#/definitions/LogLevel"},{"type":"boolean","const":false},{"$ref":"#/definitions/FileLogOptions"}],"description":"Specify the minimum log level to output to rotating files or file output options. If `false` no log files will be created."},"console":{"$ref":"#/definitions/LogLevel","description":"Specify the minimum log level streamed to the console (or docker container)"}},"description":"Configure log levels and file options for an AppLogger.\\n\\n```ts\\nconst infoLogger = loggerApp({\\n level: \'info\' // console and file will log any levels `info` and above\\n});\\n\\nconst logger = loggerApp({\\n console: \'debug\', // console will log `debug` and higher\\n file: \'warn\' // file will log `warn` and higher\\n});\\n\\nconst fileLogger = loggerRollingApp({\\n console: \'debug\', // console will log `debug` and higher\\n file: {\\n level: \'warn\', // file will log `warn` and higher\\n path: \'/my/cool/path/output.log\', // optionally, output to log file at this path\\n frequency: \'hourly\', // optionally, rotate hourly\\n }\\n});\\n```","title":"LogOptions"},"CacheConfigOptions":{"type":"object","properties":{"metadata":{"$ref":"#/definitions/CacheMetadataConfig"},"scrobble":{"$ref":"#/definitions/CacheScrobbleConfig"},"auth":{"$ref":"#/definitions/CacheAuthConfig"},"regex":{"type":"number","description":"Number of regex functions to cache (LRU)","default":200}},"title":"CacheConfigOptions"},"CacheMetadataConfig":{"$ref":"#/definitions/CacheConfigType","title":"CacheMetadataConfig"},"CacheConfigType":{"anyOf":[{"$ref":"#/definitions/CacheMemoryConfig"},{"$ref":"#/definitions/CacheValkeyConfig"},{"$ref":"#/definitions/CacheFileConfig"},{"$ref":"#/definitions/CacheNoopConfig"}],"title":"CacheConfigType"},"CacheMemoryConfig":{"type":"object","properties":{"provider":{"type":"string","enum":["memory"]},"connection":{"type":"string"},"lruSize":{"type":"number"},"ttl":{"type":"number"}},"required":["provider"],"title":"CacheMemoryConfig"},"CacheValkeyConfig":{"type":"object","properties":{"provider":{"type":"string","enum":["valkey"]},"connection":{"type":"string"}},"required":["provider"],"title":"CacheValkeyConfig"},"CacheFileConfig":{"type":"object","properties":{"provider":{"type":"string","enum":["file"]},"connection":{"type":"string"}},"required":["provider"],"title":"CacheFileConfig"},"CacheNoopConfig":{"type":"object","properties":{"provider":{"type":"boolean","const":false},"connection":{"type":"string"}},"required":["provider"],"title":"CacheNoopConfig"},"CacheScrobbleConfig":{"$ref":"#/definitions/CacheConfigType","title":"CacheScrobbleConfig"},"CacheAuthConfig":{"$ref":"#/definitions/CacheConfigType","title":"CacheAuthConfig"},"TransformerCommonConfig":{"type":"object","properties":{"defaults":{"type":"object"},"data":{"type":"object"},"type":{"type":"string"},"name":{"type":"string"},"options":{"$ref":"#/definitions/TransformOptions"}},"required":["type"],"title":"TransformerCommonConfig"},"TransformOptions":{"type":"object","properties":{"failOnFetch":{"type":"boolean"},"throwOnFailure":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string","enum":["artists","title","albumArtists","album","duration","meta","art"]}}]},"ttl":{"type":"string"}},"title":"TransformOptions"}}}'),p=JSON.parse('{"sourceDefaults":{"maxPollRetries":0,"maxRequestRetries":1,"retryMultiplier":1.5},"clientDefaults":{"maxRequestRetries":1,"retryMultiplier":1.5},"baseUrl":"http://localhost","sources":[{"type":"spotify","clients":["myConfig"],"name":"mySpotifySource","data":{"clientId":"a89cba1569901a0671d5a9875fed4be1","clientSecret":"ec42e09d5ae0ee0f0816ca151008412a","redirectUri":"http://localhost:9078/callback"}}],"clients":[{"type":"maloja","name":"myConfig","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}}]}'),d=i(8258),u=i(4020),f=i(8182),h=i.n(f),y=i(4254),m=i(4848);let b=(0,u.IM)({schema:c,ajvOptions:{strict:"log",strictSchema:"log",allowUnionTypes:!0},onCreateAjv:e=>(e.addKeyword("deprecationMessage"),h().default(e),e)});function g(){let{colorMode:e}=(0,r.G)(),t=(0,o.l)({desktopBreakpoint:500}),[i,n]=(0,s.useState)({json:p});return(0,m.jsxs)("div",{className:"schemaWrapper2",children:[(0,m.jsxs)("div",{style:{padding:"0 1em 0 1em",flexBasis:"min-content",flexGrow:2},children:[(0,m.jsxs)(y.A,{type:"note",summary:"Config Structure",children:[(0,m.jsxs)("p",{children:["This displays the ",(0,m.jsx)("strong",{children:"structure"})," of the ",(0,m.jsxs)("a",{href:"/configuration?configType=aio#configuration-types",children:["all-in-one (",(0,m.jsx)("code",{children:"config.json"}),") configuration"]})," with all possible properties, their shape, and descriptions/types."]}),(0,m.jsx)("p",{children:"Use this to understand how to write a valid config."})]}),(0,m.jsx)(d.Es,{name:"Todos Model",schema:c,expanded:!1,hideTopBar:!1,renderRootTreeLines:!0,emptyText:"No schema defined",defaultExpandedDepth:0,markup:!0})]}),(0,m.jsxs)("div",{style:{flexBasis:"auto",flexGrow:3},children:[(0,m.jsx)("div",{className:"fileEditor2",children:(0,m.jsxs)(y.A,{type:"note",summary:"Config Example",children:[(0,m.jsxs)("p",{children:["This displays an ",(0,m.jsx)("strong",{children:"example config file"})," of a ",(0,m.jsxs)("a",{href:"/configuration?configType=aio#configuration-types",children:["all-in-one (",(0,m.jsx)("code",{children:"config.json"}),") configuration"]})," that adheres to the shown ",(0,m.jsx)("strong",{children:"Config Structure."})]}),(0,m.jsxs)("ul",{children:[(0,m.jsxs)("li",{children:[(0,m.jsx)("strong",{children:"text"})," mode lets you edit the JSON directly."]}),(0,m.jsxs)("li",{children:[(0,m.jsx)("strong",{children:"tree"})," mode gives you a guided point-and-click editing experience that always keeps the JSON ",(0,m.jsx)("i",{children:"syntax"})," valid."]})]}),(0,m.jsxs)("p",{children:["Both modes ",(0,m.jsx)("strong",{children:"validate"})," that the configuraion is correct. Any errors show up as squiggly lines."]}),(0,m.jsxs)("p",{children:["After you finish editing, switch to ",(0,m.jsx)("strong",{children:"text"})," and then copy all text to get a completed config."]})]})}),(0,m.jsx)("div",{className:"fileEditorMobile2",children:(0,m.jsxs)(y.A,{type:"warning",summary:"Config Example Mobile Experience",children:[(0,m.jsxs)("p",{children:["This displays an ",(0,m.jsx)("strong",{children:"example config file"})," of a ",(0,m.jsxs)("a",{href:"/configuration?configType=aio#configuration-types",children:["all-in-one (",(0,m.jsx)("code",{children:"config.json"}),") configuration"]})," that adheres to the shown ",(0,m.jsx)("strong",{children:"Config Structure."})]}),(0,m.jsxs)("p",{children:["The example config file editor is meant for a larger screen experience so ",(0,m.jsx)("strong",{children:"only the read-only example is shown."})," Please use this site on a tablet/laptop/desktop to enable file editor features."]})]})}),"mobile"===t?(0,m.jsx)(l(),{title:"CONFIG/config.json",language:"json5",children:JSON.stringify(i,null,2)}):(0,m.jsx)(u.t$,{validator:b,theme:e,content:i,mode:"text",style:{height:"initial"}})]})]})}function S(){return(0,m.jsx)(n.default,{fallback:(0,m.jsx)("div",{children:"Loading..."}),children:()=>(0,m.jsx)(g,{})})}},1891(e,t,i){i.r(t),i.d(t,{default:()=>l}),i(6540);var s=i(195),n=i(8478),r=i(349),o=i(4848);function a(){return(0,o.jsx)(r.A,{fallback:({error:e,tryAgain:t})=>(0,o.jsxs)("div",{children:[(0,o.jsxs)("p",{children:["Playground component crashed because of error: ",e.message,"."]}),(0,o.jsx)("button",{onClick:t,children:"Try Again!"})]}),children:(0,o.jsx)(n.default,{fallback:(0,o.jsx)("div",{children:"Loading..."}),children:()=>{let e=i(5543).A;return(0,o.jsx)(e,{})}})})}function l(){return(0,o.jsx)(s.A,{title:"Playground",description:"Config Playground",children:(0,o.jsx)(a,{})})}}}]); \ No newline at end of file +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["8406"],{3936(e,t,i){i.d(t,{A:()=>d});var s=i(6540),n=i(4164),r=i(7988),o=i(2303),a=i(1422),l=i(4848);function c(e){return!!e&&("SUMMARY"===e.tagName||c(e.parentElement))}function p({summary:e,children:t,...i}){(0,r.A)().collectAnchor(i.id);let d=(0,o.A)(),u=(0,s.useRef)(null),{collapsed:f,setCollapsed:h}=(0,a.u)({initialState:!i.open}),[y,m]=(0,s.useState)(i.open),b=s.isValidElement(e)?e:(0,l.jsx)("summary",{children:e??"Details"});return(0,l.jsxs)("details",{...i,ref:u,open:y,"data-collapsed":f,className:(0,n.A)("details_lb9f",d&&"isBrowser_bmU9",i.className),onMouseDown:e=>{c(e.target)&&e.detail>1&&e.preventDefault()},onClick:e=>{e.stopPropagation();let t=e.target;c(t)&&function e(t,i){return!!t&&(t===i||e(t.parentElement,i))}(t,u.current)&&(e.preventDefault(),f?(h(!1),m(!0)):h(!0))},children:[b,(0,l.jsx)(a.N,{lazy:!1,collapsed:f,onCollapseTransitionEnd:e=>{h(e),m(!e)},children:(0,l.jsx)("div",{className:"collapsibleContent_i85q",children:t})})]})}function d({...e}){return(0,l.jsx)(p,{...e,className:(0,n.A)("alert alert--info","details_b_Ee",e.className)})}},4254(e,t,i){i.d(t,{A:()=>c});var s=i(3936),n=i(804),r=i(633),o=i(3069),a=i(3550),l=i(4848);let c=e=>{let t,i="";switch(e.type){case"warning":i="alert--warning",t=(0,l.jsx)(n.A,{});break;case"danger":i="alert--danger",t=(0,l.jsx)(r.A,{});break;case"note":i="alert--secondary",t=(0,l.jsx)(a.A,{});break;case"tip":i="alert--success",t=(0,l.jsx)(o.A,{})}let c=void 0===t?null:(0,l.jsx)("span",{className:"admoIcon",children:t}),p=(0,l.jsxs)("summary",{children:[c,e.summary]});return(0,l.jsx)(s.A,{className:i,summary:p,children:e.children})}},5543(e,t,i){i.d(t,{A:()=>S});var s=i(6540),n=i(8478),r=i(5293),o=i(4581),a=i(2170),l=i.n(a),c=JSON.parse('{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"sourceDefaults":{"$ref":"#/definitions/SourceDefaults"},"clientDefaults":{"$ref":"#/definitions/ClientDefaults"},"sources":{"type":"array","items":{"$ref":"#/definitions/SourceAIOConfig"}},"clients":{"type":"array","items":{"$ref":"#/definitions/ClientAIOConfig"}},"webhooks":{"type":"array","items":{"$ref":"#/definitions/WebhookConfig"}},"port":{"type":"number","description":"Set the port the multi-scrobbler UI will be served from","default":9078,"examples":[9078]},"baseUrl":{"type":"string","description":"Set the Base URL the application should assume the UI is served from.\\n\\nThis will affect how default redirect URLs are generated (spotify, lastfm, deezer) and some logging messages.\\n\\nIt will NOT set the actual interface/IP that the application is listening on.\\n\\nThis can also be set using the BASE_URL environmental variable.","default":"http://localhost","examples":["http://localhost","http://192.168.0.101","https://ms.myDomain.tld"]},"logging":{"$ref":"#/definitions/LogOptions"},"disableWeb":{"type":"boolean","description":"Disable web server from running/listening on port.\\n\\nThis will also make any ingress sources (Plex, Jellyfin, Tautulli, etc...) unusable"},"debugMode":{"type":"boolean","description":"Enables ALL relevant logging and debug options for all sources/clients, when none are defined.\\n\\nThis is a convenience shortcut for enabling all output needed to troubleshoot an issue and does not need to be on for normal operation.\\n\\nIt can also be enabled with the environmental variable DEBUG_MODE=true","default":false,"examples":[false]},"cache":{"$ref":"#/definitions/CacheConfigOptions"},"transformers":{"type":"array","items":{"$ref":"#/definitions/TransformerCommonConfig"}}},"definitions":{"SourceDefaults":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"SourceDefaults"},"LogLevel":{"type":"string","enum":["silent","fatal","error","warn","info","log","verbose","debug","trace"],"description":"Names of log levels that can be invoked on the logger\\n\\nFrom lowest to highest:\\n\\n* `trace`\\n* `debug`\\n* `verbose`\\n* `log`\\n* `info`\\n* `warn`\\n* `error`\\n* `fatal`\\n* `silent` (will never output anything)\\n\\nWhen used in `LogOptions` specifies the **minimum** level the output should log at.","title":"LogLevel"},"FileLogOptions":{"type":"object","properties":{"timestamp":{"type":"string","enum":["unix","iso","auto"],"description":"For rolling log files\\n\\nWhen\\n* value passed to rolling destination is a string (`path` from LogOptions is a string) and\\n* `frequency` is defined\\n\\nThis determines the format of the datetime inserted into the log file name:\\n\\n* `unix` - unix epoch timestamp in milliseconds\\n* `iso` - Full [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) datetime IE \'2024-03-07T20:11:34Z\'\\n* `auto`\\n * When frequency is `daily` only inserts date IE YYYY-MM-DD\\n * Otherwise inserts full ISO8601 datetime","default":"auto"},"size":{"type":["number","string"],"description":"The maximum size of a given rolling log file.\\n\\nCan be combined with frequency. Use k, m and g to express values in KB, MB or GB.\\n\\nNumerical values will be considered as MB.","default":"10MB"},"frequency":{"anyOf":[{"type":"string","enum":["daily"]},{"type":"string","enum":["hourly"]},{"type":"number"}],"description":"The amount of time a given rolling log file is used. Can be combined with size.\\n\\nUse `daily` or `hourly` to rotate file every day (or every hour). Existing file within the current day (or hour) will be re-used.\\n\\nNumerical values will be considered as a number of milliseconds. Using a numerical value will always create a new file upon startup.","default":"daily"},"path":{"anyOf":[{"type":"string"},{"$comment":"() => string"}],"description":"The path and filename to use for log files.\\n\\nIf using rolling files the filename will be appended with `.N` (a number) BEFORE the extension based on rolling status.\\n\\nMay also be specified using env LOG_PATH or a function that returns a string.\\n\\nIf path is relative the absolute path will be derived from `logBaseDir` (in `LoggerAppExtras`) which defaults to CWD","default":"./logs/app.log"},"level":{"anyOf":[{"$ref":"#/definitions/LogLevel"},{"type":"boolean","const":false}],"description":"Specify the minimum log level to output to rotating files. If `false` no log files will be created."}},"title":"FileLogOptions"},"ScrobbleThresholds":{"type":"object","properties":{"duration":{"type":["number","null"],"description":"The number of seconds a track has been listened to before it should be considered scrobbled.\\n\\nSet to null to disable.","default":240,"examples":[240]},"percent":{"type":["number","null"],"description":"The percentage (as an integer) of a track that should have been seen played before it should be scrobbled. Only used if the Source provides information about how long the track is.\\n\\nSet to null to disable.\\n\\nNOTE: This should be used with care when the Source is a \\"polling\\" type (has an \'interval\' property). If the track is short and the interval is too high MS may ignore the track if percentage is high because it had not \\"seen\\" the track for long enough from first discovery, even if you have been playing the track for longer.","default":50,"examples":[50]}},"title":"ScrobbleThresholds"},"PlayTransformOptions":{"type":"object","properties":{"log":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["all"]}]},"preCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-5069%5B%5D%7CExternalMetadataTerm)%3E"},"compare":{"type":"object","properties":{"candidate":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-5069%5B%5D%7CExternalMetadataTerm)%3E"},"existing":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-5069%5B%5D%7CExternalMetadataTerm)%3E"}}},"postCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-5069%5B%5D%7CExternalMetadataTerm)%3E"}},"title":"PlayTransformOptions"},"PlayTransformPartsConfig<(def-alias-617285649-262-339-617285649-0-5069[]|ExternalMetadataTerm)>":{"anyOf":[{"$ref":"#/definitions/PlayTransformPartsArray%3C(def-alias-617285649-262-339-617285649-0-5069%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"},{"$ref":"#/definitions/PlayTransformParts%3C(def-alias-617285649-262-339-617285649-0-5069%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"}],"description":"Represents the weakly-defined user config. May be an array of parts or one parts object","title":"PlayTransformPartsConfig<(def-alias-617285649-262-339-617285649-0-5069[]|ExternalMetadataTerm)>"},"PlayTransformPartsArray<(def-alias-617285649-262-339-617285649-0-5069[]|ExternalMetadataTerm),MaybeStageTyped>":{"type":"array","items":{"$ref":"#/definitions/PlayTransformParts%3C(def-alias-617285649-262-339-617285649-0-5069%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"},"title":"PlayTransformPartsArray<(def-alias-617285649-262-339-617285649-0-5069[]|ExternalMetadataTerm),MaybeStageTyped>"},"PlayTransformParts<(def-alias-617285649-262-339-617285649-0-5069[]|ExternalMetadataTerm),MaybeStageTyped>":{"anyOf":[{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"artists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"albumArtists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"album":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"duration":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"meta":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"$ref":"#/definitions/StageTypeMetadata"},"name":{"type":"string"},"stageHash":{"type":"string"},"score":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"artists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"albumArtists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"album":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"duration":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"meta":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"$ref":"#/definitions/StageTypeUser"},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"artists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"albumArtists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"album":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"duration":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"meta":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"type":"string","enum":["native"]},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"artists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"albumArtists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"album":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"duration":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"meta":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"name":{"type":"string"}}},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{},"artists":{},"albumArtists":{},"album":{},"duration":{},"meta":{},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"type":"string"},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]}],"title":"PlayTransformParts<(def-alias-617285649-262-339-617285649-0-5069[]|ExternalMetadataTerm),MaybeStageTyped>"},"WhenConditionsConfig":{"$ref":"#/definitions/WhenConditions%3Cstring%3E","title":"WhenConditionsConfig"},"WhenConditions":{"type":"array","items":{"$ref":"#/definitions/WhenParts%3Cstring%3E"},"title":"WhenConditions"},"WhenParts":{"$ref":"#/definitions/PlayTransformPartsAtomic%3Cstring%3E","title":"WhenParts"},"PlayTransformPartsAtomic":{"type":"object","properties":{"title":{"type":"string"},"artists":{"type":"string"},"albumArtists":{"type":"string"},"album":{"type":"string"},"duration":{"type":"string"},"meta":{"type":"string"}},"title":"PlayTransformPartsAtomic"},"Whennable":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"}},"title":"Whennable"},"FlowControlTerm":{"type":"string","enum":["continue","stop"],"title":"FlowControlTerm"},"StageTypeMetadata":{"type":"string","enum":["spotify","musicbrainz","native"],"title":"StageTypeMetadata"},"SearchAndReplaceTerm":{"anyOf":[{"type":"string"},{"$ref":"#/definitions/ConditionalSearchAndReplaceTerm"}],"title":"SearchAndReplaceTerm"},"ConditionalSearchAndReplaceTerm":{"$ref":"#/definitions/ConditionalSearchAndReplaceRegExp","title":"ConditionalSearchAndReplaceTerm"},"ConditionalSearchAndReplaceRegExp":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"search":{"anyOf":[{"type":"string"},{"type":"string","format":"regex"}],"description":"The search value to test for\\n\\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object","examples":[["find this string","/some string*/ig"]]},"replace":{"type":"string","description":"The replacement string/value to use when search is found\\n\\nThis can be a literal string like `\'replace with this`, an empty string to remove the search value (`\'\'`), or a special regex value\\n\\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace"},"test":{"$comment":"(obj: SearchAndReplaceRegExp) => boolean","type":"object","properties":{"namedArgs":{"type":"object","properties":{"obj":{"$ref":"#/definitions/SearchAndReplaceRegExp"}},"required":["obj"],"additionalProperties":false}},"description":"An optional function to execute before running this search-and-replace that decides if it should run at all"}},"required":["replace","search"],"title":"ConditionalSearchAndReplaceRegExp"},"SearchAndReplaceRegExp":{"type":"object","properties":{"search":{"anyOf":[{"type":"string"},{"type":"string","format":"regex"}],"description":"The search value to test for\\n\\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object","examples":[["find this string","/some string*/ig"]]},"replace":{"type":"string","description":"The replacement string/value to use when search is found\\n\\nThis can be a literal string like `\'replace with this`, an empty string to remove the search value (`\'\'`), or a special regex value\\n\\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace"},"test":{"$comment":"(obj: SearchAndReplaceRegExp) => boolean","type":"object","properties":{"namedArgs":{"type":"object","properties":{"obj":{"$ref":"#/definitions/SearchAndReplaceRegExp"}},"required":["obj"],"additionalProperties":false}},"description":"An optional function to execute before running this search-and-replace that decides if it should run at all"}},"required":["search","replace"],"title":"SearchAndReplaceRegExp"},"ExternalMetadataTerm":{"anyOf":[{"type":"boolean"},{"not":{}},{"$ref":"#/definitions/Whennable"}],"title":"ExternalMetadataTerm"},"StageTypeUser":{"type":"string","enum":["user"],"title":"StageTypeUser"},"ClientDefaults":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\\n\\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\\n\\nThis setting should only be changed in specific scenarios where MS is handling multiple \\"relaying\\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\\n\\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \\"new\\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":3,"examples":[3]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"ClientDefaults"},"MatchLoggingOptions":{"type":"object","properties":{"onNoMatch":{"type":"boolean","description":"Log to DEBUG when a new track does NOT match an existing scrobble","default":false,"examples":[false]},"onMatch":{"type":"boolean","description":"Log to DEBUG when a new track DOES match an existing scrobble","default":false,"examples":[false]},"confidenceBreakdown":{"type":"boolean","description":"Include confidence breakdowns in track match logging, if applicable","default":false,"examples":[false]}},"description":"Scrobble matching (between new source track and existing client scrobbles) logging options. Used for debugging.","title":"MatchLoggingOptions"},"SourceAIOConfig":{"anyOf":[{"$ref":"#/definitions/SpotifySourceAIOConfig"},{"$ref":"#/definitions/PlexApiSourceAIOConfig"},{"$ref":"#/definitions/DeezerAIOCompatConfig"},{"$ref":"#/definitions/ListenbrainzEndpointSourceAIOConfig"},{"$ref":"#/definitions/LastFMEndpointSourceAIOConfig"},{"$ref":"#/definitions/SubsonicSourceAIOConfig"},{"$ref":"#/definitions/JellyApiSourceAIOConfig"},{"$ref":"#/definitions/LastFmSouceAIOConfig"},{"$ref":"#/definitions/LibrefmSouceAIOConfig"},{"$ref":"#/definitions/YTMusicSourceAIOConfig"},{"$ref":"#/definitions/YandexMusicBridgeSourceAIOConfig"},{"$ref":"#/definitions/MPRISSourceAIOConfig"},{"$ref":"#/definitions/MopidySourceAIOConfig"},{"$ref":"#/definitions/ListenBrainzSourceAIOConfig"},{"$ref":"#/definitions/JRiverSourceAIOConfig"},{"$ref":"#/definitions/KodiSourceAIOConfig"},{"$ref":"#/definitions/WebScrobblerSourceAIOConfig"},{"$ref":"#/definitions/ChromecastSourceAIOConfig"},{"$ref":"#/definitions/MalojaSourceAIOConfig"},{"$ref":"#/definitions/MusikcubeSourceAIOConfig"},{"$ref":"#/definitions/MusicCastSourceAIOConfig"},{"$ref":"#/definitions/MPDSourceAIOConfig"},{"$ref":"#/definitions/VLCSourceAIOConfig"},{"$ref":"#/definitions/IcecastSourceAIOConfig"},{"$ref":"#/definitions/AzuracastSourceAIOConfig"},{"$ref":"#/definitions/KoitoSourceAIOConfig"},{"$ref":"#/definitions/TealSourceAIOConfig"},{"$ref":"#/definitions/RockskySourceAIOConfig"},{"$ref":"#/definitions/SonosSourceAIOConfig"}],"title":"SourceAIOConfig"},"SpotifySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SpotifySourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["spotify"]}},"required":["data","type"],"title":"SpotifySourceAIOConfig"},"SpotifySourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)\\n\\nIt is unlikely you should need to change this unless you scrobble many very short tracks often\\n\\nReading:\\n* https://developer.spotify.com/documentation/web-api/guides/rate-limits/\\n* https://medium.com/mendix/limiting-your-amount-of-calls-in-mendix-most-of-the-time-rest-835dde55b10e\\n * Rate limit may ~180 req/min\\n* https://community.spotify.com/t5/Spotify-for-Developers/Web-API-ratelimit/m-p/5503150/highlight/true#M7930\\n * Informally indicated as 20 req/sec? Probably for burstiness","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"clientId":{"type":"string","description":"spotify client id","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"clientSecret":{"type":"string","description":"spotify client secret","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"redirectUri":{"type":"string","description":"spotify redirect URI -- required only if not the default shown here. URI must end in \\"callback\\"","default":"http://localhost:9078/callback","examples":["http://localhost:9078/callback"]}},"required":["clientId","clientSecret"],"title":"SpotifySourceData"},"CommonSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"CommonSourceOptions"},"PlexApiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/PlexApiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/PlexApiOptions"},"type":{"type":"string","enum":["plex"]}},"required":["data","type"],"title":"PlexApiSourceAIOConfig"},"PlexApiData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"token":{"type":"string"},"url":{"type":"string","description":"http(s)://HOST:PORT of the Plex server to connect to"},"usersAllow":{"anyOf":[{"type":"string"},{"type":"boolean","enum":[true]},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\\n\\nIf `true` MS will scrobble activity from all users"},"usersBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble for these users (case-insensitive)"},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device or application name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device or application name contains strings from this list (case-insensitive)"},"librariesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if library name contains string from this list (case-insensitive)"},"librariesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if library name contains strings from this list (case-insensitive)"}},"required":["url"],"title":"PlexApiData"},"PlexApiOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"ignoreInvalidCert":{"type":"boolean","description":"Ignore invalid cert errors when connecting to Plex\\n\\nUseful for Plex servers using \\"Required\\" Secure Connections with self-signed certificates\\n\\nDo not enable unless you know you need this.","default":false}},"title":"PlexApiOptions"},"DeezerAIOCompatConfig":{"anyOf":[{"$ref":"#/definitions/DeezerSourceAIOConfig"},{"$ref":"#/definitions/DeezerInternalAIOConfig"}],"title":"DeezerAIOCompatConfig"},"DeezerSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/DeezerData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["deezer"]}},"required":["data","type"],"title":"DeezerSourceAIOConfig"},"DeezerData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"clientId":{"type":"string","description":"deezer client id","examples":["a89cba1569901a0671d5a9875fed4be1"]},"clientSecret":{"type":"string","description":"deezer client secret","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"redirectUri":{"type":"string","description":"deezer redirect URI -- required only if not the default shown here. URI must end in \\"callback\\"","default":"http://localhost:9078/deezer/callback","examples":["http://localhost:9078/deezer/callback"]},"accessToken":{"type":"string"}},"required":["clientId","clientSecret"],"title":"DeezerData"},"DeezerInternalAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/DeezerInternalData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/DeezerInternalSourceOptions"},"type":{"type":"string","enum":["deezer"]}},"required":["data","type"],"title":"DeezerInternalAIOConfig"},"DeezerInternalData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"arl":{"type":"string","description":"ARL retrieved from Deezer response header"},"userAgent":{"type":"string","description":"User agent","default":"Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0"},"accountId":{"type":"string","description":"The ID (USER_ID) of the linked account to monitor. If not set, monitors the main ARL account"}},"required":["arl"],"title":"DeezerInternalData"},"DeezerInternalSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"fuzzyDiscoveryIgnore":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["aggressive"]}]}},"title":"DeezerInternalSourceOptions"},"ListenbrainzEndpointSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ListenbrainzEndpointData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["endpointlz"]}},"required":["type"],"title":"ListenbrainzEndpointSourceAIOConfig"},"ListenbrainzEndpointData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\\n\\nIf you are using multiple Listenbrainz endpoint sources (scrobbles for many users) you can use a slug to match Sources with individual users/origins\\n\\nExample:\\n\\n* slug: \'usera\' => API URL: http://localhost:9078/api/listenbrainz/usera\\n* slug: \'originb\' => API URL: http://localhost:9078/api/listenbrainz/originb\\n\\nIf no slug is found from an extension\'s incoming webhook event the first Listenbrainz source without a slug will be used"},"token":{"type":["string","null"],"description":"If an LZ submission request contains this token in the Authorization Header it will be used to match the submission with this Source\\n\\nSee: https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#add-the-user-token-to-your-requests"}},"title":"ListenbrainzEndpointData"},"LastFMEndpointSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LastFMEndpointData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["endpointlfm"]}},"required":["type"],"title":"LastFMEndpointSourceAIOConfig"},"LastFMEndpointData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\\n\\nIf you are using multiple Last.fm endpoint sources (scrobbles for many users) you can use a slug to match Sources with individual users/origins\\n\\nExample:\\n\\n* slug: \'usera\' => API URL: http://localhost:9078/api/lastfm/usera\\n* slug: \'originb\' => API URL: http://localhost:9078/api/lastfm/originb\\n\\nIf no slug is found from an extension\'s incoming webhook event the first Last.fm source without a slug will be used"}},"title":"LastFMEndpointData"},"SubsonicSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SubsonicData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["subsonic"]}},"required":["data","type"],"title":"SubsonicSourceAIOConfig"},"SubsonicData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the subsonic media server to query","examples":["http://airsonic.local"]},"user":{"type":"string","description":"Username to login to the server with","examples":[["MyUser"]]},"password":{"type":"string","description":"Password for the user to login to the server with","examples":["MyPassword"]},"ignoreTlsErrors":{"type":"boolean","description":"If your subsonic server is using self-signed certs you may need to disable TLS errors in order to get a connection\\n\\nWARNING: This should be used with caution as your traffic may not be encrypted.","default":false},"legacyAuthentication":{"type":"boolean","description":"Older Subsonic versions, and some badly implemented servers (Nextcloud), use legacy authentication which sends your password in CLEAR TEXT. This is less secure than the newer, recommended hashing authentication method but in some cases it is needed. See \\"Authentication\\" section here => https://www.subsonic.org/pages/api.jsp\\n\\nIf this option is not specified it will be turned on if the subsonic server responds with error code 41 \\"Token authentication not supported for LDAP users.\\" -- See Error Handling section => https://www.subsonic.org/pages/api.jsp","default":false},"usersAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\\n\\nIf undefined or an empty string/list MS will scrobble activity from all users"}},"required":["url","user","password"],"title":"SubsonicData"},"JellyApiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JellyApiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/JellyApiOptions"},"type":{"type":"string","enum":["jellyfin"]}},"required":["data","type"],"title":"JellyApiSourceAIOConfig"},"JellyApiData":{"type":"object","properties":{"url":{"type":"string","description":"HOST:PORT of the Jellyfin server to connect to"},"user":{"type":"string","description":"The username of the user to authenticate for or track scrobbles for"},"password":{"type":"string","description":"Password of the username to authenticate for\\n\\nRequired if `apiKey` is not provided."},"apiKey":{"type":"string","description":"API Key to authenticate with.\\n\\nRequired if `password` is not provided."},"usersAllow":{"anyOf":[{"type":"string"},{"type":"boolean","enum":[true]},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\\n\\nIf `true` MS will scrobble activity from all users"},"usersBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble for these users (case-insensitive)"},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device or application name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device or application name contains strings from this list (case-insensitive)"},"librariesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if library name contains string from this list (case-insensitive)"},"librariesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if library name contains strings from this list (case-insensitive)"},"additionalAllowedLibraryTypes":{"type":"array","items":{},"description":"Allow MS to scrobble audio media in libraries classified other than \'music\'\\n\\n`librariesAllow` will achieve the same result as this but this is more convenient if you do not want to explicitly list every library name or are only using `librariesBlock`"},"allowUnknown":{"type":"boolean","description":"Force media with a type of \\"Unknown\\" to be counted as Audio","default":false,"deprecated":"use allowMediaTypes instead"},"allowMediaTypes":{"anyOf":[{"type":"array","items":{}},{"type":"string"}],"description":"Allow these media types to be scrobbled.\\n\\nIf not defined or empty, uses \'Audio\' as default. If non-empty then *only* uses these types (make sure you include Audio!)\\n\\nValues are case-insensitive.\\n\\nSee https://github.com/jellyfin/jellyfin-sdk-typescript/blob/master/src/generated-client/models/media-type.ts#L22 for possible types"},"frontendUrlOverride":{"type":"string","description":"HOST:PORT of the Jellyfin server that your browser will be able to access from the frontend (and thus load images and links from)\\nIf unspecified it will use the normal server HOST and PORT from the `url`\\nNecessary if you are using a reverse proxy or other network configuration that prevents the frontend from accessing the server directly\\n\\nENV: JELLYFIN_FRONTEND_URL_OVERRIDE"}},"required":["url","user"],"title":"JellyApiData"},"JellyApiOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"JellyApiOptions"},"LastFmSouceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LastFmSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `lastfm.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["lastfm"]}},"required":["configureAs","data","type"],"title":"LastFmSouceAIOConfig"},"LastFmSourceData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"API Key generated from Last.fm/Libre.fm account","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"secret":{"type":"string","description":"Secret generated from Last.fm/Libre.fm account","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \\"lastfm/callback\\" in the URL somewhere.","default":"http://localhost:9078/lastfm/callback","examples":["http://localhost:9078/lastfm/callback"]},"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"}},"required":["apiKey","secret"],"title":"LastFmSourceData"},"LibrefmSouceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LibrefmData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `librefm.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["librefm"]}},"required":["data","type"],"title":"LibrefmSouceAIOConfig"},"LibrefmData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"Optional Secret for Libre.fm account\\n\\nThis can be anything, or it will be generated by multi-scrobbler","examples":["anyValue"]},"secret":{"type":"string","description":"Optional Secret for Libre.fm account\\n\\nThis can be anything, or it will be generated by multi-scrobbler","examples":["anyValue"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"path":{"type":"string"},"urlBase":{"type":"string","description":"(Optional) The host and path prefix for your Libre.fm instance","default":"https://libre.fm/2.0/"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \\"librefm/callback\\" in the URL somewhere.","default":"http://localhost:9078/librefm/callback","examples":["http://localhost:9078/librefm/callback"]}},"title":"LibrefmData"},"YTMusicSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/YTMusicData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"type":"object","properties":{"logAuth":{"type":"boolean","description":"When true MS will log to DEBUG all of the credentials data it receives from YTM"},"logDiff":{"type":"boolean","description":"Always log history diff\\n\\nBy default MS will log to `WARN` if history diff is inconsistent but does not log if diff is expected (on new tracks found)\\nSet this to `true` to ALWAYS log diff on new tracks. Expected diffs will log to `DEBUG` and inconsistent diffs will continue to log to `WARN`","default":false},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}}},"type":{"type":"string","enum":["ytmusic"]}},"required":["type"],"title":"YTMusicSourceAIOConfig"},"YTMusicData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"cookie":{"type":"string","description":"The cookie retrieved from the Request Headers of music.youtube.com after logging in.\\n\\nSee https://ytmusicapi.readthedocs.io/en/stable/setup/browser.html#copy-authentication-headers for how to retrieve this value.","examples":["VISITOR_INFO1_LIVE=jMp2xA1Xz2_PbVc; __Secure-3PAPISID=3AxsXpy0M/AkISpjek; ..."]},"clientId":{"type":"string","description":"Google Cloud Console project OAuth Client ID\\n\\nGenerated from a custom OAuth Client, see docs"},"clientSecret":{"type":"string","description":"Google Cloud Console project OAuth Client Secret\\n\\nGenerated from a custom OAuth Client, see docs"},"redirectUri":{"type":"string","description":"Google Cloud Console project OAuth Client Authorized redirect URI\\n\\nGenerated from a custom OAuth Client, see docs. multi-scrobbler will generate a default based on BASE_URL.\\nOnly specify this if the default does not work for you."},"innertubeOptions":{"$ref":"#/definitions/InnertubeOptions","description":"Additional options for authorization and tailoring YTM client"}},"title":"YTMusicData"},"InnertubeOptions":{"type":"object","properties":{"po_token":{"type":"string","description":"Proof of Origin token\\n\\nMay be required if YTM starts returning 403"},"visitor_data":{"type":"string","description":"Visitor ID value found in VISITOR_INFO1_LIVE or visitorData cookie\\n\\nMay be required if YTM starts returning 403"},"account_index":{"type":"number","description":"If account login results in being able to choose multiple account, use a zero-based index to choose which one to monitor","examples":[0,1]},"location":{"type":"string"},"lang":{"type":"string"},"generate_session_locally":{"type":"boolean"},"device_category":{"type":"string"},"client_type":{"type":"string"},"timezone":{"type":"string"}},"title":"InnertubeOptions"},"YandexMusicBridgeSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/YandexMusicBridgeData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["ymbridge"]}},"required":["type"],"title":"YandexMusicBridgeSourceAIOConfig"},"YandexMusicBridgeData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the local Python bridge, for example http://yandex-music-bridge:9980"},"apiKey":{"type":"string","description":"Optional API key sent as X-API-Key to the bridge"}},"required":["url"],"title":"YandexMusicBridgeData"},"MPRISSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MPRISData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["mpris"]}},"required":["data","type"],"title":"MPRISSourceAIOConfig"},"MPRISData":{"type":"object","properties":{"blacklist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any players that START WITH these values, case-insensitive","examples":[["spotify","vlc"]]},"whitelist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY from any players that START WITH these values, case-insensitive\\n\\nIf whitelist is present then blacklist is ignored","examples":[["spotify","vlc"]]}},"title":"MPRISData"},"MopidySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MopidyData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["mopidy"]}},"required":["data","type"],"title":"MopidySourceAIOConfig"},"MopidyData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the Mopidy HTTP server to connect to\\n\\nYou MUST have Mopidy-HTTP extension enabled: https://mopidy.com/ext/http\\n\\nmulti-scrobbler connects to the WebSocket endpoint that ultimately looks like this => `ws://localhost:6680/mopidy/ws/`\\n\\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\\n\\nParts => [default value]\\n\\n* Protocol => `ws://`\\n* Hostname => `localhost`\\n* Port => `6680`\\n* Path => `/mopidy/ws/`","examples":["ws://localhost:6680/mopidy/ws/"],"default":"ws://localhost:6680/mopidy/ws/"},"uriBlacklist":{"type":"array","items":{"type":"string"},"description":"Do not scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive\\n\\nEX: Don\'t scrobble tracks from soundcloud by adding \'soundcloud\' to this list.\\n\\nList is ignored if uriWhitelist is used."},"uriWhitelist":{"type":"array","items":{"type":"string"},"description":"Only scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive\\n\\nEX: Only scrobble tracks from soundcloud by adding \'soundcloud\' to this list."},"albumBlacklist":{"type":"array","items":{"type":"string"},"description":"Remove album data that matches any case-insensitive string from this list when scrobbling,\\n\\nFor certain sources (Soundcloud) Mopidy does not have all track info (Album) and will instead use \\"Soundcloud\\" as the Album name. You can prevent multi-scrobbler from using this bad Album data by adding the fake name to this list. Multi-scrobbler will still scrobble the track, just without the bad data.","examples":[["Soundcloud","Mixcloud"]],"default":["Soundcloud"]}},"title":"MopidyData"},"ListenBrainzSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ListenBrainzSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `listenbrainz.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["listenbrainz"]}},"required":["configureAs","data","type"],"title":"ListenBrainzSourceAIOConfig"},"ListenBrainzSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the ListenBrainz server, if not using the default","examples":["https://api.listenbrainz.org/"],"default":"https://api.listenbrainz.org/"},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","username"],"title":"ListenBrainzSourceData"},"JRiverSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JRiverData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["jriver"]}},"required":["data","type"],"title":"JRiverSourceAIOConfig"},"JRiverData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the JRiver HTTP server to connect to\\n\\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:52199/MCWS/v1/`\\n\\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\\n\\nParts => [default value]\\n\\n* Protocol => `http://`\\n* Hostname => `localhost`\\n* Port => `52199`\\n* Path => `/MCWS/v1/`","examples":["http://localhost:52199/MCWS/v1/"],"default":"http://localhost:52199/MCWS/v1/"},"username":{"type":"string","description":"If you have enabled authentication, the username you set"},"password":{"type":"string","description":"If you have enabled authentication, the password you set"}},"required":["url"],"title":"JRiverData"},"KodiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/KodiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["kodi"]}},"required":["data","type"],"title":"KodiSourceAIOConfig"},"KodiData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the Kodi HTTP server to connect to\\n\\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:8080/jsonrpc`\\n\\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\\n\\nParts => [default value]\\n\\n* Protocol => `http://`\\n* Hostname => `localhost`\\n* Port => `8080`\\n* Path => `/jsonrpc`","examples":["http://localhost:8080/jsonrpc"],"default":"http://localhost:8080/jsonrpc"},"username":{"type":"string","description":"The username set for Remote Control via Web Sever"},"password":{"type":"string","description":"The password set for Remote Control via Web Sever"}},"required":["url","username","password"],"title":"KodiData"},"WebScrobblerSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/WebScrobblerData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["webscrobbler"]}},"required":["type"],"title":"WebScrobblerSourceAIOConfig"},"WebScrobblerData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\\n\\nIn WebScrobbler\'s Webhook you must set an \'API URL\'. All MS WebScrobbler sources must start like:\\n\\nhttp://localhost:9078/api/webscrobbler\\n\\nIf you are using multiple WebScrobbler sources (scrobbles for many users) you must use a slug to match Sources with each users extension.\\n\\nExample:\\n\\n* slug: \'usera\' => API URL: http://localhost:9078/api/webscrobbler/usera\\n* slug: \'userb\' => API URL: http://localhost:9078/api/webscrobbler/userb\\n\\nIf no slug is found from an extension\'s incoming webhook event the first WebScrobbler source without a slug will be used"},"blacklist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Block scrobbling from specific WebScrobbler Connectors","examples":[["youtube"]]},"whitelist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only allow scrobbling from specific WebScrobbler Connectors","examples":[["mixcloud","soundcloud","bandcamp"]]}},"title":"WebScrobblerData"},"ChromecastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ChromecastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["chromecast"]}},"required":["data","type"],"title":"ChromecastSourceAIOConfig"},"ChromecastData":{"type":"object","properties":{"blacklistDevices":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any cast devices that START WITH these values, case-insensitive\\n\\nUseful when used with auto discovery","examples":[["home-mini","family-tv"]]},"whitelistDevices":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY scrobble from any cast device that START WITH these values, case-insensitive\\n\\nIf whitelist is present then blacklist is ignored\\n\\nUseful when used with auto discovery","examples":[["home-mini","family-tv"]]},"blacklistApps":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any application that START WITH these values, case-insensitive","examples":[["spotify","pandora"]]},"whitelistApps":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY scrobble from any application that START WITH these values, case-insensitive\\n\\nIf whitelist is present then blacklist is ignored","examples":[["spotify","pandora"]]},"useAvahi":{"type":"boolean","description":"Try to use Avahi and avahi-browse to resolve mDNS devices instead of native mDNS querying\\n\\nUseful for docker (alpine) container where mDNS resolution is not yet supported. Avahi socket must be exposed to the container and avahi-tools must be installed.","default":false},"useAutoDiscovery":{"type":"boolean","description":"Use mDNS to discovery Google Cast devices on your next automatically?\\n\\nIf not explicitly set then it is TRUE if `devices` is not set"},"devices":{"type":"array","items":{"$ref":"#/definitions/ChromecastDeviceInfo"},"description":"A list of Google Cast devices to monitor\\n\\nIf this is used then `useAutoDiscovery` is set to FALSE, if not explicitly set"},"allowUnknownMedia":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Chromecast Apps report a \\"media type\\" in the status info returned for whatever is currently playing\\n\\n* If set to TRUE then Music AND Generic/Unknown media will be tracked for ALL APPS\\n* If set to FALSE then only media explicitly typed as Music will be tracked for ALL APPS\\n* If set to a list then only Apps whose name contain one of these values, case-insensitive, will have Music AND Generic/Unknown tracked\\n\\nSee https://developers.google.com/cast/docs/media/messages#MediaInformation \\"metadata\\" property","default":false},"forceMediaRecognitionOn":{"type":"array","items":{"type":"string"},"description":"Media provided by any App whose name is listed here will ALWAYS be tracked, regardless of the \\"media type\\" reported\\n\\nApps will be recognized if they CONTAIN any of these values, case-insensitive"}},"title":"ChromecastData"},"ChromecastDeviceInfo":{"type":"object","properties":{"name":{"type":"string","description":"A friendly name to identify this device","examples":["MySmartTV"]},"address":{"type":"string","description":"The IP address of the device","examples":["192.168.0.115"]}},"required":["name","address"],"title":"ChromecastDeviceInfo"},"MalojaSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MalojaSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `maloja.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["maloja"]}},"required":["configureAs","data","type"],"title":"MalojaSourceAIOConfig"},"MalojaSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for maloja server","examples":["http://localhost:42010"]},"apiKey":{"type":"string","description":"API Key for Maloja server","examples":["myApiKey"]}},"required":["apiKey","url"],"title":"MalojaSourceData"},"MusikcubeSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MusikcubeData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["musikcube"]}},"required":["data","type"],"title":"MusikcubeSourceAIOConfig"},"MusikcubeData":{"type":"object","properties":{"url":{"type":"string","description":"URL of the Musikcube Websocket (Metadata) server to connect to\\n\\nYou MUST have enabled \'metadata\' server and set a password: https://github.com/clangen/musikcube/wiki/remote-api-documentation\\n * musikcube -> settings -> server setup\\n\\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\\n\\nParts => [default value]\\n\\n* Protocol => `ws://`\\n* Hostname => `localhost`\\n* Port => `7905`","examples":["ws://localhost:7905"],"default":"ws://localhost:7905"},"password":{"type":"string","description":"Password set in Musikcube https://github.com/clangen/musikcube/wiki/remote-api-documentation\\n\\n* musikcube -> settings -> server setup -> password"},"device_id":{"type":"string"}},"required":["password"],"title":"MusikcubeData"},"MusicCastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MusicCastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["musiccast"]}},"required":["data","type"],"title":"MusicCastSourceAIOConfig"},"MusicCastData":{"type":"object","properties":{"url":{"type":"string","description":"The host or URL of the YamahaExtendedControl endpoint to use","examples":[["192.168.0.101","http://192.168.0.101/YamahaExtendedControl"]]}},"required":["url"],"title":"MusicCastData"},"MPDSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MPDData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/MPDSourceOptions"},"type":{"type":"string","enum":["mpd"]}},"required":["data","options","type"],"title":"MPDSourceAIOConfig"},"MPDData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL:PORT of the MPD server to connect to\\n\\nTo use this you must have TCP connections enabled for your MPD server https://mpd.readthedocs.io/en/stable/user.html#client-connections","examples":["localhost:6600"],"default":"localhost:6600"},"path":{"type":"string","description":"If using socket specify the path instead of url.\\n\\ntrailing `~` is replaced by your home directory"},"password":{"type":"string","description":"Password for the server, if set https://mpd.readthedocs.io/en/stable/user.html#permissions-and-passwords"}},"title":"MPDData"},"MPDSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"MPDSourceOptions"},"VLCSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/VLCData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/VLCSourceOptions"},"type":{"type":"string","enum":["vlc"]}},"required":["data","type"],"title":"VLCSourceAIOConfig"},"VLCData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL:PORT of the VLC server to connect to\\n\\nTo use this you must have the Web (http) interface module enabled and a password set","examples":["localhost:8080"],"default":"localhost:8080"},"password":{"type":"string","description":"Password for the server"}},"required":["password"],"title":"VLCData"},"VLCSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"filenamePatterns":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"A list of regular expressions to use to extract metadata (title, album, artist) from a filename\\n\\nUsed when VLC reports only the filename for the current audio track"},"logFilenamePatterns":{"type":"boolean","description":"Log to DEBUG when a filename-only track is matched or not matched by filenamePatterns","default":false},"dumpVlcMetadata":{"type":"boolean","description":"Dump all the metadata VLC reports for an audio track to DEBUG.\\n\\nUse this if reporting an issue with VLC not correctly capturing metadata for a track.","default":false}},"title":"VLCSourceOptions"},"IcecastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/IcecastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/IcecastSourceOptions"},"type":{"type":"string","enum":["icecast"]}},"required":["data","type"],"title":"IcecastSourceAIOConfig"},"IcecastData":{"type":"object","properties":{"sources":{"type":"array","items":{"$ref":"#/definitions/IcecastSource"}},"icestatsEndpoint":{"type":"string"},"statsEndpoint":{"type":"string"},"nextsongsEndpoint":{"type":"string"},"sevenhtmlEndpoint":{"type":"string"},"icyMetaInt":{"type":"number"},"url":{"type":"string","description":"The Icecast stream URL"}},"required":["url"],"title":"IcecastData"},"IcecastSource":{"type":"string","enum":["icy","ogg","icestats","stats","sevenhtml","nextsongs"],"title":"IcecastSource"},"IcecastSourceOptions":{"type":"object","properties":{"systemScrobble":{"type":"boolean","description":"For Sources that support manual listening, should MS default to scrobbling when no user interaction has occurred?\\n\\nIf not specified MS will use a Source\'s specific behavior, see Source\'s documentation."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"IcecastSourceOptions"},"AzuracastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/AzuracastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["azuracast"]}},"required":["data","type"],"title":"AzuracastSourceAIOConfig"},"AzuracastData":{"type":"object","properties":{"url":{"type":"string","description":"Base URL of the Azuracast instance\\n\\nThis does NOT include the station. If a station is included it will be ignored. Use `station` field to specify station, if necessary","examples":["https://radio.mydomain.tld","http://localhost:80"]},"station":{"type":"string","description":"The specific station to monitor\\n\\nScrobbling will only occur if any of the monitor conditions are met AND the station is ONLINE.\\n\\nTo monitor multiple stations create a Source for each station.","examples":["my-station-1"]},"monitorWhenListeners":{"type":["boolean","number"],"description":"Only activate scrobble monitoring if station\\n\\n* `true` => has any current listeners\\n* `number` => has EQUAL TO or MORE THAN X number of listeners"},"monitorWhenLive":{"type":"boolean","description":"Only activate scrobble monitoring if station has a live DJ/Streamer","default":true},"apiKey":{"type":"string","description":"API Key used to access data about private streams\\n\\nhttps://www.azuracast.com/docs/developers/apis/#api-authentication"}},"required":["url","station"],"title":"AzuracastData"},"KoitoSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/KoitoSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `koito.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["koito"]}},"required":["configureAs","data","type"],"title":"KoitoSourceAIOConfig"},"KoitoSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the Koito server","examples":["http://192.168.0.100:4110"]},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","url","username"],"title":"KoitoSourceData"},"TealSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/TealSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/TealSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"Should always be `souce` when using Tealfm as a Source","default":"source","examples":["source"]},"type":{"type":"string","enum":["tealfm"]}},"required":["configureAs","data","type"],"title":"TealSourceAIOConfig"},"TealSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"baseUri":{"type":"string","description":"The base URI of the Multi-Scrobbler to use for ATProto OAuth\\n\\nOnly include this if you want to use OAuth. The URI must be a non-IP/non-local domain using https: protocol."},"identifier":{"type":"string","description":"Identify the account to login as\\n\\n* For **App Password** Auth - your email\\n* For **Oauth** - your handle minus the @"},"appPassword":{"type":"string","description":"The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account\\n\\nThis is created under https://bsky.app/settings/app-passwords\\n\\n**Use this if you are self-hosting Multi-Scrobbler on localhost or accessed like http://IP:PORT**"},"serviceAllow":{"type":"array","items":{"type":"string"}},"serviceDeny":{"type":"array","items":{"type":"string"}}},"required":["identifier"],"title":"TealSourceData"},"TealSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"TealSourceOptions"},"RockskySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/RockskySourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/RockskySourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `rocksky.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["rocksky"]}},"required":["configureAs","data","type"],"title":"RockskySourceAIOConfig"},"RockskySourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"key":{"type":"string","description":"API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"handle":{"type":"string","description":"The **fully-qualified** handle for your ATPRoto/Bluesky account, like:\\n\\n* alice.bsky.social\\n* foxxmd.com\\n* mysuer.blacksky.app"}},"required":["handle","key"],"title":"RockskySourceData"},"RockskySourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"audioScrobblerUrl":{"type":"string","description":"URL for the Rocksky *Listenbrainz* endpoint, if not using the default","examples":["https://audioscrobbler.rocksky.app"],"default":"https://audioscrobbler.rocksky.app"},"apiUrl":{"type":"string","description":"URL for the Rocksky *API* endpoint, if not using the default","examples":["https://api.rocksky.app"],"default":"https://api.rocksky.app"}},"title":"RockskySourceOptions"},"SonosSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SonosData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/SonosSourceOptions"},"type":{"type":"string","enum":["sonos"]}},"required":["data","type"],"title":"SonosSourceAIOConfig"},"SonosData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\\n\\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\\n* If the player has an existing session w/ track then MS attempts to scrobble it\\n\\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\\n\\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\\n\\n* The current Player session is ended and the Player is removed from MS\\n* MS attempts to scrobble, if the Player has an existing session w/ track\\n\\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\\n\\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"host":{"type":"string","description":"IP address of any connected Sonos speaker or device","examples":["192.168.0.170"]},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device name contains strings from this list (case-insensitive)"},"groupsAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive)"},"groupsBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive)"}},"required":["host"],"title":"SonosData"},"SonosSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\\n\\n* `false` => do not log\\n* `debug` => log to DEBUG level\\n* `warn` => log to WARN level (default)\\n\\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\\n\\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\\n\\n* supports fetching a listen history\\n* and this option is enabled\\n\\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \\"scrobbable\\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\\n\\n* Only applies if this source supports fetching a listen history\\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"logEmptyPlayer":{"type":"boolean"}},"title":"SonosSourceOptions"},"ClientAIOConfig":{"anyOf":[{"$ref":"#/definitions/MalojaClientAIOConfig"},{"$ref":"#/definitions/LastfmClientAIOConfig"},{"$ref":"#/definitions/LibrefmClientAIOConfig"},{"$ref":"#/definitions/ListenBrainzClientAIOConfig"},{"$ref":"#/definitions/KoitoClientAIOConfig"},{"$ref":"#/definitions/TealClientAIOConfig"},{"$ref":"#/definitions/RockSkyClientAIOConfig"},{"$ref":"#/definitions/DiscordClientAIOConfig"}],"title":"ClientAIOConfig"},"MalojaClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/MalojaClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Maloja as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["maloja"]}},"required":["data","name","type"],"title":"MalojaClientAIOConfig"},"MalojaClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for maloja server","examples":["http://localhost:42010"]},"apiKey":{"type":"string","description":"API Key for Maloja server","examples":["myApiKey"]}},"required":["apiKey","url"],"title":"MalojaClientData"},"CommonClientOptions":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\\n\\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\\n\\nThis setting should only be changed in specific scenarios where MS is handling multiple \\"relaying\\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\\n\\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \\"new\\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":3,"examples":[3]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"CommonClientOptions"},"LastfmClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/LastfmData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/LastfmClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using LastFM as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["lastfm"]}},"required":["data","name","type"],"title":"LastfmClientAIOConfig"},"LastfmData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"API Key generated from Last.fm/Libre.fm account","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"secret":{"type":"string","description":"Secret generated from Last.fm/Libre.fm account","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \\"lastfm/callback\\" in the URL somewhere.","default":"http://localhost:9078/lastfm/callback","examples":["http://localhost:9078/lastfm/callback"]}},"required":["apiKey","secret"],"title":"LastfmData"},"LastfmClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\\n\\n* `true` (default) => Report Now Playing from any eligible Source. \\n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\\n* `false` => Do not report Now Playing\\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\\n\\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\\n\\nThis setting should only be changed in specific scenarios where MS is handling multiple \\"relaying\\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\\n\\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \\"new\\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":3,"examples":[3]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"LastfmClientOptions"},"LibrefmClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/LibrefmData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/LibrefmClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Librefm as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["librefm"]}},"required":["data","name","type"],"title":"LibrefmClientAIOConfig"},"LibrefmClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\\n\\n* `true` (default) => Report Now Playing from any eligible Source. \\n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\\n* `false` => Do not report Now Playing\\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\\n\\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\\n\\nThis setting should only be changed in specific scenarios where MS is handling multiple \\"relaying\\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\\n\\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \\"new\\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":3,"examples":[3]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"LibrefmClientOptions"},"ListenBrainzClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/ListenBrainzClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Listenbrainz as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["listenbrainz"]}},"required":["data","name","type"],"title":"ListenBrainzClientAIOConfig"},"ListenBrainzClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the ListenBrainz server, if not using the default","examples":["https://api.listenbrainz.org/"],"default":"https://api.listenbrainz.org/"},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","username"],"title":"ListenBrainzClientData"},"KoitoClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/KoitoClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Koito as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["koito"]}},"required":["data","name","type"],"title":"KoitoClientAIOConfig"},"KoitoClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the Koito server","examples":["http://192.168.0.100:4110"]},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","url","username"],"title":"KoitoClientData"},"TealClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/TealClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/TealClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Tealfm as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["tealfm"]}},"required":["data","name","type"],"title":"TealClientAIOConfig"},"TealClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"baseUri":{"type":"string","description":"The base URI of the Multi-Scrobbler to use for ATProto OAuth\\n\\nOnly include this if you want to use OAuth. The URI must be a non-IP/non-local domain using https: protocol."},"identifier":{"type":"string","description":"Identify the account to login as\\n\\n* For **App Password** Auth - your email\\n* For **Oauth** - your handle minus the @"},"appPassword":{"type":"string","description":"The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account\\n\\nThis is created under https://bsky.app/settings/app-passwords\\n\\n**Use this if you are self-hosting Multi-Scrobbler on localhost or accessed like http://IP:PORT**"}},"required":["identifier"],"title":"TealClientData"},"TealClientOptions":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\\n\\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\\n\\nThis setting should only be changed in specific scenarios where MS is handling multiple \\"relaying\\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\\n\\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \\"new\\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":3,"examples":[3]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"TealClientOptions"},"RockSkyClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/RockSkyClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/RockSkyClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using RockSky as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["rocksky"]}},"required":["data","name","type"],"title":"RockSkyClientAIOConfig"},"RockSkyClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"key":{"type":"string","description":"API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"handle":{"type":"string","description":"The **fully-qualified** handle for your ATPRoto/Bluesky account, like:\\n\\n* alice.bsky.social\\n* foxxmd.com\\n* mysuer.blacksky.app"}},"required":["handle","key"],"title":"RockSkyClientData"},"RockSkyClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\\n\\n* `true` (default) => Report Now Playing from any eligible Source. \\n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\\n* `false` => Do not report Now Playing\\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\\n\\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\\n\\nThis setting should only be changed in specific scenarios where MS is handling multiple \\"relaying\\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\\n\\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \\"new\\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":3,"examples":[3]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"audioScrobblerUrl":{"type":"string","description":"URL for the Rocksky *Listenbrainz* endpoint, if not using the default","examples":["https://audioscrobbler.rocksky.app"],"default":"https://audioscrobbler.rocksky.app"},"apiUrl":{"type":"string","description":"URL for the Rocksky *API* endpoint, if not using the default","examples":["https://api.rocksky.app"],"default":"https://api.rocksky.app"}},"title":"RockSkyClientOptions"},"DiscordClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/DiscordClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Koito as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["discord"]}},"required":["data","name","type"],"title":"DiscordClientAIOConfig"},"DiscordClientData":{"type":"object","properties":{"token":{"type":"string"},"applicationId":{"type":"string"},"artwork":{"anyOf":[{"type":"boolean"},{"type":"string"},{"type":"array","items":{"type":"string"}}]},"artworkDefaultUrl":{"type":"string"},"statusOverrideAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"$ref":"#/definitions/StatusType"}}]},"listeningActivityAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"ipcLocations":{"anyOf":[{"type":"string"},{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"array","minItems":2,"items":[{"type":"number"},{"type":"string"}],"maxItems":2}]}}]}},"title":"DiscordClientData"},"StatusType":{"type":"string","enum":["online","idle","dnd","invisible"],"title":"StatusType"},"WebhookConfig":{"anyOf":[{"$ref":"#/definitions/GotifyConfig"},{"$ref":"#/definitions/NtfyConfig"},{"$ref":"#/definitions/AppriseConfig"}],"title":"WebhookConfig"},"GotifyConfig":{"type":"object","properties":{"type":{"type":"string","enum":["gotify","ntfy","apprise"],"description":"Webhook type. Valid values are:\\n\\n* gotify\\n* ntfy","examples":["gotify"]},"name":{"type":"string","description":"A friendly name used to identify webhook config in logs"},"url":{"type":"string","description":"The URL of the Gotify server. Same URL that would be used to reach the Gotify UI","examples":["http://192.168.0.100:8078"]},"token":{"type":"string","description":"The token created for this Application in Gotify","examples":["AQZI58fA.rfSZbm"]},"priorities":{"$ref":"#/definitions/PrioritiesConfig","description":"Priority of messages\\n\\n* Info -> 5\\n* Warn -> 7\\n* Error -> 10"}},"required":["token","type","url"],"title":"GotifyConfig"},"PrioritiesConfig":{"type":"object","properties":{"info":{"type":"number","examples":[5]},"warn":{"type":"number","examples":[7]},"error":{"type":"number","examples":[10]}},"required":["info","warn","error"],"title":"PrioritiesConfig"},"NtfyConfig":{"type":"object","properties":{"type":{"type":"string","enum":["gotify","ntfy","apprise"],"description":"Webhook type. Valid values are:\\n\\n* gotify\\n* ntfy","examples":["gotify"]},"name":{"type":"string","description":"A friendly name used to identify webhook config in logs"},"url":{"type":"string","description":"The URL of the Ntfy server","examples":["http://192.168.0.100:8078"]},"topic":{"type":"string","description":"The topic mutli-scrobbler should POST to"},"username":{"type":"string","description":"Required if topic is protected"},"password":{"type":"string","description":"Required if topic is protected"},"token":{"type":"string","description":"Use instead of username/password, required if topic is protected"},"priorities":{"$ref":"#/definitions/PrioritiesConfig","description":"Priority of messages\\n\\n* Info -> 3\\n* Warn -> 4\\n* Error -> 5"}},"required":["topic","type","url"],"title":"NtfyConfig"},"AppriseConfig":{"type":"object","properties":{"type":{"type":"string","enum":["gotify","ntfy","apprise"],"description":"Webhook type. Valid values are:\\n\\n* gotify\\n* ntfy","examples":["gotify"]},"name":{"type":"string","description":"A friendly name used to identify webhook config in logs"},"host":{"type":"string","description":"The URL of the apprise-api server","examples":["http://192.168.0.100:8078"]},"urls":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"If using [Stateless Endpoints](https://github.com/caronc/apprise-api?tab=readme-ov-file#stateless-solution) the Apprise config URL(s) to send"},"keys":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"If using [Persistent Store Endpoints](https://github.com/caronc/apprise-api?tab=readme-ov-file#persistent-storage-solution) the Configuration ID(s) to send to\\n\\nNote: If multiple keys are defined then MS will attempt to POST to each one individually"},"tags":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Optional [tag(s)](https://github.com/caronc/apprise-api?tab=readme-ov-file#tagging) to send in the notification payload"}},"required":["host","type"],"title":"AppriseConfig"},"LogOptions":{"type":"object","properties":{"level":{"$ref":"#/definitions/LogLevel","description":"Specify the minimum log level for all log outputs without their own level specified.\\n\\nDefaults to env `LOG_LEVEL` or `info` if not specified.","default":"info"},"file":{"anyOf":[{"$ref":"#/definitions/LogLevel"},{"type":"boolean","const":false},{"$ref":"#/definitions/FileLogOptions"}],"description":"Specify the minimum log level to output to rotating files or file output options. If `false` no log files will be created."},"console":{"$ref":"#/definitions/LogLevel","description":"Specify the minimum log level streamed to the console (or docker container)"}},"description":"Configure log levels and file options for an AppLogger.\\n\\n```ts\\nconst infoLogger = loggerApp({\\n level: \'info\' // console and file will log any levels `info` and above\\n});\\n\\nconst logger = loggerApp({\\n console: \'debug\', // console will log `debug` and higher\\n file: \'warn\' // file will log `warn` and higher\\n});\\n\\nconst fileLogger = loggerRollingApp({\\n console: \'debug\', // console will log `debug` and higher\\n file: {\\n level: \'warn\', // file will log `warn` and higher\\n path: \'/my/cool/path/output.log\', // optionally, output to log file at this path\\n frequency: \'hourly\', // optionally, rotate hourly\\n }\\n});\\n```","title":"LogOptions"},"CacheConfigOptions":{"type":"object","properties":{"metadata":{"$ref":"#/definitions/CacheMetadataConfig"},"scrobble":{"$ref":"#/definitions/CacheScrobbleConfig"},"auth":{"$ref":"#/definitions/CacheAuthConfig"},"regex":{"type":"number","description":"Number of regex functions to cache (LRU)","default":200}},"title":"CacheConfigOptions"},"CacheMetadataConfig":{"$ref":"#/definitions/CacheConfigType","title":"CacheMetadataConfig"},"CacheConfigType":{"anyOf":[{"$ref":"#/definitions/CacheMemoryConfig"},{"$ref":"#/definitions/CacheValkeyConfig"},{"$ref":"#/definitions/CacheFileConfig"},{"$ref":"#/definitions/CacheNoopConfig"}],"title":"CacheConfigType"},"CacheMemoryConfig":{"type":"object","properties":{"provider":{"type":"string","enum":["memory"]},"connection":{"type":"string"},"lruSize":{"type":"number"},"ttl":{"type":"number"}},"required":["provider"],"title":"CacheMemoryConfig"},"CacheValkeyConfig":{"type":"object","properties":{"provider":{"type":"string","enum":["valkey"]},"connection":{"type":"string"}},"required":["provider"],"title":"CacheValkeyConfig"},"CacheFileConfig":{"type":"object","properties":{"provider":{"type":"string","enum":["file"]},"connection":{"type":"string"}},"required":["provider"],"title":"CacheFileConfig"},"CacheNoopConfig":{"type":"object","properties":{"provider":{"type":"boolean","const":false},"connection":{"type":"string"}},"required":["provider"],"title":"CacheNoopConfig"},"CacheScrobbleConfig":{"$ref":"#/definitions/CacheConfigType","title":"CacheScrobbleConfig"},"CacheAuthConfig":{"$ref":"#/definitions/CacheConfigType","title":"CacheAuthConfig"},"TransformerCommonConfig":{"type":"object","properties":{"defaults":{"type":"object"},"data":{"type":"object"},"type":{"type":"string"},"name":{"type":"string"},"options":{"$ref":"#/definitions/TransformOptions"}},"required":["type"],"title":"TransformerCommonConfig"},"TransformOptions":{"type":"object","properties":{"failOnFetch":{"type":"boolean"},"throwOnFailure":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string","enum":["artists","title","albumArtists","album","duration","meta","art"]}}]},"ttl":{"type":"string"}},"title":"TransformOptions"}}}'),p=JSON.parse('{"sourceDefaults":{"maxPollRetries":0,"maxRequestRetries":1,"retryMultiplier":1.5},"clientDefaults":{"maxRequestRetries":1,"retryMultiplier":1.5},"baseUrl":"http://localhost","sources":[{"type":"spotify","clients":["myConfig"],"name":"mySpotifySource","data":{"clientId":"a89cba1569901a0671d5a9875fed4be1","clientSecret":"ec42e09d5ae0ee0f0816ca151008412a","redirectUri":"http://localhost:9078/callback"}}],"clients":[{"type":"maloja","name":"myConfig","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}}]}'),d=i(8258),u=i(4020),f=i(8182),h=i.n(f),y=i(4254),m=i(4848);let b=(0,u.IM)({schema:c,ajvOptions:{strict:"log",strictSchema:"log",allowUnionTypes:!0},onCreateAjv:e=>(e.addKeyword("deprecationMessage"),h().default(e),e)});function g(){let{colorMode:e}=(0,r.G)(),t=(0,o.l)({desktopBreakpoint:500}),[i,n]=(0,s.useState)({json:p});return(0,m.jsxs)("div",{className:"schemaWrapper2",children:[(0,m.jsxs)("div",{style:{padding:"0 1em 0 1em",flexBasis:"min-content",flexGrow:2},children:[(0,m.jsxs)(y.A,{type:"note",summary:"Config Structure",children:[(0,m.jsxs)("p",{children:["This displays the ",(0,m.jsx)("strong",{children:"structure"})," of the ",(0,m.jsxs)("a",{href:"/configuration?configType=aio#configuration-types",children:["all-in-one (",(0,m.jsx)("code",{children:"config.json"}),") configuration"]})," with all possible properties, their shape, and descriptions/types."]}),(0,m.jsx)("p",{children:"Use this to understand how to write a valid config."})]}),(0,m.jsx)(d.Es,{schema:c,renderRootTreeLines:!0,emptyText:"No schema defined",defaultExpandedDepth:0,markup:!0})]}),(0,m.jsxs)("div",{style:{flexBasis:"auto",flexGrow:3},children:[(0,m.jsx)("div",{className:"fileEditor2",children:(0,m.jsxs)(y.A,{type:"note",summary:"Config Example",children:[(0,m.jsxs)("p",{children:["This displays an ",(0,m.jsx)("strong",{children:"example config file"})," of a ",(0,m.jsxs)("a",{href:"/configuration?configType=aio#configuration-types",children:["all-in-one (",(0,m.jsx)("code",{children:"config.json"}),") configuration"]})," that adheres to the shown ",(0,m.jsx)("strong",{children:"Config Structure."})]}),(0,m.jsxs)("ul",{children:[(0,m.jsxs)("li",{children:[(0,m.jsx)("strong",{children:"text"})," mode lets you edit the JSON directly."]}),(0,m.jsxs)("li",{children:[(0,m.jsx)("strong",{children:"tree"})," mode gives you a guided point-and-click editing experience that always keeps the JSON ",(0,m.jsx)("i",{children:"syntax"})," valid."]})]}),(0,m.jsxs)("p",{children:["Both modes ",(0,m.jsx)("strong",{children:"validate"})," that the configuraion is correct. Any errors show up as squiggly lines."]}),(0,m.jsxs)("p",{children:["After you finish editing, switch to ",(0,m.jsx)("strong",{children:"text"})," and then copy all text to get a completed config."]})]})}),(0,m.jsx)("div",{className:"fileEditorMobile2",children:(0,m.jsxs)(y.A,{type:"warning",summary:"Config Example Mobile Experience",children:[(0,m.jsxs)("p",{children:["This displays an ",(0,m.jsx)("strong",{children:"example config file"})," of a ",(0,m.jsxs)("a",{href:"/configuration?configType=aio#configuration-types",children:["all-in-one (",(0,m.jsx)("code",{children:"config.json"}),") configuration"]})," that adheres to the shown ",(0,m.jsx)("strong",{children:"Config Structure."})]}),(0,m.jsxs)("p",{children:["The example config file editor is meant for a larger screen experience so ",(0,m.jsx)("strong",{children:"only the read-only example is shown."})," Please use this site on a tablet/laptop/desktop to enable file editor features."]})]})}),"mobile"===t?(0,m.jsx)(l(),{title:"CONFIG/config.json",language:"json5",children:JSON.stringify(i,null,2)}):(0,m.jsx)(u.t$,{validator:b,theme:e,content:i,mode:"text",style:{height:"initial"}})]})]})}function S(){return(0,m.jsx)(n.default,{fallback:(0,m.jsx)("div",{children:"Loading..."}),children:()=>(0,m.jsx)(g,{})})}},1891(e,t,i){i.r(t),i.d(t,{default:()=>l}),i(6540);var s=i(195),n=i(8478),r=i(349),o=i(4848);function a(){return(0,o.jsx)(r.A,{fallback:({error:e,tryAgain:t})=>(0,o.jsxs)("div",{children:[(0,o.jsxs)("p",{children:["Playground component crashed because of error: ",e.message,"."]}),(0,o.jsx)("button",{onClick:t,children:"Try Again!"})]}),children:(0,o.jsx)(n.default,{fallback:(0,o.jsx)("div",{children:"Loading..."}),children:()=>{let e=i(5543).A;return(0,o.jsx)(e,{})}})})}function l(){return(0,o.jsx)(s.A,{title:"Playground",description:"Playground",children:(0,o.jsx)(a,{})})}}}]); \ No newline at end of file diff --git a/assets/js/153c3ceb.54592e87.js b/assets/js/153c3ceb.54592e87.js deleted file mode 100644 index ffd3f5f9..00000000 --- a/assets/js/153c3ceb.54592e87.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["1309"],{9127(e,t,l){l.r(t),l.d(t,{metadata:()=>a,default:()=>p,frontMatter:()=>n,contentTitle:()=>i,toc:()=>d,assets:()=>o});var a=JSON.parse('{"id":"development/flatpak","title":"Flatpak","description":"Building Flatpak App locally","source":"@site/docs/development/flatpak.md","sourceDirName":"development","slug":"/development/flatpak","permalink":"/multi-scrobbler/development/flatpak","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/development/flatpak.md","tags":[],"version":"current","sidebarPosition":4,"frontMatter":{"toc_min_heading_level":2,"toc_max_heading_level":5,"sidebar_position":4,"title":"Flatpak","description":"Building Flatpak App locally"},"sidebar":"tutorialSidebar","previous":{"title":"Client Development/Tutorial","permalink":"/multi-scrobbler/development/dev-client"},"next":{"title":"FAQ","permalink":"/multi-scrobbler/FAQ"}}'),r=l(4848),s=l(8453);let n={toc_min_heading_level:2,toc_max_heading_level:5,sidebar_position:4,title:"Flatpak",description:"Building Flatpak App locally"},i,o={},d=[{value:"Install Requirements",id:"install-requirements",level:2},{value:"Flatpak and flatpak-builder",id:"flatpak-and-flatpak-builder",level:3},{value:"flatpak-node-generator",id:"flatpak-node-generator",level:4},{value:"Update Project source",id:"update-project-source",level:2},{value:"Generate Sources and Build",id:"generate-sources-and-build",level:2},{value:"Use Setup Script",id:"use-setup-script",level:3},{value:"Manual Setup",id:"manual-setup",level:3},{value:"Use flatpak-node-generator to generate sources",id:"use-flatpak-node-generator-to-generate-sources",level:4},{value:"Build flatpak",id:"build-flatpak",level:4}];function c(e){let t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",h4:"h4",p:"p",pre:"pre",...(0,s.R)(),...e.components},{Details:l}=t;return l||function(e,t){throw Error("Expected "+(t?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h2,{id:"install-requirements",children:"Install Requirements"}),"\n",(0,r.jsx)(t.h3,{id:"flatpak-and-flatpak-builder",children:"Flatpak and flatpak-builder"}),"\n",(0,r.jsxs)(t.p,{children:["Install ",(0,r.jsx)(t.a,{href:"https://flatpak.org/setup/",children:"Flatpak"})]}),"\n",(0,r.jsxs)(t.p,{children:["Install ",(0,r.jsx)(t.a,{href:"https://docs.flatpak.org/en/latest/first-build.html#building-your-first-flatpak",children:"flatpak-builder"})]}),"\n",(0,r.jsx)(t.h4,{id:"flatpak-node-generator",children:(0,r.jsx)(t.a,{href:"https://github.com/flatpak/flatpak-builder-tools/tree/master/node",children:"flatpak-node-generator"})}),"\n",(0,r.jsxs)(t.p,{children:["Requires python 3.7+, ",(0,r.jsx)(t.a,{href:"https://pip.pypa.io/en/stable/",children:"pip"}),"/",(0,r.jsx)(t.a,{href:"https://pypa.github.io/pipx/",children:"pipx"})]}),"\n",(0,r.jsx)(t.h2,{id:"update-project-source",children:"Update Project source"}),"\n",(0,r.jsxs)(t.p,{children:["Set the ",(0,r.jsx)(t.code,{children:"branch"})," ",(0,r.jsx)(t.code,{children:"tag"})," or ",(0,r.jsx)(t.code,{children:"commit"})," to use for MS in the ",(0,r.jsx)(t.code,{children:"source"})," section of ",(0,r.jsx)(t.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/flatpak/io.github.foxxmd.multiscrobbler.yml",children:(0,r.jsx)(t.code,{children:"io.github.foxxmd.multiscrobbler.yml"})})]}),"\n",(0,r.jsx)(t.h2,{id:"generate-sources-and-build",children:"Generate Sources and Build"}),"\n",(0,r.jsx)(t.h3,{id:"use-setup-script",children:"Use Setup Script"}),"\n",(0,r.jsx)(t.p,{children:"A convenience bash script is provided that automates generating offline sources and building the flatpak app for you. This is the recommend method to use."}),"\n",(0,r.jsxs)(t.p,{children:["Located in the project at ",(0,r.jsx)(t.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/flatpak/setup.sh",children:(0,r.jsx)(t.code,{children:"flatpak/setup.sh"})}),", run it from the ",(0,r.jsx)(t.code,{children:"flatpak"})," directory with this syntax:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-shell",children:"./setup.sh -o -b /path/to/flatpak/build/dir\n"})}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:"Args:\n\n-o => Delete and overwrite any existing generated sources\n-b => The absolute path to the directory that should be used for flatpak build/artifacts. If not defined will use `CWD/build`\n-y => Proceed without confirming settings\n"})}),"\n",(0,r.jsx)(t.h3,{id:"manual-setup",children:"Manual Setup"}),"\n",(0,r.jsxs)(t.p,{children:["If you cannot use ",(0,r.jsx)(t.code,{children:"setup.sh"})," follow the below to manually generate sources and build the flatpak app:"]}),"\n",(0,r.jsxs)(l,{children:[(0,r.jsx)("summary",{children:"Instructions"}),(0,r.jsxs)(t.h4,{id:"use-flatpak-node-generator-to-generate-sources",children:["Use ",(0,r.jsx)(t.code,{children:"flatpak-node-generator"})," to generate sources"]}),(0,r.jsxs)(t.p,{children:["First, ",(0,r.jsxs)(t.a,{href:"https://github.com/flatpak/flatpak-builder-tools/issues/354#issuecomment-1478518442",children:["make sure ",(0,r.jsx)(t.code,{children:"node_modules"})," is deleted or empty"]})," for both the project and ",(0,r.jsx)(t.code,{children:"docsite"})," directories."]}),(0,r.jsx)(t.p,{children:"Then, from MS project root:"}),(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-shell",metastring:'title="PROJECT_DIR"',children:"flatpak-node-generator npm package-lock.json\n"})}),(0,r.jsxs)(t.p,{children:["Move ",(0,r.jsx)(t.code,{children:"generated-sources.json"})," into ",(0,r.jsx)(t.code,{children:"PROJECT_DIR/flatpak"})," and rename ",(0,r.jsx)(t.code,{children:"generated-sources.0.json"})]}),(0,r.jsxs)(t.p,{children:["Then, generate ",(0,r.jsx)(t.code,{children:"docsite"})," sources:"]}),(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-shell",metastring:'title="PROJECT_DIR"',children:"flatpak-node-generator npm docsite/package-lock.json\n"})}),(0,r.jsxs)(t.p,{children:["Move ",(0,r.jsx)(t.code,{children:"generated-sources.json"})," into ",(0,r.jsx)(t.code,{children:"PROJECT_DIR/flatpak"})," and rename ",(0,r.jsx)(t.code,{children:"generated-sources.1.json"})]}),(0,r.jsx)(t.h4,{id:"build-flatpak",children:"Build flatpak"}),(0,r.jsxs)(t.p,{children:["In the below command replace ",(0,r.jsx)(t.code,{children:"/home/yourUser/multi-scrobbler-flatpak"})," with a directory to use for storing flatpak build/artifacts."]}),(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-shell",metastring:'title="PROJECT_DIR/flatpak"',children:"flatpak-builder --repo=/home/yourUser/multi-scrobbler-flatpak/repo --state-dir=/home/yourUser/multi-scrobbler-flatpak/state /home/yourUser/multi-scrobbler-flatpak/build io.github.foxxmd.multiscrobbler.yml --force-clean\n"})}),(0,r.jsx)(t.admonition,{type:"info",children:(0,r.jsxs)(t.p,{children:["Append ",(0,r.jsx)(t.code,{children:"--install --user"})," to the above command to have the app installed immediately."]})})]}),"\n",(0,r.jsx)(t.h1,{id:"run-app",children:"Run App"}),"\n",(0,r.jsxs)(t.p,{children:["If built with ",(0,r.jsx)(t.code,{children:"--install --user"})," (default when using ",(0,r.jsx)(t.code,{children:"setup.sh"}),") you can now run MS using the command"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-shell",children:"flatpak run -u io.github.foxxmd.multiscrobbler\n"})})]})}function p(e={}){let{wrapper:t}={...(0,s.R)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}}}]); \ No newline at end of file diff --git a/assets/js/153c3ceb.b35858a6.js b/assets/js/153c3ceb.b35858a6.js new file mode 100644 index 00000000..5bbc2025 --- /dev/null +++ b/assets/js/153c3ceb.b35858a6.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["1309"],{9127(e,t,l){l.r(t),l.d(t,{metadata:()=>a,default:()=>p,frontMatter:()=>n,contentTitle:()=>i,toc:()=>d,assets:()=>o});var a=JSON.parse('{"id":"development/flatpak","title":"Flatpak","description":"Building Flatpak App","source":"@site/docs/development/flatpak.md","sourceDirName":"development","slug":"/development/flatpak","permalink":"/multi-scrobbler/development/flatpak","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/development/flatpak.md","tags":[],"version":"current","sidebarPosition":4,"frontMatter":{"toc_min_heading_level":2,"toc_max_heading_level":5,"sidebar_position":4,"title":"Flatpak","description":"Building Flatpak App"},"sidebar":"tutorialSidebar","previous":{"title":"Source Development/Tutorial","permalink":"/multi-scrobbler/development/dev-source"},"next":{"title":"FAQ","permalink":"/multi-scrobbler/FAQ"}}'),r=l(4848),s=l(8453);let n={toc_min_heading_level:2,toc_max_heading_level:5,sidebar_position:4,title:"Flatpak",description:"Building Flatpak App"},i,o={},d=[{value:"Install Requirements",id:"install-requirements",level:2},{value:"Flatpak and flatpak-builder",id:"flatpak-and-flatpak-builder",level:3},{value:"flatpak-node-generator",id:"flatpak-node-generator",level:4},{value:"Update Project source",id:"update-project-source",level:2},{value:"Generate Sources and Build",id:"generate-sources-and-build",level:2},{value:"Use Setup Script",id:"use-setup-script",level:3},{value:"Manual Setup",id:"manual-setup",level:3},{value:"Use flatpak-node-generator to generate sources",id:"use-flatpak-node-generator-to-generate-sources",level:4},{value:"Build flatpak",id:"build-flatpak",level:4}];function c(e){let t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",h4:"h4",p:"p",pre:"pre",...(0,s.R)(),...e.components},{Details:l}=t;return l||function(e,t){throw Error("Expected "+(t?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h2,{id:"install-requirements",children:"Install Requirements"}),"\n",(0,r.jsx)(t.h3,{id:"flatpak-and-flatpak-builder",children:"Flatpak and flatpak-builder"}),"\n",(0,r.jsxs)(t.p,{children:["Install ",(0,r.jsx)(t.a,{href:"https://flatpak.org/setup/",children:"Flatpak"})]}),"\n",(0,r.jsxs)(t.p,{children:["Install ",(0,r.jsx)(t.a,{href:"https://docs.flatpak.org/en/latest/first-build.html#building-your-first-flatpak",children:"flatpak-builder"})]}),"\n",(0,r.jsx)(t.h4,{id:"flatpak-node-generator",children:(0,r.jsx)(t.a,{href:"https://github.com/flatpak/flatpak-builder-tools/tree/master/node",children:"flatpak-node-generator"})}),"\n",(0,r.jsxs)(t.p,{children:["Requires python 3.7+, ",(0,r.jsx)(t.a,{href:"https://pip.pypa.io/en/stable/",children:"pip"}),"/",(0,r.jsx)(t.a,{href:"https://pypa.github.io/pipx/",children:"pipx"})]}),"\n",(0,r.jsx)(t.h2,{id:"update-project-source",children:"Update Project source"}),"\n",(0,r.jsxs)(t.p,{children:["Set the ",(0,r.jsx)(t.code,{children:"branch"})," ",(0,r.jsx)(t.code,{children:"tag"})," or ",(0,r.jsx)(t.code,{children:"commit"})," to use for MS in the ",(0,r.jsx)(t.code,{children:"source"})," section of ",(0,r.jsx)(t.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/flatpak/io.github.foxxmd.multiscrobbler.yml",children:(0,r.jsx)(t.code,{children:"io.github.foxxmd.multiscrobbler.yml"})})]}),"\n",(0,r.jsx)(t.h2,{id:"generate-sources-and-build",children:"Generate Sources and Build"}),"\n",(0,r.jsx)(t.h3,{id:"use-setup-script",children:"Use Setup Script"}),"\n",(0,r.jsx)(t.p,{children:"A convenience bash script is provided that automates generating offline sources and building the flatpak app for you. This is the recommend method to use."}),"\n",(0,r.jsxs)(t.p,{children:["Located in the project at ",(0,r.jsx)(t.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/flatpak/setup.sh",children:(0,r.jsx)(t.code,{children:"flatpak/setup.sh"})}),", run it from the ",(0,r.jsx)(t.code,{children:"flatpak"})," directory with this syntax:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-shell",children:"./setup.sh -o -b /path/to/flatpak/build/dir\n"})}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:"Args:\n\n-o => Delete and overwrite any existing generated sources\n-b => The absolute path to the directory that should be used for flatpak build/artifacts. If not defined will use `CWD/build`\n-y => Proceed without confirming settings\n"})}),"\n",(0,r.jsx)(t.h3,{id:"manual-setup",children:"Manual Setup"}),"\n",(0,r.jsxs)(t.p,{children:["If you cannot use ",(0,r.jsx)(t.code,{children:"setup.sh"})," follow the below to manually generate sources and build the flatpak app:"]}),"\n",(0,r.jsxs)(l,{children:[(0,r.jsx)("summary",{children:"Instructions"}),(0,r.jsxs)(t.h4,{id:"use-flatpak-node-generator-to-generate-sources",children:["Use ",(0,r.jsx)(t.code,{children:"flatpak-node-generator"})," to generate sources"]}),(0,r.jsxs)(t.p,{children:["First, ",(0,r.jsxs)(t.a,{href:"https://github.com/flatpak/flatpak-builder-tools/issues/354#issuecomment-1478518442",children:["make sure ",(0,r.jsx)(t.code,{children:"node_modules"})," is deleted or empty"]})," for both the project and ",(0,r.jsx)(t.code,{children:"docsite"})," directories."]}),(0,r.jsx)(t.p,{children:"Then, from MS project root:"}),(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-shell",metastring:'title="PROJECT_DIR"',children:"flatpak-node-generator npm package-lock.json\n"})}),(0,r.jsxs)(t.p,{children:["Move ",(0,r.jsx)(t.code,{children:"generated-sources.json"})," into ",(0,r.jsx)(t.code,{children:"PROJECT_DIR/flatpak"})," and rename ",(0,r.jsx)(t.code,{children:"generated-sources.0.json"})]}),(0,r.jsxs)(t.p,{children:["Then, generate ",(0,r.jsx)(t.code,{children:"docsite"})," sources:"]}),(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-shell",metastring:'title="PROJECT_DIR"',children:"flatpak-node-generator npm docsite/package-lock.json\n"})}),(0,r.jsxs)(t.p,{children:["Move ",(0,r.jsx)(t.code,{children:"generated-sources.json"})," into ",(0,r.jsx)(t.code,{children:"PROJECT_DIR/flatpak"})," and rename ",(0,r.jsx)(t.code,{children:"generated-sources.1.json"})]}),(0,r.jsx)(t.h4,{id:"build-flatpak",children:"Build flatpak"}),(0,r.jsxs)(t.p,{children:["In the below command replace ",(0,r.jsx)(t.code,{children:"/home/yourUser/multi-scrobbler-flatpak"})," with a directory to use for storing flatpak build/artifacts."]}),(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-shell",metastring:'title="PROJECT_DIR/flatpak"',children:"flatpak-builder --repo=/home/yourUser/multi-scrobbler-flatpak/repo --state-dir=/home/yourUser/multi-scrobbler-flatpak/state /home/yourUser/multi-scrobbler-flatpak/build io.github.foxxmd.multiscrobbler.yml --force-clean\n"})}),(0,r.jsx)(t.admonition,{type:"info",children:(0,r.jsxs)(t.p,{children:["Append ",(0,r.jsx)(t.code,{children:"--install --user"})," to the above command to have the app installed immediately."]})})]}),"\n",(0,r.jsx)(t.h1,{id:"run-app",children:"Run App"}),"\n",(0,r.jsxs)(t.p,{children:["If built with ",(0,r.jsx)(t.code,{children:"--install --user"})," (default when using ",(0,r.jsx)(t.code,{children:"setup.sh"}),") you can now run MS using the command"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-shell",children:"flatpak run -u io.github.foxxmd.multiscrobbler\n"})})]})}function p(e={}){let{wrapper:t}={...(0,s.R)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}}}]); \ No newline at end of file diff --git a/assets/js/202c4968.2152cfdc.js b/assets/js/202c4968.2152cfdc.js new file mode 100644 index 00000000..4f1caf12 --- /dev/null +++ b/assets/js/202c4968.2152cfdc.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["2068"],{3668(e,t,o){o.r(t),o.d(t,{metadata:()=>i,default:()=>h,frontMatter:()=>c,contentTitle:()=>l,toc:()=>a,assets:()=>d});var i=JSON.parse('{"id":"configuration/sources/koito-source","title":"Koito","description":"Koito (Source)","source":"@site/docs/configuration/sources/koito-source.mdx","sourceDirName":"configuration/sources","slug":"/configuration/sources/koito-source","permalink":"/multi-scrobbler/configuration/sources/koito-source","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/sources/koito-source.mdx","tags":[],"version":"current","frontMatter":{"title":"Koito","description":"Koito (Source)","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Kodi","permalink":"/multi-scrobbler/configuration/sources/kodi"},"next":{"title":"Lastfm (Endpoint)","permalink":"/multi-scrobbler/configuration/sources/lastfm-endpoint"}}'),n=o(4848),r=o(8453);o(8194),o(698),o(2170);var s=o(7419);let c={title:"Koito",description:"Koito (Source)",toc_min_heading_level:2,toc_max_heading_level:5},l,d={},a=[{value:"Configuration",id:"configuration",level:4}];function u(e){let t={a:"a",admonition:"admonition",code:"code",em:"em",h4:"h4",li:"li",p:"p",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components},{Config:o}=t;return o||function(e,t){throw Error("Expected "+(t?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Config",!0),(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)(t.p,{children:["This Source monitors a Koito account's scrobble history and then re-scrobbles discovered tracks to configured ",(0,n.jsx)(t.a,{href:"/configuration/clients",children:"Clients."})]}),"\n",(0,n.jsx)(t.admonition,{title:"Other Uses",type:"tip",children:(0,n.jsxs)(t.p,{children:["To ",(0,n.jsx)(t.em,{children:"scrobble to"})," a Koito server, create a ",(0,n.jsx)(t.a,{href:"/configuration/clients/koito",children:"Koito (Client)"})]})}),"\n",(0,n.jsxs)(t.p,{children:["See the ",(0,n.jsx)(t.a,{href:"/configuration/clients/koito",children:"Koito (Client)"})," configuration for general setup. The only difference for ",(0,n.jsx)(t.strong,{children:"Source"})," configuration:"]}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsxs)(t.li,{children:[(0,n.jsx)(t.a,{href:"/configuration?configType=file#configuration-types",children:"File/AIO config"})," must include ",(0,n.jsx)(t.code,{children:'"configureAs": "source"'})]}),"\n"]}),"\n",(0,n.jsx)(t.h4,{id:"configuration",children:"Configuration"}),"\n",(0,n.jsx)(o,{config:"KoitoSourceConfig",fileContent:s.A,name:"koito",children:(0,n.jsxs)(t.table,{children:[(0,n.jsx)(t.thead,{children:(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,n.jsx)(t.th,{children:"Required?"}),(0,n.jsx)(t.th,{children:"Default"}),(0,n.jsx)(t.th,{children:"Description"})]})}),(0,n.jsxs)(t.tbody,{children:[(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{style:{textAlign:"left"},children:(0,n.jsx)(t.code,{children:"SOURCE_KOITO_TOKEN"})}),(0,n.jsx)(t.td,{children:"Yes"}),(0,n.jsx)(t.td,{}),(0,n.jsx)(t.td,{children:"API Key from your Koito Account"})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{style:{textAlign:"left"},children:(0,n.jsx)(t.code,{children:"SOURCE_KOITO_USER"})}),(0,n.jsx)(t.td,{children:"Yes"}),(0,n.jsx)(t.td,{}),(0,n.jsx)(t.td,{children:"Your Koito username"})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{style:{textAlign:"left"},children:(0,n.jsx)(t.code,{children:"SOURCE_KOITO_URL"})}),(0,n.jsx)(t.td,{children:"No"}),(0,n.jsx)(t.td,{}),(0,n.jsx)(t.td,{children:"The base URL for the Koito server"})]})]})]})})]})}function h(e={}){let{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(u,{...e})}):u(e)}},7419(e,t,o){o.d(t,{A:()=>i});let i='[\n {\n "name": "koito-client",\n "configureAs": "client",\n "data": {\n "token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",\n "username": "admin",\n "url": "http://192.168.0.100:4110" \n }\n },\n {\n "name": "koito-source",\n "configureAs": "source",\n "data": {\n "token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",\n "username": "admin",\n "url": "http://192.168.0.100:4110" \n }\n }\n]\n'}}]); \ No newline at end of file diff --git a/assets/js/202c4968.e63c2f8e.js b/assets/js/202c4968.e63c2f8e.js deleted file mode 100644 index 2a6db1c9..00000000 --- a/assets/js/202c4968.e63c2f8e.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["2068"],{3668(e,t,o){o.r(t),o.d(t,{metadata:()=>n,default:()=>h,frontMatter:()=>c,contentTitle:()=>l,toc:()=>a,assets:()=>d});var n=JSON.parse('{"id":"configuration/sources/koito-source","title":"Koito","description":"This Source monitors a Koito account\'s scrobble history and then re-scrobbles discovered tracks to configured Clients.","source":"@site/docs/configuration/sources/koito-source.mdx","sourceDirName":"configuration/sources","slug":"/configuration/sources/koito-source","permalink":"/multi-scrobbler/configuration/sources/koito-source","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/sources/koito-source.mdx","tags":[],"version":"current","frontMatter":{"title":"Koito","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Kodi","permalink":"/multi-scrobbler/configuration/sources/kodi"},"next":{"title":"Lastfm (Endpoint)","permalink":"/multi-scrobbler/configuration/sources/lastfm-endpoint"}}'),i=o(4848),r=o(8453);o(8194),o(698),o(2170);var s=o(7419);let c={title:"Koito",toc_min_heading_level:2,toc_max_heading_level:5},l,d={},a=[{value:"Configuration",id:"configuration",level:4}];function u(e){let t={a:"a",admonition:"admonition",code:"code",em:"em",h4:"h4",li:"li",p:"p",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components},{Config:o}=t;return o||function(e,t){throw Error("Expected "+(t?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Config",!0),(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(t.p,{children:["This Source monitors a Koito account's scrobble history and then re-scrobbles discovered tracks to configured ",(0,i.jsx)(t.a,{href:"/configuration/clients",children:"Clients."})]}),"\n",(0,i.jsx)(t.admonition,{title:"Other Uses",type:"tip",children:(0,i.jsxs)(t.p,{children:["To ",(0,i.jsx)(t.em,{children:"scrobble to"})," a Koito server, create a ",(0,i.jsx)(t.a,{href:"/configuration/clients/koito",children:"Koito (Client)"})]})}),"\n",(0,i.jsxs)(t.p,{children:["See the ",(0,i.jsx)(t.a,{href:"/configuration/clients/koito",children:"Koito (Client)"})," configuration for general setup. The only difference for ",(0,i.jsx)(t.strong,{children:"Source"})," configuration:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.a,{href:"/configuration?configType=file#configuration-types",children:"File/AIO config"})," must include ",(0,i.jsx)(t.code,{children:'"configureAs": "source"'})]}),"\n"]}),"\n",(0,i.jsx)(t.h4,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsx)(o,{config:"KoitoSourceConfig",fileContent:s.A,name:"koito",children:(0,i.jsxs)(t.table,{children:[(0,i.jsx)(t.thead,{children:(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,i.jsx)(t.th,{children:"Required?"}),(0,i.jsx)(t.th,{children:"Default"}),(0,i.jsx)(t.th,{children:"Description"})]})}),(0,i.jsxs)(t.tbody,{children:[(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{style:{textAlign:"left"},children:(0,i.jsx)(t.code,{children:"SOURCE_KOITO_TOKEN"})}),(0,i.jsx)(t.td,{children:"Yes"}),(0,i.jsx)(t.td,{}),(0,i.jsx)(t.td,{children:"API Key from your Koito Account"})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{style:{textAlign:"left"},children:(0,i.jsx)(t.code,{children:"SOURCE_KOITO_USER"})}),(0,i.jsx)(t.td,{children:"Yes"}),(0,i.jsx)(t.td,{}),(0,i.jsx)(t.td,{children:"Your Koito username"})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{style:{textAlign:"left"},children:(0,i.jsx)(t.code,{children:"SOURCE_KOITO_URL"})}),(0,i.jsx)(t.td,{children:"No"}),(0,i.jsx)(t.td,{}),(0,i.jsx)(t.td,{children:"The base URL for the Koito server"})]})]})]})})]})}function h(e={}){let{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(u,{...e})}):u(e)}},7419(e,t,o){o.d(t,{A:()=>n});let n='[\n {\n "name": "koito-client",\n "configureAs": "client",\n "data": {\n "token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",\n "username": "admin",\n "url": "http://192.168.0.100:4110" \n }\n },\n {\n "name": "koito-source",\n "configureAs": "source",\n "data": {\n "token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",\n "username": "admin",\n "url": "http://192.168.0.100:4110" \n }\n }\n]\n'}}]); \ No newline at end of file diff --git a/assets/js/2a24ad03.37ab5a42.js b/assets/js/2a24ad03.37ab5a42.js new file mode 100644 index 00000000..b5f39939 --- /dev/null +++ b/assets/js/2a24ad03.37ab5a42.js @@ -0,0 +1,13 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["899"],{7265(e,t,i){i.d(t,{Ay:()=>d,RM:()=>c});var l=i(4848),s=i(8453),n=i(2170),r=i.n(n);let c=[];function o(e){let t={a:"a",code:"code",li:"li",p:"p",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsxs)(t.p,{children:["A ",(0,l.jsx)(t.strong,{children:"Source"})," that monitors active listening uses one of two metrics to determine if a Song is scrobbable based on your listening:"]}),"\n",(0,l.jsxs)(t.ul,{children:["\n",(0,l.jsxs)(t.li,{children:[(0,l.jsx)(t.strong,{children:"percent"})," of track listened to (50% or more)"]}),"\n",(0,l.jsxs)(t.li,{children:["or amount of time (",(0,l.jsx)(t.strong,{children:"duration"}),") track was listened to (4 minutes or more)"]}),"\n"]}),"\n",(0,l.jsxs)(t.p,{children:["These default values are based on ",(0,l.jsx)(t.a,{href:"https://www.last.fm/api/scrobbling#when-is-a-scrobble-a-scrobble",children:"last.fm's scrobble guidance."})]}),"\n",(0,l.jsxs)(t.p,{children:["These values can be customized in the ",(0,l.jsx)(t.code,{children:"options"})," of Source's ",(0,l.jsx)(t.a,{href:"/configuration#configuration-types",children:"file or aio config"}),":"]}),"\n",(0,l.jsx)(r(),{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 d(e={}){let{wrapper:t}={...(0,s.R)(),...e.components};return t?(0,l.jsx)(t,{...e,children:(0,l.jsx)(o,{...e})}):o(e)}},9052(e,t,i){i.r(t),i.d(t,{metadata:()=>l,default:()=>j,frontMatter:()=>d,contentTitle:()=>h,toc:()=>a,assets:()=>x});var l=JSON.parse('{"id":"configuration/sources/sources","title":"Overview","description":"Scrobble Sources","source":"@site/docs/configuration/sources/sources.mdx","sourceDirName":"configuration/sources","slug":"/configuration/sources/","permalink":"/multi-scrobbler/configuration/sources/","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/sources/sources.mdx","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"sidebar_position":2,"title":"Overview","description":"Scrobble Sources","toc_max_heading_level":4},"sidebar":"tutorialSidebar","previous":{"title":"Teal.fm","permalink":"/multi-scrobbler/configuration/clients/tealfm"},"next":{"title":"Azuracast","permalink":"/multi-scrobbler/configuration/sources/azuracast"}}'),s=i(4848),n=i(8453),r=i(7265),c=i(8194),o=i(698);let d={sidebar_position:2,title:"Overview",description:"Scrobble Sources",toc_max_heading_level:4},h,x={},a=[{value:"Sources",id:"sources",level:2},{value:"Features",id:"features",level:2},{value:"Types of Sources",id:"types-of-sources",level:3},{value:"By Communication Method",id:"by-communication-method",level:4},{value:"By Data Source of Truth",id:"by-data-source-of-truth",level:4},{value:"Multi-Device/User",id:"multi-deviceuser",level:3},{value:"Limiting Scrobble Destination",id:"limiting-scrobble-destination",level:3},{value:"Scrobble Thresholds",id:"scrobble-thresholds",level:3},...r.RM,{value:"Should Scrobble Behavior",id:"should-scrobble-behavior",level:3}];function f(e){let t={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,n.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(t.p,{children:["A ",(0,s.jsx)(t.strong,{children:"Source"})," is a data source that contains information about tracks you are playing like a music player or platform. Examples are ",(0,s.jsx)(t.strong,{children:"Spotify, Jellyfin, Plex, Youtube Music, Airsonic"}),", etc..."]}),"\n",(0,s.jsx)(t.h2,{id:"sources",children:"Sources"}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{style:{textAlign:"left"},children:"Name"}),(0,s.jsx)(t.th,{style:{textAlign:"left"},children:"Networking"}),(0,s.jsx)(t.th,{style:{textAlign:"left"},children:"Scrobble SOT"}),(0,s.jsx)(t.th,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"#multi-deviceuser",children:"Multi Device/User"})}),(0,s.jsx)(t.th,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"#limiting-scrobble-destination",children:"Scrobble Destination"})}),(0,s.jsx)(t.th,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"#scrobble-thresholds",children:"Thresholds"})}),(0,s.jsx)(t.th,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"#should-scrobble-behavior",children:"Should Scrobble"})})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/azuracast",children:"Azuracast"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/deezer",children:"Deezer"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/google-cast",children:"Google Cast (Chromecast)"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/icecast",children:"Icecast"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/jellyfin",children:"Jellyfin"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/jriver",children:"JRiver"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/kodi",children:"Kodi"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/koito-source",children:"Koito"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/lastfm-source",children:"Last.fm"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/librefm-source",children:"Libre.fm"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/lastfm-endpoint",children:"Last.fm (Endpoint)"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=ingress#by-communication-method",children:"Ingress"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/listenbrainz-source",children:"ListenBrainz"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/listenbrainz-endpoint",children:"ListenBrainz (Endpoint)"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=ingress#by-communication-method",children:"Ingress"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/maloja",children:"Maloja"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/mopidy",children:"Mopidy"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/mpd",children:"MPD (Music Player Daemon)"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/musikcube",children:"Musikcube"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/plex",children:"Plex"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/rocksky-source",children:"Rocksky"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/sonos",children:"Sonos"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/spotify",children:"Spotify"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/subsonic",children:"Subsonic-compatible APIs"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/tealfm-source",children:"teal.fm"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/webscrobbler",children:"WebScrobbler"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Ingress"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/vlc",children:"VLC"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/yamaha-musiccast",children:"Yamaha MusicCast"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/yandex-music",children:"Yandex Music"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"}}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/youtube-music",children:"Youtube Music"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]})]})]}),"\n",(0,s.jsx)(t.h2,{id:"features",children:"Features"}),"\n",(0,s.jsx)(t.h3,{id:"types-of-sources",children:"Types of Sources"}),"\n",(0,s.jsx)(t.p,{children:"The Sources implemented in multi-scrobbler can be broken down into two categories."}),"\n",(0,s.jsx)(t.h4,{id:"by-communication-method",children:"By Communication Method"}),"\n",(0,s.jsx)(t.p,{children:"How does this Source get data from the service/application?"}),"\n",(0,s.jsxs)(c.A,{groupId:"sourceComm",queryString:!0,children:[(0,s.jsx)(o.A,{value:"active",label:"Active",children:(0,s.jsx)(t.p,{children:"The MS Source makes network requests to the service which returns a response with data MS can use to monitor listening activity or scrobbles."})}),(0,s.jsxs)(o.A,{value:"ingress",label:"Ingress",children:[(0,s.jsx)(t.p,{children:"The MS Source waits for the service/application to send data to multi-scrobbler."}),(0,s.jsx)(t.p,{children:"These types of Sources require that multi-scrobbler is accessible to the service, generally through an open port or reverse proxy."})]})]}),"\n",(0,s.jsx)(t.h4,{id:"by-data-source-of-truth",children:"By Data Source of Truth"}),"\n",(0,s.jsx)(t.p,{children:"How does MS determine if/when a song has been played and is scrobbable?"}),"\n",(0,s.jsxs)(c.A,{groupId:"sot",queryString:!0,children:[(0,s.jsxs)(o.A,{value:"activity",label:"Listening Activity",children:[(0,s.jsx)(t.p,{children:"These Sources expose some kind of real-time listening data for users, like:"}),(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsx)(t.li,{children:"player state (paused, stopped, playing)"}),"\n",(0,s.jsx)(t.li,{children:"player position (1:50/2:40)"}),"\n",(0,s.jsx)(t.li,{children:"current listened duration (Track A playing for 1m:20s)"}),"\n"]}),(0,s.jsxs)(t.p,{children:["For theses Sources multi-scrobbler keeps track of the real-time state of your activity and scrobbles a track after it meets certain ",(0,s.jsx)(t.a,{href:"#scrobble-thresholds",children:"scrobble thresholds"}),"."]})]}),(0,s.jsxs)(o.A,{value:"history",label:"Listening History",children:[(0,s.jsx)(t.p,{children:'Some Sources expose their own version of "this track has been scrobbled."'}),(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsx)(t.li,{children:'Listenbrainz, Koito, and Last.fm are all scrobble services that have "definitive" histories of scrobbled tracks.'}),"\n"]}),(0,s.jsx)(t.p,{children:"For these Sources multi-scrobbler monitors the listening history data and scrobbles when it sees a new entry in the list."})]})]}),"\n",(0,s.jsx)(t.h3,{id:"multi-deviceuser",children:"Multi-Device/User"}),"\n",(0,s.jsx)(t.p,{children:"Some Sources report which User is playing a track and/or on what device the track is being played. These Sources can be filtered with configuration to only monitor activity for specific devices or users."}),"\n",(0,s.jsx)(t.h3,{id:"limiting-scrobble-destination",children:"Limiting Scrobble Destination"}),"\n",(0,s.jsxs)(t.p,{children:["To limit which Clients a Source will scrobble to use the ",(0,s.jsx)(t.code,{children:"clients"})," property in a Source's ",(0,s.jsx)(t.a,{href:"/configuration?configType=file#configuration-types",children:"File/AIO configuration"})," to specify the ",(0,s.jsx)(t.strong,{children:"name"})," property of the Clients that it should scrobble to:"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json5",metastring:'title="jellyfin.json"',children:'{\n "name": "MyJellyfin",\n "clients": ["myMaloja","koitoA","listenbrainzFoo"]\n "data": {\n // ...\n },\n}\n'})}),"\n",(0,s.jsx)(t.admonition,{type:"note",children:(0,s.jsxs)(t.p,{children:["If ",(0,s.jsx)(t.code,{children:"clients"})," is empty (",(0,s.jsx)(t.code,{children:'"clients": []'}),") or is omitted then the default is for the Source to scrobble to ",(0,s.jsx)(t.strong,{children:"all"})," configured Clients."]})}),"\n",(0,s.jsxs)(t.admonition,{type:"note",children:[(0,s.jsxs)(t.p,{children:["If a Client is configured using ",(0,s.jsx)(t.a,{href:"/configuration?configType=env#configuration-types",children:"ENV"})," then it is given a generated name. Check the multi-scrobbler Dashboard to find their name in a subheader under the Client type. Example:"]}),(0,s.jsx)(t.p,{children:(0,s.jsx)(t.img,{alt:"Client Name Subheader",src:i(4400).A+"",width:"177",height:"101"})}),(0,s.jsxs)(t.p,{children:["Otherwise, use the ",(0,s.jsx)(t.code,{children:"name"})," property you gave the client in it's File/AIO config."]})]}),"\n",(0,s.jsx)(t.h3,{id:"scrobble-thresholds",children:"Scrobble Thresholds"}),"\n",(0,s.jsx)(r.Ay,{filename:"spotify"}),"\n",(0,s.jsxs)(t.p,{children:["These can also be configured ",(0,s.jsx)(t.strong,{children:"globally"})," for all Sources using ENVs:"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{children:"SOURCE_SCROBBLE_DURATION=40\nSOURCE_SCROBBLE_PERCENT=20\n"})}),"\n",(0,s.jsx)(t.h3,{id:"should-scrobble-behavior",children:"Should Scrobble Behavior"}),"\n",(0,s.jsx)(t.admonition,{type:"note",children:(0,s.jsx)(t.p,{children:"This feature is currently available only for some Sources. Check each Source's documentation to see if it supported."})}),"\n",(0,s.jsxs)(t.p,{children:["For ",(0,s.jsx)(t.em,{children:"most"})," Sources multi-scrobbler can determine if a user is listening and scrobble automatically. For Sources which multi-scrobbler cannot do this or require additional configuration, a user can indicate to MS that it should or should not scrobble. This action can be taken on the Web Dashboard by clicking the link next to ",(0,s.jsx)(t.strong,{children:"Should Scrobble"})," found in each Source's status card."]}),"\n",(0,s.jsxs)(t.p,{children:["Additionally, for these Sources, the default behavior for Scrobbling can be explicitly set by adding the following value to a Source's ",(0,s.jsx)(t.code,{children:"options"})," in ",(0,s.jsx)(t.a,{href:"/configuration?configType=file#configuration-types",children:"file or aio config"}),":"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json5",metastring:'title="azuracast.json"',children:'{\n "data": {\n // ...\n },\n "options": {\n // set to true to always scrobble automatically\n // set to false to never scrobble automatically\n "systemScrobble": true\n }\n}\n'})})]})}function j(e={}){let{wrapper:t}={...(0,n.R)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(f,{...e})}):f(e)}},4400(e,t,i){i.d(t,{A:()=>l});let l="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALEAAABlCAYAAAAChgL8AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAPA0lEQVR4nO3ceVxV1d7H8c8+A8ggs8iQoqIiKAKiOODUY5qVkd6s6y2v5pBlmWmpWWmKtxK1wbQnLYfM7MnsajfMDFBURBEVFBAZxAlBUBAQDjKevZ8/VByyWyjI2bXerxd/wBnW2ofvWfu3195rSx7ePRUEQcU0Td0BQbhXIsSC6okQC6onQiyongixoHoixILqiRALqidCLKieCLGgeiLEguqJEAuqJ0IsqJ4IsaB6IsSC6okQC6onQiyongixoHq6e3mxi4tbQ/VDEO6aGIkF1RMhFlRPhFhQvXsIsVgkLZiGuwyxIjIsmIy7C7HIsGBCRDkhqN5dhFgR8RVMihiJBdUTU2yC6okQC6onQiyongixoHr1D7E4nhNMjBiJBdUTIW4k+g4hhC0P439fDKS51NS9uTu6dsMIWx7Gpy8GYmvC2/CXDLFk3Yo+j4Yw9pkQQgJbom/wBmx48NmR9Pd2oyQjA0ODlGB63Hs8zKghXtjfp0DVZqdxxtqTwBGjGOFhulG5p5Ud9aXv/zo75gWjSVnDU9O3kd8E9bVkG8is5bMY/oAeCYWC8DNsT7jQoG1oHHsxLMgKDLFExRtuOYzQ2HXgkZEhPNHPm7bOzdFVFHIi6QA/bvwP2zPKkAEwY9C89bw3AGLeH8esHRXQvA+vhk6iv1kBrnkv83GKsUH7fEe1Z9i5O4d/jm3Do0Pas+GLTGobv9V6M92v1z3SOD/KZxFb2L95Mj3rvqoSjv2G8Zi7jpqsXwh9I5TZ36VT06AtS9j26EFXc6hMPkxCxY1H9B6DCVv5PnOeCabrA7boq6vR2LjQdcAI5ixbzPxBzr/9DzGkEP7DAfbt+InIk3KD9hig2YMziYneQsyCfljU/dXI6UNHOC9rce8ViKe2wZttEPd1JG56Es3trNFIMucP7ybyUCYNP57p6ezXETPJSHJSBuXXh2FdG0bPnkA/Zw2XU7awcMlmYnIqkCxdCX72Zeb83YeHpk7gwNEwfr50h7dViohdtYTYBu/vf1eblUpy2RO0cvehq6NExkXTm54ywRBrcfR7hEmjBxPs7YKNUkbeqRS2b/iGDYcKr+3OJCzb9WfiuGH8T9dWOJrXUpKTRvSmDXwedRZ54Ewi5/bGDMB+MJ9EPkTp7khOBj9MwLUCuPWoMPaNqiVh2WRe+cmNGd/OZ4T1ARbOTiNocgg9Pay5khHBknc3cMztcV6fPIy+bS0pz07gu+Wr2JBSeufZRo0rXu0s0cjFZGZd4vqYaRH4GCM7mKEUxbJk3jfsKbn6auVKHntXL2VVp0953d+fR4Lt+SXc8Ov31fky69v5/M0mnaXPzWFjngLoeaDPcJ4fNZAgT0csqgpIjYtk9ZptHCmSAQnn4aFsnupN9tehLC4dzPRRPWhnVU1u8g5WfPwtMRckekz7nE9C7K/uBfpOZ1f0FGIXTWBmhAGlJpuMs0Ye69KaTm11cLFh91sNweRCbOH3Tz5d/DhtKOZ4/D7ia10I7NOfF95ti8WrM1mRXoNk15s3F0/lIbsrnD64l30Flvj0C+LpWZ441rzGgrxUIg61ZnB3d5opF0nceYxTGacpvLIHpVs/urlokPOPEZGUz6nsqrowSnovxr/uSVHOSdItfAn0DeHtN1pyuk17zNIzOKrvQpBnMJPny+SOW0p06R1irHXCraUGjBfJvXA9wlraB/hgr5Ep3BtFTMltr1MK2fLWOH7RS8jVlch/6N8i4TLkNVbOCsKhMp+khEMYHLwIGvocn/i04LUpX3K4/MZz7fo9T6hFBVmpqdDZD6+gEYTONTB+ajiFafuIatuLwb5OUJjOzoQcUnNrrn4uSgnn8ytRulrg6mqLhkIavpi5NyYWYgmLmtNs+/5ndCmbWR9fgoyWTuM/Ys3oBxg0sC2r0jORvLoRZCdRe/x7Zr4dTq4CZuEDGRtsRXY+1Kb/zMIPwf3rifgbkvhqyQriawF2sGd0K74e70HurvW8uyrrajlR9ymYkf71DN6MKkGx9Gf22jkM7+bPiaVTmbK1ENmsAy99/j5jWvnRu5OW6IN3OMzRW9HcXAKlnNK6aQkNTs4OSMjknsu7YwkjV1VgqKrHR2Xuy5gJPXCsyWL19HdYc6IKJBv6z/qAsCFDmDh0G4mbL9a1b2+RwcwXVrCvVEHj+gifrJ5Ij4696eseztcRa1lY7ciDvk6Qvp33F+3lRikvYyirQMEC6+aW9ejg/WNiIVYoOr6HDcdjMLdrSQe/bri7uNHNzxYJCSsrSyTAmJdDjlHBx2sYb07RsD0xk7T0ONasr6obJe7qiNWYS3Lq5asjUEUeZy/K4FRM5sniq+9bfZ4z543Q2hyLZr83zyUh3fJb3SY2CG07P7o7aDBmHeXQZWucW1gDcDIhjbLBfekU0AmruhArlCcf4uC1PYd8IZUjuTI92llj/wcmsSWNCU8SY3IhBr17MFNnjSOkiz3mEhgrism5fGsk5extLFjkwIwJgwgcPobuIwClmoJjO1m+ZB2ROXdbt8nIt4dMUVB+tf+8NaC3qC6l+IoC1jY42mnAIAMyBReLUXDGvbUL2jvskjU6c8z1oNRUU/kH5rE0zW2wkUDn9RRfbHzqV4/X2NpiI1E381JbffNnIiPL17bjd/Opw87OGgmZyyVlJnnVgWmFWHJk2LSXGdnZSMqmD1n6YxInLlzB9vF5bJnme9MTazgbvZZXotdj49YGH5/ODHhiBE/4DuWt6edInvELF3+zkUZmzOPMOSN0ccPTQw85VYCRrKPHKRnpglPfwfT7KpXom+tiyYkRiz5lhj8kLHuZqT+W/W4ziqGMMsDm4j4+Wb6H3Nu+FXLZOYoUaH6v26N1xdNDjySXcPbcbxzMNjHTmifWtqCVqxmSnMuenw5wPL+cGmzx929907dNwj7wSea+9y8WjfKg6nwWB3b8yNJ1B7gkS+gdHLDVALKMAkg6M/T3cyuVQpKOXcAoWeLr166u3xWHt7H5ZDWSQ19mvvMP+ro3QwKkZi3pM24ak/zMUEoT2Lq3CPnGoSZanfaOo37tqWQSixQkezecSo6zL+4wsXEJJBmc8PF0xqqmkvqU2IoMigKY6TG7qUGNozcBrTQoFRkcOXEfTrDchSYZibXtH2fxp/1uOckgF+1nSeguUjMNyG4dGPevN2h95BLN2nfD311zUy2pUFas0CbAB5+e81jrn0j6ZSs69vDHSaokbWccp4wgl+aRXaoQ6NCLGR/Z8FjsJuZuPHEfts5IZuxBcp8egXuv7nh9kUZqLVB7mvXvf4n3BxMJ9h/JkvUjqDRUgKUVFloJpSqXrR+uIapIAWQKC4uRcab7mLdYYLmG0PDbmqlMZt2agwTPDGL0kqUEJGZwQeNMl4D2OGvz2Zy2hx316HXt+VxyZWgb8AzLFvUi7t9LWXmwAseeQXTWgWH/AQ5fabhPqSE1yUissXSmo3dHOt/808EZK8qIXraYz6JPYnAO4NGhQbTOD2fe2mO3BL721A+8PmM1PyQWYdulNw8P6IRNwVH+vXQ+r31z+upzq5NZs2QLcTnVOHj5EuDbCsf7dc1BRjRbM2vQuAUztMuNKzNqzkbwxgtzWbgxjtTcMmRzcyjLJ2XPZuZPmc3C2KJrtbKRlO/X8X1yIbWOHegV5InTr/oukxfxES+8s4mdp4x4dO9Ff/8WVKbt5rM35/JxQnm9dv3GrHDCVsdx0mBFez9vAj1boJWcGfSQD+bKJXZuO8xlU6wlAMnDu2f9uqZcX+0s4+raqjH69CcgYRP8Cv8XOgDd7g94+r047jSlbOp0Hf7BV5+NxO34OsZO30q2qU0QXyNCLKieaR3YCcJdECEWVE+EWFA9EWJB9USIBdUTIRZUT4RYUD0RYkH1VBxiCefhC9i18knaXN8KjSP9XpzPdz98S/Ty4bRV8dYJf5xpXYp5j/R+TzLriebsWhTKLyfzOGuip0mFhvXnCrGjA9aXkomKTee4Kd4gQWgU9zHEV1fefvdQGov3PcAzf/PDtSaL7z9cznab4cye+CBd7Ks5vXcj734cyQklgDc3zMD5y5d57eeSa9draPEa/xFfBO3luZc2U7cOUuvJ5DVhjPW4emOEVZEhlEeFMXRhEv3nrWNS0Vo2WQ5mTLA7muw9fPjeD1Q98gLTQnxxUS5wYNNK3tuYpsqLdIQmqIm1bfoSXP0TMye8wqtbagiZs4hFg4tYOXUSIS99SZb3OGYPd0dTdYzIGAP+A4NuXEKpbcug/s5kRO+7tVQwnmHdq8/z1LIjVF2MYtaoCYxcdvTa5ZsaWvbthd3Ojxk75m1W5HXj7WULGK3dxrTRE3l2cRKuo6cw1ttE7wwi/K77HmKlKJ6NP6aRX1pM6tZojprpOR7+E8mXrlByZj9b4y/TtmNr9NSQHL2fws596H/tYlqdVzAPtsgkYnf+bWvUjFSUlnDJUINcXU5RUQnF5devQFaoTNjOhsP5lJScJXJbIhXWuWzbmESewUBOfBTR5+zp2N72t9fNCSbtvodYvlJ+de0kgCxjVKqprLyxHzcajUg6HRqgJj2WnRc6MijYEQ06Og/siX1yDLsL6rPfV6gov3JjmbwsIytVN7VpxGgEvV4nQqxSpj0JZTxN5K48fAb2oqW5F4P6WnMo6iBFonYVbmLaIUbmzK69nGwfxMA+QfQ2TyQi3jSXjQtNx8RDDHLufiJPtOPpCb0xj4shvvzGY1WVVUgOrfBpY4+FqAX+skw+xCgF7NqZib2LQsyOlJtur6RQGh/J1uIAZi+fwhAHkeK/KhWssZNwfPRtNo06x7TxX5EiTmIItzHpM3ZScxe8PL158ilvsiO+EmfhhDsy4RBLtBg4iRVT2pO//xvmbT7XCDfEFv4MVFBOCMJ/Z/oHdoLwOxq1nOhWUki34oLGbKJRJNq3INHOqam7IfxBjRvi4gImnklvzCYaxWoQIVYRUU4IqteoI3GifQtWN2YDjSTRvkVTd0Goh8YNsZ2T2C0LjU6UE4LqiRALqidCLKieCLGgeiLEguqJEAuqJ0IsqN49zRN3D/BrqH4Iwl0TI7GgeiLEguqJEAuqJ0IsqJ4IsaB6IsSC6okQC6onQiyongixoHoixILqiRALqidCLKieCLGgeiLEgur9P7w+WrWPSpyoAAAAAElFTkSuQmCC"}}]); \ No newline at end of file diff --git a/assets/js/2a24ad03.4089e2ea.js b/assets/js/2a24ad03.4089e2ea.js deleted file mode 100644 index e5deeb4b..00000000 --- a/assets/js/2a24ad03.4089e2ea.js +++ /dev/null @@ -1,13 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["899"],{7265(e,t,i){i.d(t,{Ay:()=>d,RM:()=>c});var l=i(4848),s=i(8453),n=i(2170),r=i.n(n);let c=[];function o(e){let t={a:"a",code:"code",li:"li",p:"p",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsxs)(t.p,{children:["A ",(0,l.jsx)(t.strong,{children:"Source"})," that monitors active listening uses one of two metrics to determine if a Song is scrobbable based on your listening:"]}),"\n",(0,l.jsxs)(t.ul,{children:["\n",(0,l.jsxs)(t.li,{children:[(0,l.jsx)(t.strong,{children:"percent"})," of track listened to (50% or more)"]}),"\n",(0,l.jsxs)(t.li,{children:["or amount of time (",(0,l.jsx)(t.strong,{children:"duration"}),") track was listened to (4 minutes or more)"]}),"\n"]}),"\n",(0,l.jsxs)(t.p,{children:["These default values are based on ",(0,l.jsx)(t.a,{href:"https://www.last.fm/api/scrobbling#when-is-a-scrobble-a-scrobble",children:"last.fm's scrobble guidance."})]}),"\n",(0,l.jsxs)(t.p,{children:["These values can be customized in the ",(0,l.jsx)(t.code,{children:"options"})," of Source's ",(0,l.jsx)(t.a,{href:"/configuration#configuration-types",children:"file or aio config"}),":"]}),"\n",(0,l.jsx)(r(),{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 d(e={}){let{wrapper:t}={...(0,s.R)(),...e.components};return t?(0,l.jsx)(t,{...e,children:(0,l.jsx)(o,{...e})}):o(e)}},9052(e,t,i){i.r(t),i.d(t,{metadata:()=>l,default:()=>j,frontMatter:()=>d,contentTitle:()=>h,toc:()=>a,assets:()=>x});var l=JSON.parse('{"id":"configuration/sources/sources","title":"Overview","description":"A Source is a data source that contains information about tracks you are playing like a music player or platform. Examples are Spotify, Jellyfin, Plex, Youtube Music, Airsonic, etc...","source":"@site/docs/configuration/sources/sources.mdx","sourceDirName":"configuration/sources","slug":"/configuration/sources/","permalink":"/multi-scrobbler/configuration/sources/","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/sources/sources.mdx","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"sidebar_position":2,"title":"Overview","toc_max_heading_level":4},"sidebar":"tutorialSidebar","previous":{"title":"Teal.fm","permalink":"/multi-scrobbler/configuration/clients/tealfm"},"next":{"title":"Azuracast","permalink":"/multi-scrobbler/configuration/sources/azuracast"}}'),s=i(4848),n=i(8453),r=i(7265),c=i(8194),o=i(698);let d={sidebar_position:2,title:"Overview",toc_max_heading_level:4},h,x={},a=[{value:"Sources",id:"sources",level:2},{value:"Features",id:"features",level:2},{value:"Types of Sources",id:"types-of-sources",level:3},{value:"By Communication Method",id:"by-communication-method",level:4},{value:"By Data Source of Truth",id:"by-data-source-of-truth",level:4},{value:"Multi-Device/User",id:"multi-deviceuser",level:3},{value:"Limiting Scrobble Destination",id:"limiting-scrobble-destination",level:3},{value:"Scrobble Thresholds",id:"scrobble-thresholds",level:3},...r.RM,{value:"Should Scrobble Behavior",id:"should-scrobble-behavior",level:3}];function f(e){let t={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,n.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(t.p,{children:["A ",(0,s.jsx)(t.strong,{children:"Source"})," is a data source that contains information about tracks you are playing like a music player or platform. Examples are ",(0,s.jsx)(t.strong,{children:"Spotify, Jellyfin, Plex, Youtube Music, Airsonic"}),", etc..."]}),"\n",(0,s.jsx)(t.h2,{id:"sources",children:"Sources"}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{style:{textAlign:"left"},children:"Name"}),(0,s.jsx)(t.th,{style:{textAlign:"left"},children:"Networking"}),(0,s.jsx)(t.th,{style:{textAlign:"left"},children:"Scrobble SOT"}),(0,s.jsx)(t.th,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"#multi-deviceuser",children:"Multi Device/User"})}),(0,s.jsx)(t.th,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"#limiting-scrobble-destination",children:"Scrobble Destination"})}),(0,s.jsx)(t.th,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"#scrobble-thresholds",children:"Thresholds"})}),(0,s.jsx)(t.th,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"#should-scrobble-behavior",children:"Should Scrobble"})})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/azuracast",children:"Azuracast"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/deezer",children:"Deezer"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/google-cast",children:"Google Cast (Chromecast)"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/icecast",children:"Icecast"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/jellyfin",children:"Jellyfin"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/jriver",children:"JRiver"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/kodi",children:"Kodi"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/koito-source",children:"Koito"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/lastfm-source",children:"Last.fm"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/librefm-source",children:"Libre.fm"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/lastfm-endpoint",children:"Last.fm (Endpoint)"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=ingress#by-communication-method",children:"Ingress"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/listenbrainz-source",children:"ListenBrainz"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/listenbrainz-endpoint",children:"ListenBrainz (Endpoint)"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=ingress#by-communication-method",children:"Ingress"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/maloja",children:"Maloja"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/mopidy",children:"Mopidy"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/mpd",children:"MPD (Music Player Daemon)"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/musikcube",children:"Musikcube"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/plex",children:"Plex"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/rocksky-source",children:"Rocksky"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/sonos",children:"Sonos"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/spotify",children:"Spotify"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/subsonic",children:"Subsonic-compatible APIs"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/tealfm-source",children:"teal.fm"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/webscrobbler",children:"WebScrobbler"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Ingress"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/vlc",children:"VLC"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/yamaha-musiccast",children:"Yamaha MusicCast"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/yandex-music",children:"Yandex Music"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=activity#by-data-source-of-truth",children:"Activity"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"}}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"/configuration/sources/youtube-music",children:"Youtube Music"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sourceComm=active#by-communication-method",children:"Active"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:(0,s.jsx)(t.a,{href:"./?sot=history#by-data-source-of-truth",children:"History"})}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"}),(0,s.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]})]})]}),"\n",(0,s.jsx)(t.h2,{id:"features",children:"Features"}),"\n",(0,s.jsx)(t.h3,{id:"types-of-sources",children:"Types of Sources"}),"\n",(0,s.jsx)(t.p,{children:"The Sources implemented in multi-scrobbler can be broken down into two categories."}),"\n",(0,s.jsx)(t.h4,{id:"by-communication-method",children:"By Communication Method"}),"\n",(0,s.jsx)(t.p,{children:"How does this Source get data from the service/application?"}),"\n",(0,s.jsxs)(c.A,{groupId:"sourceComm",queryString:!0,children:[(0,s.jsx)(o.A,{value:"active",label:"Active",children:(0,s.jsx)(t.p,{children:"The MS Source makes network requests to the service which returns a response with data MS can use to monitor listening activity or scrobbles."})}),(0,s.jsxs)(o.A,{value:"ingress",label:"Ingress",children:[(0,s.jsx)(t.p,{children:"The MS Source waits for the service/application to send data to multi-scrobbler."}),(0,s.jsx)(t.p,{children:"These types of Sources require that multi-scrobbler is accessible to the service, generally through an open port or reverse proxy."})]})]}),"\n",(0,s.jsx)(t.h4,{id:"by-data-source-of-truth",children:"By Data Source of Truth"}),"\n",(0,s.jsx)(t.p,{children:"How does MS determine if/when a song has been played and is scrobbable?"}),"\n",(0,s.jsxs)(c.A,{groupId:"sot",queryString:!0,children:[(0,s.jsxs)(o.A,{value:"activity",label:"Listening Activity",children:[(0,s.jsx)(t.p,{children:"These Sources expose some kind of real-time listening data for users, like:"}),(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsx)(t.li,{children:"player state (paused, stopped, playing)"}),"\n",(0,s.jsx)(t.li,{children:"player position (1:50/2:40)"}),"\n",(0,s.jsx)(t.li,{children:"current listened duration (Track A playing for 1m:20s)"}),"\n"]}),(0,s.jsxs)(t.p,{children:["For theses Sources multi-scrobbler keeps track of the real-time state of your activity and scrobbles a track after it meets certain ",(0,s.jsx)(t.a,{href:"#scrobble-thresholds",children:"scrobble thresholds"}),"."]})]}),(0,s.jsxs)(o.A,{value:"history",label:"Listening History",children:[(0,s.jsx)(t.p,{children:'Some Sources expose their own version of "this track has been scrobbled."'}),(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsx)(t.li,{children:'Listenbrainz, Koito, and Last.fm are all scrobble services that have "definitive" histories of scrobbled tracks.'}),"\n"]}),(0,s.jsx)(t.p,{children:"For these Sources multi-scrobbler monitors the listening history data and scrobbles when it sees a new entry in the list."})]})]}),"\n",(0,s.jsx)(t.h3,{id:"multi-deviceuser",children:"Multi-Device/User"}),"\n",(0,s.jsx)(t.p,{children:"Some Sources report which User is playing a track and/or on what device the track is being played. These Sources can be filtered with configuration to only monitor activity for specific devices or users."}),"\n",(0,s.jsx)(t.h3,{id:"limiting-scrobble-destination",children:"Limiting Scrobble Destination"}),"\n",(0,s.jsxs)(t.p,{children:["To limit which Clients a Source will scrobble to use the ",(0,s.jsx)(t.code,{children:"clients"})," property in a Source's ",(0,s.jsx)(t.a,{href:"/configuration?configType=file#configuration-types",children:"File/AIO configuration"})," to specify the ",(0,s.jsx)(t.strong,{children:"name"})," property of the Clients that it should scrobble to:"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json5",metastring:'title="jellyfin.json"',children:'{\n "name": "MyJellyfin",\n "clients": ["myMaloja","koitoA","listenbrainzFoo"]\n "data": {\n // ...\n },\n}\n'})}),"\n",(0,s.jsx)(t.admonition,{type:"note",children:(0,s.jsxs)(t.p,{children:["If ",(0,s.jsx)(t.code,{children:"clients"})," is empty (",(0,s.jsx)(t.code,{children:'"clients": []'}),") or is omitted then the default is for the Source to scrobble to ",(0,s.jsx)(t.strong,{children:"all"})," configured Clients."]})}),"\n",(0,s.jsxs)(t.admonition,{type:"note",children:[(0,s.jsxs)(t.p,{children:["If a Client is configured using ",(0,s.jsx)(t.a,{href:"/configuration?configType=env#configuration-types",children:"ENV"})," then it is given a generated name. Check the multi-scrobbler Dashboard to find their name in a subheader under the Client type. Example:"]}),(0,s.jsx)(t.p,{children:(0,s.jsx)(t.img,{alt:"Client Name Subheader",src:i(4400).A+"",width:"177",height:"101"})}),(0,s.jsxs)(t.p,{children:["Otherwise, use the ",(0,s.jsx)(t.code,{children:"name"})," property you gave the client in it's File/AIO config."]})]}),"\n",(0,s.jsx)(t.h3,{id:"scrobble-thresholds",children:"Scrobble Thresholds"}),"\n",(0,s.jsx)(r.Ay,{filename:"spotify"}),"\n",(0,s.jsxs)(t.p,{children:["These can also be configured ",(0,s.jsx)(t.strong,{children:"globally"})," for all Sources using ENVs:"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{children:"SOURCE_SCROBBLE_DURATION=40\nSOURCE_SCROBBLE_PERCENT=20\n"})}),"\n",(0,s.jsx)(t.h3,{id:"should-scrobble-behavior",children:"Should Scrobble Behavior"}),"\n",(0,s.jsx)(t.admonition,{type:"note",children:(0,s.jsx)(t.p,{children:"This feature is currently available only for some Sources. Check each Source's documentation to see if it supported."})}),"\n",(0,s.jsxs)(t.p,{children:["For ",(0,s.jsx)(t.em,{children:"most"})," Sources multi-scrobbler can determine if a user is listening and scrobble automatically. For Sources which multi-scrobbler cannot do this or require additional configuration, a user can indicate to MS that it should or should not scrobble. This action can be taken on the Web Dashboard by clicking the link next to ",(0,s.jsx)(t.strong,{children:"Should Scrobble"})," found in each Source's status card."]}),"\n",(0,s.jsxs)(t.p,{children:["Additionally, for these Sources, the default behavior for Scrobbling can be explicitly set by adding the following value to a Source's ",(0,s.jsx)(t.code,{children:"options"})," in ",(0,s.jsx)(t.a,{href:"/configuration?configType=file#configuration-types",children:"file or aio config"}),":"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json5",metastring:'title="azuracast.json"',children:'{\n "data": {\n // ...\n },\n "options": {\n // set to true to always scrobble automatically\n // set to false to never scrobble automatically\n "systemScrobble": true\n }\n}\n'})})]})}function j(e={}){let{wrapper:t}={...(0,n.R)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(f,{...e})}):f(e)}},4400(e,t,i){i.d(t,{A:()=>l});let l="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALEAAABlCAYAAAAChgL8AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAPA0lEQVR4nO3ceVxV1d7H8c8+A8ggs8iQoqIiKAKiOODUY5qVkd6s6y2v5pBlmWmpWWmKtxK1wbQnLYfM7MnsajfMDFBURBEVFBAZxAlBUBAQDjKevZ8/VByyWyjI2bXerxd/wBnW2ofvWfu3195rSx7ePRUEQcU0Td0BQbhXIsSC6okQC6onQiyongixoHoixILqiRALqidCLKieCLGgeiLEguqJEAuqJ0IsqJ4IsaB6IsSC6okQC6onQiyongixoHq6e3mxi4tbQ/VDEO6aGIkF1RMhFlRPhFhQvXsIsVgkLZiGuwyxIjIsmIy7C7HIsGBCRDkhqN5dhFgR8RVMihiJBdUTU2yC6okQC6onQiyongixoHr1D7E4nhNMjBiJBdUTIW4k+g4hhC0P439fDKS51NS9uTu6dsMIWx7Gpy8GYmvC2/CXDLFk3Yo+j4Yw9pkQQgJbom/wBmx48NmR9Pd2oyQjA0ODlGB63Hs8zKghXtjfp0DVZqdxxtqTwBGjGOFhulG5p5Ud9aXv/zo75gWjSVnDU9O3kd8E9bVkG8is5bMY/oAeCYWC8DNsT7jQoG1oHHsxLMgKDLFExRtuOYzQ2HXgkZEhPNHPm7bOzdFVFHIi6QA/bvwP2zPKkAEwY9C89bw3AGLeH8esHRXQvA+vhk6iv1kBrnkv83GKsUH7fEe1Z9i5O4d/jm3Do0Pas+GLTGobv9V6M92v1z3SOD/KZxFb2L95Mj3rvqoSjv2G8Zi7jpqsXwh9I5TZ36VT06AtS9j26EFXc6hMPkxCxY1H9B6DCVv5PnOeCabrA7boq6vR2LjQdcAI5ixbzPxBzr/9DzGkEP7DAfbt+InIk3KD9hig2YMziYneQsyCfljU/dXI6UNHOC9rce8ViKe2wZttEPd1JG56Es3trNFIMucP7ybyUCYNP57p6ezXETPJSHJSBuXXh2FdG0bPnkA/Zw2XU7awcMlmYnIqkCxdCX72Zeb83YeHpk7gwNEwfr50h7dViohdtYTYBu/vf1eblUpy2RO0cvehq6NExkXTm54ywRBrcfR7hEmjBxPs7YKNUkbeqRS2b/iGDYcKr+3OJCzb9WfiuGH8T9dWOJrXUpKTRvSmDXwedRZ54Ewi5/bGDMB+MJ9EPkTp7khOBj9MwLUCuPWoMPaNqiVh2WRe+cmNGd/OZ4T1ARbOTiNocgg9Pay5khHBknc3cMztcV6fPIy+bS0pz07gu+Wr2JBSeufZRo0rXu0s0cjFZGZd4vqYaRH4GCM7mKEUxbJk3jfsKbn6auVKHntXL2VVp0953d+fR4Lt+SXc8Ov31fky69v5/M0mnaXPzWFjngLoeaDPcJ4fNZAgT0csqgpIjYtk9ZptHCmSAQnn4aFsnupN9tehLC4dzPRRPWhnVU1u8g5WfPwtMRckekz7nE9C7K/uBfpOZ1f0FGIXTWBmhAGlJpuMs0Ye69KaTm11cLFh91sNweRCbOH3Tz5d/DhtKOZ4/D7ia10I7NOfF95ti8WrM1mRXoNk15s3F0/lIbsrnD64l30Flvj0C+LpWZ441rzGgrxUIg61ZnB3d5opF0nceYxTGacpvLIHpVs/urlokPOPEZGUz6nsqrowSnovxr/uSVHOSdItfAn0DeHtN1pyuk17zNIzOKrvQpBnMJPny+SOW0p06R1irHXCraUGjBfJvXA9wlraB/hgr5Ep3BtFTMltr1MK2fLWOH7RS8jVlch/6N8i4TLkNVbOCsKhMp+khEMYHLwIGvocn/i04LUpX3K4/MZz7fo9T6hFBVmpqdDZD6+gEYTONTB+ajiFafuIatuLwb5OUJjOzoQcUnNrrn4uSgnn8ytRulrg6mqLhkIavpi5NyYWYgmLmtNs+/5ndCmbWR9fgoyWTuM/Ys3oBxg0sC2r0jORvLoRZCdRe/x7Zr4dTq4CZuEDGRtsRXY+1Kb/zMIPwf3rifgbkvhqyQriawF2sGd0K74e70HurvW8uyrrajlR9ymYkf71DN6MKkGx9Gf22jkM7+bPiaVTmbK1ENmsAy99/j5jWvnRu5OW6IN3OMzRW9HcXAKlnNK6aQkNTs4OSMjknsu7YwkjV1VgqKrHR2Xuy5gJPXCsyWL19HdYc6IKJBv6z/qAsCFDmDh0G4mbL9a1b2+RwcwXVrCvVEHj+gifrJ5Ij4696eseztcRa1lY7ciDvk6Qvp33F+3lRikvYyirQMEC6+aW9ejg/WNiIVYoOr6HDcdjMLdrSQe/bri7uNHNzxYJCSsrSyTAmJdDjlHBx2sYb07RsD0xk7T0ONasr6obJe7qiNWYS3Lq5asjUEUeZy/K4FRM5sniq+9bfZ4z543Q2hyLZr83zyUh3fJb3SY2CG07P7o7aDBmHeXQZWucW1gDcDIhjbLBfekU0AmruhArlCcf4uC1PYd8IZUjuTI92llj/wcmsSWNCU8SY3IhBr17MFNnjSOkiz3mEhgrism5fGsk5extLFjkwIwJgwgcPobuIwClmoJjO1m+ZB2ROXdbt8nIt4dMUVB+tf+8NaC3qC6l+IoC1jY42mnAIAMyBReLUXDGvbUL2jvskjU6c8z1oNRUU/kH5rE0zW2wkUDn9RRfbHzqV4/X2NpiI1E381JbffNnIiPL17bjd/Opw87OGgmZyyVlJnnVgWmFWHJk2LSXGdnZSMqmD1n6YxInLlzB9vF5bJnme9MTazgbvZZXotdj49YGH5/ODHhiBE/4DuWt6edInvELF3+zkUZmzOPMOSN0ccPTQw85VYCRrKPHKRnpglPfwfT7KpXom+tiyYkRiz5lhj8kLHuZqT+W/W4ziqGMMsDm4j4+Wb6H3Nu+FXLZOYoUaH6v26N1xdNDjySXcPbcbxzMNjHTmifWtqCVqxmSnMuenw5wPL+cGmzx929907dNwj7wSea+9y8WjfKg6nwWB3b8yNJ1B7gkS+gdHLDVALKMAkg6M/T3cyuVQpKOXcAoWeLr166u3xWHt7H5ZDWSQ19mvvMP+ro3QwKkZi3pM24ak/zMUEoT2Lq3CPnGoSZanfaOo37tqWQSixQkezecSo6zL+4wsXEJJBmc8PF0xqqmkvqU2IoMigKY6TG7qUGNozcBrTQoFRkcOXEfTrDchSYZibXtH2fxp/1uOckgF+1nSeguUjMNyG4dGPevN2h95BLN2nfD311zUy2pUFas0CbAB5+e81jrn0j6ZSs69vDHSaokbWccp4wgl+aRXaoQ6NCLGR/Z8FjsJuZuPHEfts5IZuxBcp8egXuv7nh9kUZqLVB7mvXvf4n3BxMJ9h/JkvUjqDRUgKUVFloJpSqXrR+uIapIAWQKC4uRcab7mLdYYLmG0PDbmqlMZt2agwTPDGL0kqUEJGZwQeNMl4D2OGvz2Zy2hx316HXt+VxyZWgb8AzLFvUi7t9LWXmwAseeQXTWgWH/AQ5fabhPqSE1yUissXSmo3dHOt/808EZK8qIXraYz6JPYnAO4NGhQbTOD2fe2mO3BL721A+8PmM1PyQWYdulNw8P6IRNwVH+vXQ+r31z+upzq5NZs2QLcTnVOHj5EuDbCsf7dc1BRjRbM2vQuAUztMuNKzNqzkbwxgtzWbgxjtTcMmRzcyjLJ2XPZuZPmc3C2KJrtbKRlO/X8X1yIbWOHegV5InTr/oukxfxES+8s4mdp4x4dO9Ff/8WVKbt5rM35/JxQnm9dv3GrHDCVsdx0mBFez9vAj1boJWcGfSQD+bKJXZuO8xlU6wlAMnDu2f9uqZcX+0s4+raqjH69CcgYRP8Cv8XOgDd7g94+r047jSlbOp0Hf7BV5+NxO34OsZO30q2qU0QXyNCLKieaR3YCcJdECEWVE+EWFA9EWJB9USIBdUTIRZUT4RYUD0RYkH1VBxiCefhC9i18knaXN8KjSP9XpzPdz98S/Ty4bRV8dYJf5xpXYp5j/R+TzLriebsWhTKLyfzOGuip0mFhvXnCrGjA9aXkomKTee4Kd4gQWgU9zHEV1fefvdQGov3PcAzf/PDtSaL7z9cznab4cye+CBd7Ks5vXcj734cyQklgDc3zMD5y5d57eeSa9draPEa/xFfBO3luZc2U7cOUuvJ5DVhjPW4emOEVZEhlEeFMXRhEv3nrWNS0Vo2WQ5mTLA7muw9fPjeD1Q98gLTQnxxUS5wYNNK3tuYpsqLdIQmqIm1bfoSXP0TMye8wqtbagiZs4hFg4tYOXUSIS99SZb3OGYPd0dTdYzIGAP+A4NuXEKpbcug/s5kRO+7tVQwnmHdq8/z1LIjVF2MYtaoCYxcdvTa5ZsaWvbthd3Ojxk75m1W5HXj7WULGK3dxrTRE3l2cRKuo6cw1ttE7wwi/K77HmKlKJ6NP6aRX1pM6tZojprpOR7+E8mXrlByZj9b4y/TtmNr9NSQHL2fws596H/tYlqdVzAPtsgkYnf+bWvUjFSUlnDJUINcXU5RUQnF5devQFaoTNjOhsP5lJScJXJbIhXWuWzbmESewUBOfBTR5+zp2N72t9fNCSbtvodYvlJ+de0kgCxjVKqprLyxHzcajUg6HRqgJj2WnRc6MijYEQ06Og/siX1yDLsL6rPfV6gov3JjmbwsIytVN7VpxGgEvV4nQqxSpj0JZTxN5K48fAb2oqW5F4P6WnMo6iBFonYVbmLaIUbmzK69nGwfxMA+QfQ2TyQi3jSXjQtNx8RDDHLufiJPtOPpCb0xj4shvvzGY1WVVUgOrfBpY4+FqAX+skw+xCgF7NqZib2LQsyOlJtur6RQGh/J1uIAZi+fwhAHkeK/KhWssZNwfPRtNo06x7TxX5EiTmIItzHpM3ZScxe8PL158ilvsiO+EmfhhDsy4RBLtBg4iRVT2pO//xvmbT7XCDfEFv4MVFBOCMJ/Z/oHdoLwOxq1nOhWUki34oLGbKJRJNq3INHOqam7IfxBjRvi4gImnklvzCYaxWoQIVYRUU4IqteoI3GifQtWN2YDjSTRvkVTd0Goh8YNsZ2T2C0LjU6UE4LqiRALqidCLKieCLGgeiLEguqJEAuqJ0IsqN49zRN3D/BrqH4Iwl0TI7GgeiLEguqJEAuqJ0IsqJ4IsaB6IsSC6okQC6onQiyongixoHoixILqiRALqidCLKieCLGgeiLEgur9P7w+WrWPSpyoAAAAAElFTkSuQmCC"}}]); \ No newline at end of file diff --git a/assets/js/30829cd8.a19a2ff0.js b/assets/js/30829cd8.a19a2ff0.js new file mode 100644 index 00000000..0d1d44ae --- /dev/null +++ b/assets/js/30829cd8.a19a2ff0.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["5077"],{1762(e,n,s){s.r(n),s.d(n,{metadata:()=>r,default:()=>u,frontMatter:()=>o,contentTitle:()=>c,toc:()=>d,assets:()=>a});var r=JSON.parse('{"id":"configuration/sources/tealfm-source","title":"Teal.fm","description":"Teal.fm (Source)","source":"@site/docs/configuration/sources/tealfm-source.mdx","sourceDirName":"configuration/sources","slug":"/configuration/sources/tealfm-source","permalink":"/multi-scrobbler/configuration/sources/tealfm-source","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/sources/tealfm-source.mdx","tags":[],"version":"current","frontMatter":{"title":"Teal.fm","description":"Teal.fm (Source)","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Subsonic","permalink":"/multi-scrobbler/configuration/sources/subsonic"},"next":{"title":"VLC","permalink":"/multi-scrobbler/configuration/sources/vlc"}}'),i=s(4848),t=s(8453);s(8194),s(698),s(2170);var l=s(3242);let o={title:"Teal.fm",description:"Teal.fm (Source)",toc_min_heading_level:2,toc_max_heading_level:5},c,a={},d=[{value:"Setup",id:"setup",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Service Filtering",id:"service-filtering",level:3}];function h(e){let n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",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},{Config:s,Details:r}=n;return s||f("Config",!0),r||f("Details",!0),(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.admonition,{title:"Other Uses",type:"tip",children:(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["To ",(0,i.jsx)(n.strong,{children:"scrobble to"})," an ATProto PDS, create a ",(0,i.jsx)(n.a,{href:"/configuration/clients/tealfm",children:"teal.fm (Client)"})]}),"\n"]})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.a,{href:"https://teal.fm/",children:(0,i.jsx)(n.strong,{children:"teal.fm"})})," is a social discovery and web viewing service for scrobbles stored/available on the ATProto network/your repository in a ",(0,i.jsx)(n.a,{href:"https://github.com/Bluesky-social/pds",children:"PDS (Personal Data Server)"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"This Source monitors the teal.fm scrobbles in your PDS and then re-scrobbles discovered tracks to configured clients."}),"\n",(0,i.jsxs)(n.p,{children:["Learn more about how teal.fm scrobbles work in the ",(0,i.jsx)(n.a,{href:"/configuration/clients/tealfm",children:"teal.fm (Client)"})," docs."]}),"\n",(0,i.jsx)(n.h2,{id:"setup",children:"Setup"}),"\n",(0,i.jsxs)(n.p,{children:["Follow the ",(0,i.jsx)(n.strong,{children:"Setup"})," instructions in the ",(0,i.jsx)(n.a,{href:"/configuration/clients/tealfm#setup",children:"teal.fm (Client)"})," docs to get access to your Bluesky PDS repository and scrobble data."]}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["See the ",(0,i.jsx)(n.a,{href:"/configuration/clients/tealfm#setup",children:"teal.fm (Client)"})," configuration for general setup. The main differences for ",(0,i.jsx)(n.strong,{children:"Source"})," configuration:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File/AIO config"})," must include ",(0,i.jsx)(n.code,{children:'"configureAs": "source"'})]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"service-filtering",children:"Service Filtering"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"serviceAllow"})," and ",(0,i.jsx)(n.code,{children:"serviceDeny"})," are lists that can be used to filter what Plays are scrobbles from teal.fm. These correspond to the base domain of the music service where the Play is from, originally. The values in the list can be a subset of the actual service name (they do not need to be exact matches)."]}),"\n",(0,i.jsxs)(n.p,{children:["Only one filter can be used at a time. If ",(0,i.jsx)(n.code,{children:"serviceAllow"})," is configured then ",(0,i.jsx)(n.code,{children:"serviceDeny"})," will be ignored."]}),"\n",(0,i.jsxs)(n.p,{children:["If a Play does not have an associated music service it can be allowed/denied using the special keyword ",(0,i.jsx)(n.code,{children:"unknown"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"If you are unsure what services are being used for your scrobbles you can check this by:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["use ",(0,i.jsx)(n.a,{href:"https://atproto.at/",children:"atproto.at"})," to lookup your DID/handle"]}),"\n",(0,i.jsxs)(n.li,{children:["View the ",(0,i.jsx)(n.strong,{children:"Raw Data"})," for the PDS Collection ",(0,i.jsx)(n.code,{children:"fm.teal.alpha.feed.play"})]}),"\n",(0,i.jsxs)(n.li,{children:["Check the ",(0,i.jsx)(n.code,{children:"musicServiceBaseDomain"})," property for each record"]}),"\n"]}),"\n",(0,i.jsxs)(r,{children:[(0,i.jsx)("summary",{children:"Examples"}),(0,i.jsxs)(n.p,{children:["Allowing only scrobbles from ",(0,i.jsx)(n.a,{href:"https://plyr.fm/",children:"plyr.fm"}),":"]}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="tealfm.json"',children:'[\n {\n "name": "myTealSource",\n "configureAs": "source",\n "data": {\n "identifier": "alice.bsky.social",\n "appPassword": "twog-phu7-4dhe-y4j3",\n\n "serviceAllow": ["plyr"],\n }\n }\n]\n'})}),(0,i.jsx)(n.p,{children:"Deny scrobbles from last.fm:"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="tealfm.json"',children:'[\n {\n "name": "myTealSource",\n "configureAs": "source",\n "data": {\n "identifier": "alice.bsky.social",\n "appPassword": "twog-phu7-4dhe-y4j3",\n\n "serviceDeny": ["last"],\n }\n }\n]\n'})}),(0,i.jsx)(n.p,{children:"Allow scrobbles from plyr.fm and unknown services:"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="tealfm.json"',children:'[\n {\n "name": "myTealSource",\n "configureAs": "source",\n "data": {\n "identifier": "alice.bsky.social",\n "appPassword": "twog-phu7-4dhe-y4j3",\n\n "serviceAllow": ["plyr", "unknown"],\n }\n }\n]\n'})})]}),"\n",(0,i.jsx)(s,{config:"TealClientConfig",fileContent:l.A,name:"tealfm",children:(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,i.jsx)(n.th,{children:"Required?"}),(0,i.jsx)(n.th,{children:"Default"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{style:{textAlign:"left"},children:(0,i.jsx)(n.code,{children:"SOURCE_TEALFM_IDENTIFIER"})}),(0,i.jsx)(n.td,{children:"Yes"}),(0,i.jsx)(n.td,{}),(0,i.jsxs)(n.td,{children:["Your ",(0,i.jsx)(n.strong,{children:"full"})," ATProto handle or DID. For Bluesky account this is like ",(0,i.jsx)(n.code,{children:"myUser.bsky.social"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{style:{textAlign:"left"},children:(0,i.jsx)(n.code,{children:"SOURCE_TEALFM_APP_PW"})}),(0,i.jsx)(n.td,{children:"Yes"}),(0,i.jsx)(n.td,{}),(0,i.jsx)(n.td,{children:"Bluesky/ATProto network App Password"})]})]})]})})]})}function u(e={}){let{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}function f(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}},3242(e,n,s){s.d(n,{A:()=>r});let r='[\n {\n "name": "myTeal",\n "configureAs": "client",\n "data": {\n "identifier": "alice.bsky.social",\n "appPassword": "twog-phu7-4dhe-y4j3"\n }\n },\n {\n "name": "myTealSource",\n "configureAs": "source",\n "data": {\n "identifier": "alice.bsky.social",\n "appPassword": "twog-phu7-4dhe-y4j3",\n\n // everything below is optional\n "serviceAllow": ["plyr"],\n "serviceDeny": ["last.fm"]\n }\n }\n]'}}]); \ No newline at end of file diff --git a/assets/js/30829cd8.ffdd67c4.js b/assets/js/30829cd8.ffdd67c4.js deleted file mode 100644 index 31e33cf9..00000000 --- a/assets/js/30829cd8.ffdd67c4.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["5077"],{1762(e,n,s){s.r(n),s.d(n,{metadata:()=>r,default:()=>u,frontMatter:()=>o,contentTitle:()=>c,toc:()=>d,assets:()=>a});var r=JSON.parse('{"id":"configuration/sources/tealfm-source","title":"Teal.fm","description":"To scrobble to* an ATProto PDS, create a teal.fm (Client)","source":"@site/docs/configuration/sources/tealfm-source.mdx","sourceDirName":"configuration/sources","slug":"/configuration/sources/tealfm-source","permalink":"/multi-scrobbler/configuration/sources/tealfm-source","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/sources/tealfm-source.mdx","tags":[],"version":"current","frontMatter":{"title":"Teal.fm","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Subsonic","permalink":"/multi-scrobbler/configuration/sources/subsonic"},"next":{"title":"VLC","permalink":"/multi-scrobbler/configuration/sources/vlc"}}'),i=s(4848),t=s(8453);s(8194),s(698),s(2170);var l=s(3242);let o={title:"Teal.fm",toc_min_heading_level:2,toc_max_heading_level:5},c,a={},d=[{value:"Setup",id:"setup",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Service Filtering",id:"service-filtering",level:3}];function h(e){let n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",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},{Config:s,Details:r}=n;return s||f("Config",!0),r||f("Details",!0),(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.admonition,{title:"Other Uses",type:"tip",children:(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["To ",(0,i.jsx)(n.strong,{children:"scrobble to"})," an ATProto PDS, create a ",(0,i.jsx)(n.a,{href:"/configuration/clients/tealfm",children:"teal.fm (Client)"})]}),"\n"]})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.a,{href:"https://teal.fm/",children:(0,i.jsx)(n.strong,{children:"teal.fm"})})," is a social discovery and web viewing service for scrobbles stored/available on the ATProto network/your repository in a ",(0,i.jsx)(n.a,{href:"https://github.com/Bluesky-social/pds",children:"PDS (Personal Data Server)"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"This Source monitors the teal.fm scrobbles in your PDS and then re-scrobbles discovered tracks to configured clients."}),"\n",(0,i.jsxs)(n.p,{children:["Learn more about how teal.fm scrobbles work in the ",(0,i.jsx)(n.a,{href:"/configuration/clients/tealfm",children:"teal.fm (Client)"})," docs."]}),"\n",(0,i.jsx)(n.h2,{id:"setup",children:"Setup"}),"\n",(0,i.jsxs)(n.p,{children:["Follow the ",(0,i.jsx)(n.strong,{children:"Setup"})," instructions in the ",(0,i.jsx)(n.a,{href:"/configuration/clients/tealfm#setup",children:"teal.fm (Client)"})," docs to get access to your Bluesky PDS repository and scrobble data."]}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["See the ",(0,i.jsx)(n.a,{href:"/configuration/clients/tealfm#setup",children:"teal.fm (Client)"})," configuration for general setup. The main differences for ",(0,i.jsx)(n.strong,{children:"Source"})," configuration:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File/AIO config"})," must include ",(0,i.jsx)(n.code,{children:'"configureAs": "source"'})]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"service-filtering",children:"Service Filtering"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"serviceAllow"})," and ",(0,i.jsx)(n.code,{children:"serviceDeny"})," are lists that can be used to filter what Plays are scrobbles from teal.fm. These correspond to the base domain of the music service where the Play is from, originally. The values in the list can be a subset of the actual service name (they do not need to be exact matches)."]}),"\n",(0,i.jsxs)(n.p,{children:["Only one filter can be used at a time. If ",(0,i.jsx)(n.code,{children:"serviceAllow"})," is configured then ",(0,i.jsx)(n.code,{children:"serviceDeny"})," will be ignored."]}),"\n",(0,i.jsxs)(n.p,{children:["If a Play does not have an associated music service it can be allowed/denied using the special keyword ",(0,i.jsx)(n.code,{children:"unknown"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"If you are unsure what services are being used for your scrobbles you can check this by:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["use ",(0,i.jsx)(n.a,{href:"https://atproto.at/",children:"atproto.at"})," to lookup your DID/handle"]}),"\n",(0,i.jsxs)(n.li,{children:["View the ",(0,i.jsx)(n.strong,{children:"Raw Data"})," for the PDS Collection ",(0,i.jsx)(n.code,{children:"fm.teal.alpha.feed.play"})]}),"\n",(0,i.jsxs)(n.li,{children:["Check the ",(0,i.jsx)(n.code,{children:"musicServiceBaseDomain"})," property for each record"]}),"\n"]}),"\n",(0,i.jsxs)(r,{children:[(0,i.jsx)("summary",{children:"Examples"}),(0,i.jsxs)(n.p,{children:["Allowing only scrobbles from ",(0,i.jsx)(n.a,{href:"https://plyr.fm/",children:"plyr.fm"}),":"]}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="tealfm.json"',children:'[\n {\n "name": "myTealSource",\n "configureAs": "source",\n "data": {\n "identifier": "alice.bsky.social",\n "appPassword": "twog-phu7-4dhe-y4j3",\n\n "serviceAllow": ["plyr"],\n }\n }\n]\n'})}),(0,i.jsx)(n.p,{children:"Deny scrobbles from last.fm:"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="tealfm.json"',children:'[\n {\n "name": "myTealSource",\n "configureAs": "source",\n "data": {\n "identifier": "alice.bsky.social",\n "appPassword": "twog-phu7-4dhe-y4j3",\n\n "serviceDeny": ["last"],\n }\n }\n]\n'})}),(0,i.jsx)(n.p,{children:"Allow scrobbles from plyr.fm and unknown services:"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="tealfm.json"',children:'[\n {\n "name": "myTealSource",\n "configureAs": "source",\n "data": {\n "identifier": "alice.bsky.social",\n "appPassword": "twog-phu7-4dhe-y4j3",\n\n "serviceAllow": ["plyr", "unknown"],\n }\n }\n]\n'})})]}),"\n",(0,i.jsx)(s,{config:"TealClientConfig",fileContent:l.A,name:"tealfm",children:(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,i.jsx)(n.th,{children:"Required?"}),(0,i.jsx)(n.th,{children:"Default"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{style:{textAlign:"left"},children:(0,i.jsx)(n.code,{children:"SOURCE_TEALFM_IDENTIFIER"})}),(0,i.jsx)(n.td,{children:"Yes"}),(0,i.jsx)(n.td,{}),(0,i.jsxs)(n.td,{children:["Your ",(0,i.jsx)(n.strong,{children:"full"})," ATProto handle or DID. For Bluesky account this is like ",(0,i.jsx)(n.code,{children:"myUser.bsky.social"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{style:{textAlign:"left"},children:(0,i.jsx)(n.code,{children:"SOURCE_TEALFM_APP_PW"})}),(0,i.jsx)(n.td,{children:"Yes"}),(0,i.jsx)(n.td,{}),(0,i.jsx)(n.td,{children:"Bluesky/ATProto network App Password"})]})]})]})})]})}function u(e={}){let{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}function f(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}},3242(e,n,s){s.d(n,{A:()=>r});let r='[\n {\n "name": "myTeal",\n "configureAs": "client",\n "data": {\n "identifier": "alice.bsky.social",\n "appPassword": "twog-phu7-4dhe-y4j3"\n }\n },\n {\n "name": "myTealSource",\n "configureAs": "source",\n "data": {\n "identifier": "alice.bsky.social",\n "appPassword": "twog-phu7-4dhe-y4j3",\n\n // everything below is optional\n "serviceAllow": ["plyr"],\n "serviceDeny": ["last.fm"]\n }\n }\n]'}}]); \ No newline at end of file diff --git a/assets/js/33c4ed3f.69fd96e4.js b/assets/js/33c4ed3f.69fd96e4.js deleted file mode 100644 index 2f46e481..00000000 --- a/assets/js/33c4ed3f.69fd96e4.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["429"],{8779(e,n,t){t.r(n),t.d(n,{metadata:()=>r,default:()=>h,frontMatter:()=>i,contentTitle:()=>a,toc:()=>c,assets:()=>l});var r=JSON.parse('{"id":"development/dev-source","title":"Source Development/Tutorial","description":"This document will provide a step-by-step guide for creating a (trivial) new Source in MS alongside describing what aspects of the Source need to be implemented based on the service you use. Before using this document you should review Common Development.","source":"@site/docs/development/dev-source.md","sourceDirName":"development","slug":"/development/dev-source","permalink":"/multi-scrobbler/development/dev-source","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/development/dev-source.md","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"toc_min_heading_level":2,"toc_max_heading_level":5,"sidebar_position":2,"title":"Source Development/Tutorial"},"sidebar":"tutorialSidebar","previous":{"title":"Common Development","permalink":"/multi-scrobbler/development/dev-common"},"next":{"title":"Client Development/Tutorial","permalink":"/multi-scrobbler/development/dev-client"}}'),o=t(4848),s=t(8453);let i={toc_min_heading_level:2,toc_max_heading_level:5,sidebar_position:2,title:"Source Development/Tutorial"},a,l={},c=[{value:"Scenario",id:"scenario",level:2},{value:"Minimal Implementation",id:"minimal-implementation",level:2},{value:"Define and Implement Config",id:"define-and-implement-config",level:3},{value:"Create CoolPlayer Source",id:"create-coolplayer-source",level:3},{value:"Initialize Source from Config",id:"initialize-source-from-config",level:3},{value:"Implement Play Object Transform",id:"implement-play-object-transform",level:3},{value:"Implement Stages",id:"implement-stages",level:3},{value:"Build Data",id:"build-data",level:4},{value:"Check Connection",id:"check-connection",level:4},{value:"Test Auth",id:"test-auth",level:4},{value:"Implement Polling",id:"implement-polling",level:3},{value:"Further Implementation",id:"further-implementation",level:2},{value:"Backlog",id:"backlog",level:3},{value:"Other Source Types",id:"other-source-types",level:3},{value:"Music History Source",id:"music-history-source",level:4},{value:"Non-Polling Source",id:"non-polling-source",level:4},{value:"Basic Source",id:"basic-source",level:4},{value:"Discovery",id:"discovery",level:5},{value:"Scrobbling",id:"scrobbling",level:5}];function d(e){let n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",h5:"h5",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsxs)(n.p,{children:["This document will provide a step-by-step guide for creating a (trivial) new Source in MS alongside describing what aspects of the Source need to be implemented based on the service you use. Before using this document you should review ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#common-development",children:"Common Development"}),"."]}),"\n",(0,o.jsx)(n.h2,{id:"scenario",children:"Scenario"}),"\n",(0,o.jsxs)(n.p,{children:["You are the developer of a fancy, new self-hosted web-based media player called ",(0,o.jsx)(n.strong,{children:"Cool Player."})," Cool Player has a slick interface and many bells and whistles, but most importantly it has an API. The API:"]}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["Has an unauthenticated health endpoint at ",(0,o.jsx)(n.code,{children:"/api/health"})," that returns ",(0,o.jsx)(n.code,{children:"200"})," if the service is running properly"]}),"\n",(0,o.jsxs)(n.li,{children:["Has authenticated endpoints that require a user-generated token in the header ",(0,o.jsx)(n.code,{children:"Authorization MY_TOKEN"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["Has a ",(0,o.jsx)(n.code,{children:"/api/recent"})," endpoint that lists recently played tracks with a timestamp"]}),"\n",(0,o.jsxs)(n.li,{children:["Has a ",(0,o.jsx)(n.code,{children:"/api/now-playing"})," endpoint that returns information about the state of the player like current track, player position in the track, etc..."]}),"\n"]}),"\n"]}),"\n",(0,o.jsxs)(n.li,{children:["Cool Player is by default accessed on port ",(0,o.jsx)(n.code,{children:"6969"})]}),"\n",(0,o.jsxs)(n.li,{children:["Your personal instance of Cool Player is hosted at ",(0,o.jsx)(n.code,{children:"http://192.168.0.100:6969"})," and the api is accessed at ",(0,o.jsx)(n.code,{children:"http://192.168.0.100:6969/api"})]}),"\n"]}),"\n",(0,o.jsxs)(n.p,{children:["Because there is an API that MS can actively read this will be a ",(0,o.jsx)(n.strong,{children:"polling"})," Source where MS sends requests to Cool Player to get scrobble information -- as opposed to an ",(0,o.jsx)(n.strong,{children:"ingress"})," Source like Jellyfin/Plex that uses webhooks from the service to send data to MS."]}),"\n",(0,o.jsx)(n.h2,{id:"minimal-implementation",children:"Minimal Implementation"}),"\n",(0,o.jsx)(n.h3,{id:"define-and-implement-config",children:"Define and Implement Config"}),"\n",(0,o.jsxs)(n.p,{children:["We will create a new config interface for Cool Player using the ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#config",children:"Common Config"})," and tell MS it is a valid config that can be used."]}),"\n",(0,o.jsx)(n.p,{children:"Create a new file for your config:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="/src/backend/common/infrastructure/config/source/coolplayer.ts"',children:"import { PollingOptions } from \"../common.js\";\nimport { CommonSourceConfig, CommonSourceData } from \"./index.js\";\n\n// all of the required data for the Build Data and Test Auth stages (from Common Development docs)\n// should go here\nexport interface CoolPlayerSourceData extends CommonSourceData, PollingOptions {\n// remember to annotation your properties!\n\n /**\n * The user-generated token for Cool Player auth created in Cool Player -> Settings -> User -> Tokens\n *\n * @example f243331e-cf5b-49d7-846b-0845bdc965b4\n * */\n token: string\n /**\n * The host and port where Cool Player is hosted\n *\n * @example http://192.168.0.100:6969\n * */\n baseUrl: string\n}\n\nexport interface CoolPlayerSourceConfig extends CommonSourceConfig {\n data: CoolPlayerSourceData\n}\n\nexport interface CoolPlayerSourceAIOConfig extends CoolPlayerSourceConfig {\n // when using the all-in-one 'config.json' this is how users will identify this source\n type: 'coolplayer'\n}\n"})}),"\n",(0,o.jsx)(n.p,{children:"Add the new interface to the list of valid interfaces for sources:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/common/infrastructure/config/source/sources.ts"',children:'import { ChromecastSourceAIOConfig, ChromecastSourceConfig } from "./chromecast.js";\n// ...\n// highlight-next-line\nimport { CoolPlayerSourceAIOConfig, CoolPlayerSourceConfig } from "./coolplayer.js";\n\nexport type SourceConfig =\n SpotifySourceConfig\n // ...\n // highlight-next-line\n | CoolPlayerSourceConfig;\n\nexport type SourceAIOConfig =\n SpotifySourceAIOConfig\n // ...\n // highlight-next-line\n | CoolPlayerSourceAIOConfig;\n'})}),"\n",(0,o.jsx)(n.p,{children:"Finally, add the source type identifier to the list of valid identifiers"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/common/infrastructure/Atomic.ts"',children:"export type SourceType =\n 'spotify'\n // ...\n // highlight-next-line\n | 'coolplayer';\n\nexport const sourceTypes: SourceType[] = [\n 'spotify',\n // ...\n // highlight-next-line\n 'coolplayer'\n];\n"})}),"\n",(0,o.jsxs)(n.p,{children:["Now we will create a new Source inheriting from ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#concrete-class",children:(0,o.jsx)(n.code,{children:"AbstractComponent"})})," that:"]}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsx)(n.li,{children:"accepts our config interface"}),"\n",(0,o.jsxs)(n.li,{children:["implements a function to transform CoolPlayer's track data into a ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#play-object",children:(0,o.jsx)(n.strong,{children:"PlayObject"})})]}),"\n",(0,o.jsxs)(n.li,{children:["implements required ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#stages",children:"stages"})]}),"\n",(0,o.jsx)(n.li,{children:"implements required methods to current player state and/or now playing track"}),"\n"]}),"\n",(0,o.jsx)(n.h3,{id:"create-coolplayer-source",children:"Create CoolPlayer Source"}),"\n",(0,o.jsxs)(n.p,{children:["First we create a new Source called ",(0,o.jsx)(n.code,{children:"CoolPlayerSource"})," and setup our constructor to accept the config and ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#stage-test-auth",children:"specify Auth behavior."})]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/SpotifySource.ts"',children:'import { CoolPlayerSourceConfig } from "../common/infrastructure/config/source/coolplayer.js";\nimport MemorySource from "./MemorySource.js";\nimport {\n InternalConfig,\n} from "../common/infrastructure/Atomic.js";\n\n// MemorySource is the base class used for polling-type Sources\nexport default class CoolPlayerSource extends MemorySource {\n\n // type hints for TS to know what the base class config looks like\n declare config: CoolPlayerSourceConfig;\n\n constructor(name: any, config: CoolPlayerSourceConfig, internal: InternalConfig, emitter: EventEmitter) {\n super(\'coolplayer\', name, config, internal, emitter);\n\n // Cool Player required authentication\n this.requiresAuth = true;\n // but does not require user interaction for auth to work\n this.requiresAuthInteraction = false;\n // tells MS this is a Source that can be activity monitored through API\n this.canPoll = true;\n }\n}\n'})}),"\n",(0,o.jsx)(n.h3,{id:"initialize-source-from-config",children:"Initialize Source from Config"}),"\n",(0,o.jsxs)(n.p,{children:["When MS starts it reads all configs and determines which Source to build based on the configs found. We need to tell it to build a ",(0,o.jsx)(n.code,{children:"CoolPlayerSource"})," when a ",(0,o.jsx)(n.code,{children:"coolplayer"})," config type is found."]}),"\n",(0,o.jsxs)(n.p,{children:["We modify ",(0,o.jsx)(n.code,{children:"ScrobbleSources.ts"})," to add ",(0,o.jsx)(n.code,{children:"CoolPlayerSource"})," as an option:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/ScrobbleSources.ts"',children:"// ...\nimport { CoolPlayerSource, CoolPlayerData } from \"./CoolPlayerSource.js\";\n\nexport default class ScrobbleSources {\n // ...\n buildSourcesFromConfig = async (additionalConfigs: ParsedConfig[] = []) => {\n // ...\n\n // if CoolPlayerSource should be able to be built from ENVs only \n // then add it as a case statement here\n for (const sourceType of sourceTypes) {\n switch (sourceType) {\n // ...\n case 'musikcube':\n // ...\n break;\n // highlight-start\n case 'coolplayer':\n const cp = {\n baseUrl: process.env.COOL_URL,\n token: process.env.COOL_TOKEN\n }\n if (!Object.values(cp).every(x => x === undefined)) {\n configs.push({\n type: 'coolplayer',\n name: 'unnamed',\n source: 'ENV',\n mode: 'single',\n configureAs: defaultConfigureAs,\n data: cp as CoolPlayerData\n });\n }\n break;\n // highlight-end\n default:\n break;\n }\n }\n }\n\n // ...\n\n // (required) create new CoolPlayerSource if source config type is 'coolplayer'\n addSource = async (clientConfig: ParsedConfig, defaults: SourceDefaults = {}) => {\n // ...\n let newSource: AbstractSource;\n switch (type) {\n // ...\n case 'musikcube':\n // ...\n break;\n // highlight-start\n case 'coolplayer':\n newSource = await new CoolPlayerSource(name, compositeConfig as CoolPlayerSourceConfig, internal, this.emitter);\n break;\n // highlight-end\n default:\n break;\n }\n }\n}\n"})}),"\n",(0,o.jsx)(n.h3,{id:"implement-play-object-transform",children:"Implement Play Object Transform"}),"\n",(0,o.jsxs)(n.p,{children:["Now we will create a static function that is used to take the track data returned from Cool Player's API and return a standard ",(0,o.jsxs)(n.a,{href:"/multi-scrobbler/development/dev-common#play-object",children:[(0,o.jsx)(n.code,{children:"PlayObject"}),"."]})]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/CoolPlayerSource.ts"',children:'import dayjs from "dayjs";\nimport {\n FormatPlayObjectOptions,\n} from "../common/infrastructure/Atomic.js";\nimport { PlayObject } from "../../core/Atomic.js";\n\nexport default class CoolPlayerSource extends MemorySource {\n // ...\n\n // \'obj\' should ideally be a real TS interface\n // if CoolPlayer has a ts/js client we would use that otherwise\n // we should build our own interfaces to represent track data from Cool Player\'s API\n static formatPlayObj(obj: any, options: FormatPlayObjectOptions = {}): PlayObject {\n const {\n trackName,\n artistName,\n albumName,\n duration,\n playedAt,\n } = obj;\n\n return {\n data: {\n artists: [artistName],\n album: albumName,\n track: trackName,\n // assuming seconds\n duration,\n // assuming playedAt is an ISO8601 timestamp\n playDate: dayjs(playedAt)\n },\n meta: {\n source: \'CoolPlayer\'\n }\n }\n }\n}\n'})}),"\n",(0,o.jsx)(n.h3,{id:"implement-stages",children:"Implement Stages"}),"\n",(0,o.jsxs)(n.p,{children:["Next we will implement the ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#stages",children:"Stages"})," required to get CoolPlayerSource running."]}),"\n",(0,o.jsx)(n.h4,{id:"build-data",children:"Build Data"}),"\n",(0,o.jsxs)(n.p,{children:["First we implement the ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#stage-build-data",children:"Build Data Stage"}),". We will check that the ",(0,o.jsx)(n.code,{children:"token"})," and ",(0,o.jsx)(n.code,{children:"baseUrl"})," properties have been provided by the user. Additionally, we will parse the baseUrl and add default ports/prefix."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/CoolPlayerSource.ts"',children:"import { URL } from \"url\";\n// ...\n\nexport default class CoolPlayerSource extends MemorySource {\n\n baseUrl!: URL;\n\n // ...\n\n static parseConnectionUrl(val: string) {\n const normal = normalizeUrl(val, {removeTrailingSlash: false, normalizeProtocol: true})\n const url = new URL(normal);\n\n if (url.port === null || url.port === '') {\n url.port = '6969';\n }\n if (url.pathname === '/') {\n url.pathname = '/api/';\n }\n return url;\n }\n\n protected async doBuildInitData(): Promise {\n const {\n token,\n baseUrl\n } = this.config;\n if (token === null || token === undefined || (typeof token === 'string' && token.trim() === '')) {\n throw new Error(`'token' must be defined`);\n }\n\n if (baseUrl === null || baseUrl === undefined || (typeof baseUrl === 'string' && baseUrl.trim() === '')) {\n throw new Error(`'baseUrl' must be defined`);\n }\n try {\n this.baseUrl = CoolPlayerSource.parseConnectionUrl(baseUrl);\n } catch (e) {\n throw new Error(`Could not parse baseUrl: ${baseUrl}`, {cause: e});\n }\n\n this.logger.verbose(`Config URL: ${baseUrl} => Normalized: '${this.url.toString()}'`);\n return true;\n }\n}\n"})}),"\n",(0,o.jsx)(n.h4,{id:"check-connection",children:"Check Connection"}),"\n",(0,o.jsxs)(n.p,{children:["Second we will implement the ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#stage-check-connection",children:"Check Connection Stage"}),":"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/CoolPlayerSource.ts"',children:"import request from 'superagent';\nimport { UpstreamError } from \"../common/errors/UpstreamError.js\";\n// ...\nexport default class CoolPlayerSource extends MemorySource {\n\n // ...\n\n protected async doCheckConnection(): Promise {\n try {\n const resp = await request.get(`${this.baseUrl}/health`);\n return true;\n // if /health returned version info we could instead read response and return a string with version info for visibility to the user\n // return `Cool Player Version: ${resp.body.version}`;\n } catch (e) {\n throw e;\n }\n }\n}\n"})}),"\n",(0,o.jsx)(n.h4,{id:"test-auth",children:"Test Auth"}),"\n",(0,o.jsxs)(n.p,{children:["Finally, we will implement ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#stage-test-auth",children:"Auth Test Stage"}),":"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/CoolPlayerSource.ts"',children:"import request from 'superagent';\nimport { UpstreamError } from \"../common/errors/UpstreamError.js\";\n// ...\nexport default class CoolPlayerSource extends MemorySource {\n\n // ...\n\n doAuthentication = async () => {\n try {\n const resp = await request\n .get(`${this.baseUrl}/recent`)\n .set('Authorization', `Token ${this.config.token}`);\n return true;\n } catch (e) {\n // if Cool Player returned an error as json we could parse it from error body and throw new Error with the message\n throw e;\n }\n }\n}\n"})}),"\n",(0,o.jsx)(n.h3,{id:"implement-polling",children:"Implement Polling"}),"\n",(0,o.jsxs)(n.p,{children:["The majority of Sources MS monitors primarily operate as a source of truth for a ",(0,o.jsx)(n.strong,{children:"music player"})," rather than a ",(0,o.jsx)(n.strong,{children:"played music history."})," Only Listenbrainz and Last.fm operate as a source of music history."]}),"\n",(0,o.jsxs)(n.p,{children:["To this end, MS implements a ",(0,o.jsx)(n.a,{href:"https://www.freecodecamp.org/news/state-machines-basics-of-computer-science-d42855debc66/",children:"state machine"}),' that emulates the behavior of a music player in order to keep track of when a song you are listening to should be scrobbled. It does this by monitoring the "currently playing" track reported by a Source\'s service, with varying degrees of accuracy depending on what information is returned from the service. The state machine is implemented in ',(0,o.jsx)(n.code,{children:"MemorySource"})," which our ",(0,o.jsx)(n.code,{children:"CoolPlayerSource"})," inherits from."]}),"\n",(0,o.jsxs)(n.p,{children:["For a polling Source to work properly we need to implement a function, ",(0,o.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/sources/AbstractSource.ts#L92",children:(0,o.jsx)(n.code,{children:"getRecentlyPlayed"})}),', that returns PlayObjects that are "newly" played. These are then checked against previously "discovered" plays and their timestamp to determine if they should be surfaced to Clients to scrobble.']}),"\n",(0,o.jsxs)(n.p,{children:["To take advantage of the ",(0,o.jsx)(n.code,{children:"MemorySource"})," state machine we will additionally use ",(0,o.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/sources/MemorySource.ts#L113",children:(0,o.jsx)(n.code,{children:"processRecentPlays"})})," from ",(0,o.jsx)(n.code,{children:"MemorySource"})," inside ",(0,o.jsx)(n.code,{children:"getRecentlyPlayed"}),". We pass track and/or player state returned from the Source service to ",(0,o.jsx)(n.code,{children:"processRecentPlayers"}),". It then takes care of deriving Source player state based on how this data changes over time. The advantage to using ",(0,o.jsx)(n.code,{children:"processRecentPlays"})," is that our Source service does not necessarily need to pass any player information -- as long as the track info has a ",(0,o.jsx)(n.strong,{children:"duration"})," we can more-or-less determine if it has been played long enough to scrobble."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/CoolPlayerSource.ts"',children:"import request from 'superagent';\nimport {\n SourceData,\n PlayerStateData,\n SINGLE_USER_PLATFORM_ID\n} from \"../common/infrastructure/Atomic.js\";\n// ...\nexport default class CoolPlayerSource extends MemorySource {\n\n // ...\n\n protected async getRecentlyPlayed(options: RecentlyPlayedOptions = {}): Promise {\n const plays: SourceData[] = [];\n try {\n // currently playing tracks/player state data\n const resp = await request\n .get(`${this.baseUrl}/now-playing`)\n .set('Authorization', `Token ${this.config.token}`);\n const {\n body: {\n playerState, // 'playing' or 'stopped' or 'paused'...\n position, // number of seconds into the track IE at position 48 -> ( 0:48/3:56 in player UI )\n play: { /* track data */}\n }\n } = resp;\n\n // transform into standard player state data\n const playerData: PlayerStateData = {\n platformId: SINGLE_USER_PLATFORM_ID,\n play: CoolPlayerSource.formatPlayObj(play),\n position\n };\n\n // if Cool Player does return player state we can also push a regular PlayObject to this list\n plays.push(playerData);\n } catch (e) {\n throw e;\n }\n\n // process player state through state machine\n // if the track changes or player state changes\n // and currently played track has been listened to long enough to be scrobbled it will return in newPlays\n const newPlays = this.processRecentPlays(plays);\n\n // finally, we return new plays and MS checks to see if they have been previously seen \n // before signalling to Clients that they can be scrobbled\n return newPlays;\n }\n}\n"})}),"\n",(0,o.jsxs)(n.p,{children:["Congratulations! Your ",(0,o.jsx)(n.code,{children:"CoolPlayerSource"})," has been minimally implemented and can now be used in multi-scrobbler."]}),"\n",(0,o.jsx)(n.h2,{id:"further-implementation",children:"Further Implementation"}),"\n",(0,o.jsx)(n.h3,{id:"backlog",children:"Backlog"}),"\n",(0,o.jsx)(n.p,{children:'To have your Source try to scrobble "missed" tracks when MS starts up the Source\'s service must be able to provide:'}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsx)(n.li,{children:"track information"}),"\n",(0,o.jsx)(n.li,{children:"timestamp of when the track was played"}),"\n"]}),"\n",(0,o.jsxs)(n.p,{children:["In your Source implement ",(0,o.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/sources/AbstractSource.ts#L235",children:(0,o.jsx)(n.code,{children:"getBackloggedPlays"})})," and set setting in constructor indicating it has backlogging capabilities:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/CoolPlayerSource.ts"',children:"import request from 'superagent';\nimport {\n PlayObject,\n} from \"../common/infrastructure/Atomic.js\";\n// ...\nexport default class CoolPlayerSource extends MemorySource {\n\n constructor(/* ... */) {\n super(/* ... */);\n // ...\n\n // tell MS it should try to get backlogged tracks on startup\n this.canBacklog = true;\n }\n\n // ...\n\n protected getBackloggedPlays = async (options: RecentlyPlayedOptions): Promise => {\n try {\n const resp = await request\n .get(`${this.baseUrl}/recent`)\n .set('Authorization', `Token ${this.config.token}`);\n\n // assuming list from body looks like track info returned in \n // \"Implement Play Object Transform\" section\n const {\n body = []\n } = resp;\n\n return body.map(x => CoolPlayerSource.formatPlayObj(x));\n } catch (e) {\n throw new Error('Error occurred while getting recently played', {cause: e});\n }\n\n }\n}\n"})}),"\n",(0,o.jsx)(n.h3,{id:"other-source-types",children:"Other Source Types"}),"\n",(0,o.jsx)(n.p,{children:"There are some scenarios where polling and/or state machine is not the right tool to handle determining if incoming data should be scrobbled:"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsx)(n.li,{children:"The Source service handles scrobble threshold internally, the data being received should always be scrobbled (WebScrobbler, Plex, Tautulli, Listenbrainz, Last.fm)"}),"\n",(0,o.jsx)(n.li,{children:"You prefer to handle the scrobble determination yourself"}),"\n"]}),"\n",(0,o.jsx)(n.h4,{id:"music-history-source",children:"Music History Source"}),"\n",(0,o.jsxs)(n.p,{children:["If the Source is still polling but the track returned should always be scrobbled if not already seen IE the Source service is a ",(0,o.jsx)(n.strong,{children:"music history source"})," (Listenbrainz, Last.fm), rather than a music player, then simply indicate to MS the source of truth type by setting it in the constructor. The state machine will always return a track if it is new and not seen, regardless of how recently it was seen:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/CoolPlayerSource.ts"',children:'import { SOURCE_SOT } from "../../core/Atomic.js";\n// ...\nexport default class CoolPlayerSource extends MemorySource {\n\n constructor(/* ... */) {\n super(/* ... */);\n // ...\n\n // tell MS it should immediately scrobble any new, unseen tracks from the upstream service\n this.playerSourceOfTruth = SOURCE_SOT.HISTORY;\n }\n}\n'})}),"\n",(0,o.jsx)(n.h4,{id:"non-polling-source",children:"Non-Polling Source"}),"\n",(0,o.jsxs)(n.p,{children:[(0,o.jsx)(n.strong,{children:"Ingress"})," Sources (like Plex, Tautulli, Webscrobbler, Jellyfin) do not having a polling mechanism because the upstream service contacts MS when there is an event, rather than MS contacting the upstream service."]}),"\n",(0,o.jsxs)(n.p,{children:["For these Sources you will need to implement endpoints in ",(0,o.jsx)(n.code,{children:"src/service/api.ts"})," and corresponding files. See the existing Sources in the project as references for how to do this."]}),"\n",(0,o.jsxs)(n.p,{children:["You may still wish to use the state machine ",(0,o.jsx)(n.code,{children:"MemorySource"}),' (like Jellyfin) if the events received are not "scrobble" events but instead of implementing ',(0,o.jsx)(n.code,{children:"getRecentlyPlayed"})," you will implement your own function in your Source class, like ",(0,o.jsx)(n.code,{children:"handle()"}),", that receives data and then uses ",(0,o.jsx)(n.code,{children:"processRecentPlays"}),"."]}),"\n",(0,o.jsx)(n.p,{children:"After new plays have been determined see the next section for how to scrobble..."}),"\n",(0,o.jsx)(n.h4,{id:"basic-source",children:"Basic Source"}),"\n",(0,o.jsxs)(n.p,{children:["At the core of a Source that implements ",(0,o.jsx)(n.code,{children:"AbstractSource"}),"'s functionality is the ability to ",(0,o.jsx)(n.strong,{children:"Discover"})," and ",(0,o.jsx)(n.strong,{children:"Scrobble"})," plays."]}),"\n",(0,o.jsxs)(n.p,{children:["These functions are not seen in the MVP ",(0,o.jsx)(n.code,{children:"CoolPlayerSource"})," because they are automatically done by the polling functionality after being returned from ",(0,o.jsx)(n.code,{children:"getRecentlyPlayed"}),"."]}),"\n",(0,o.jsx)(n.h5,{id:"discovery",children:"Discovery"}),"\n",(0,o.jsxs)(n.p,{children:['A Source keeps track of all the "plays" that are determined to be valid for scrobbling. When a play is valid it is checked to see if it has already been "discovered" by comparing the track info and timestamp of the play against already discovered plays. This prevents duplicate scrobbling by using the Source\'s own data and simplifies scrobbling for Sources by allowing your implementation to "always" ingest track data without having to worry about whether its new or not -- ',(0,o.jsx)(n.code,{children:"AbstractSource"})," and ",(0,o.jsx)(n.code,{children:"discover()"})," will take care of that for you."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/MyBasicSource.ts"',children:'export default class MyBasicSource extends AbstractSource {\n handle(somePlay: PlayObject) {\n // if the track is "new" and not seen before it is returned in the discovered list\n // we then know it is OK to be sent to Clients for scrobbling\n const discovered: PlayObject[] = this.discover([somePlay]);\n }\n}\n'})}),"\n",(0,o.jsx)(n.p,{children:'This additionally will be surfaced to the user in the Dashboard in the "Tracks Discovered" page.'}),"\n",(0,o.jsx)(n.h5,{id:"scrobbling",children:"Scrobbling"}),"\n",(0,o.jsx)(n.p,{children:"After a play is verified to be discovered we can then scrobble it. This will emit the plays to the ScrobbleClients service which then disseminates the play to all Clients that were configured to listen in the Source's config."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/MyBasicSource.ts"',children:"export default class MyBasicSourceSource extends AbstractSource {\n handle(somePlay: PlayObject) {\n const discovered: PlayObject[] = this.discover([somePlay]);\n // emit plays that can be scrobbled by clients\n this.scrobble(discovered);\n }\n}\n"})}),"\n",(0,o.jsxs)(n.p,{children:["If your service only emits an event when a play is scrobbled you can ",(0,o.jsx)(n.em,{children:"technically"})," skip using ",(0,o.jsx)(n.code,{children:"discover()"})," but it is good practice to use it unless you have a very good reason not to."]}),"\n",(0,o.jsx)(n.admonition,{type:"note",children:(0,o.jsxs)(n.p,{children:["Using ",(0,o.jsx)(n.code,{children:"scrobble()"}),' does not guarantee a track is actually scrobbled! The Scrobble Clients also check the play against their own "recently scrobbled" list to prevent duplicates.']})})]})}function h(e={}){let{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}}}]); \ No newline at end of file diff --git a/assets/js/33c4ed3f.ead8b782.js b/assets/js/33c4ed3f.ead8b782.js new file mode 100644 index 00000000..96a46919 --- /dev/null +++ b/assets/js/33c4ed3f.ead8b782.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["429"],{8779(e,n,r){r.r(n),r.d(n,{metadata:()=>t,default:()=>h,frontMatter:()=>i,contentTitle:()=>a,toc:()=>c,assets:()=>l});var t=JSON.parse('{"id":"development/dev-source","title":"Source Development/Tutorial","description":"Developing a new Source","source":"@site/docs/development/dev-source.md","sourceDirName":"development","slug":"/development/dev-source","permalink":"/multi-scrobbler/development/dev-source","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/development/dev-source.md","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"toc_min_heading_level":2,"toc_max_heading_level":5,"sidebar_position":2,"title":"Source Development/Tutorial","description":"Developing a new Source"},"sidebar":"tutorialSidebar","previous":{"title":"Common Development","permalink":"/multi-scrobbler/development/dev-common"},"next":{"title":"Flatpak","permalink":"/multi-scrobbler/development/flatpak"}}'),o=r(4848),s=r(8453);let i={toc_min_heading_level:2,toc_max_heading_level:5,sidebar_position:2,title:"Source Development/Tutorial",description:"Developing a new Source"},a,l={},c=[{value:"Scenario",id:"scenario",level:2},{value:"Minimal Implementation",id:"minimal-implementation",level:2},{value:"Define and Implement Config",id:"define-and-implement-config",level:3},{value:"Create CoolPlayer Source",id:"create-coolplayer-source",level:3},{value:"Initialize Source from Config",id:"initialize-source-from-config",level:3},{value:"Implement Play Object Transform",id:"implement-play-object-transform",level:3},{value:"Implement Stages",id:"implement-stages",level:3},{value:"Build Data",id:"build-data",level:4},{value:"Check Connection",id:"check-connection",level:4},{value:"Test Auth",id:"test-auth",level:4},{value:"Implement Polling",id:"implement-polling",level:3},{value:"Further Implementation",id:"further-implementation",level:2},{value:"Backlog",id:"backlog",level:3},{value:"Other Source Types",id:"other-source-types",level:3},{value:"Music History Source",id:"music-history-source",level:4},{value:"Non-Polling Source",id:"non-polling-source",level:4},{value:"Basic Source",id:"basic-source",level:4},{value:"Discovery",id:"discovery",level:5},{value:"Scrobbling",id:"scrobbling",level:5}];function d(e){let n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",h5:"h5",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsxs)(n.p,{children:["This document will provide a step-by-step guide for creating a (trivial) new Source in MS alongside describing what aspects of the Source need to be implemented based on the service you use. Before using this document you should review ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#common-development",children:"Common Development"}),"."]}),"\n",(0,o.jsx)(n.h2,{id:"scenario",children:"Scenario"}),"\n",(0,o.jsxs)(n.p,{children:["You are the developer of a fancy, new self-hosted web-based media player called ",(0,o.jsx)(n.strong,{children:"Cool Player."})," Cool Player has a slick interface and many bells and whistles, but most importantly it has an API. The API:"]}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["Has an unauthenticated health endpoint at ",(0,o.jsx)(n.code,{children:"/api/health"})," that returns ",(0,o.jsx)(n.code,{children:"200"})," if the service is running properly"]}),"\n",(0,o.jsxs)(n.li,{children:["Has authenticated endpoints that require a user-generated token in the header ",(0,o.jsx)(n.code,{children:"Authorization MY_TOKEN"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["Has a ",(0,o.jsx)(n.code,{children:"/api/recent"})," endpoint that lists recently played tracks with a timestamp"]}),"\n",(0,o.jsxs)(n.li,{children:["Has a ",(0,o.jsx)(n.code,{children:"/api/now-playing"})," endpoint that returns information about the state of the player like current track, player position in the track, etc..."]}),"\n"]}),"\n"]}),"\n",(0,o.jsxs)(n.li,{children:["Cool Player is by default accessed on port ",(0,o.jsx)(n.code,{children:"6969"})]}),"\n",(0,o.jsxs)(n.li,{children:["Your personal instance of Cool Player is hosted at ",(0,o.jsx)(n.code,{children:"http://192.168.0.100:6969"})," and the api is accessed at ",(0,o.jsx)(n.code,{children:"http://192.168.0.100:6969/api"})]}),"\n"]}),"\n",(0,o.jsxs)(n.p,{children:["Because there is an API that MS can actively read this will be a ",(0,o.jsx)(n.strong,{children:"polling"})," Source where MS sends requests to Cool Player to get scrobble information -- as opposed to an ",(0,o.jsx)(n.strong,{children:"ingress"})," Source like Jellyfin/Plex that uses webhooks from the service to send data to MS."]}),"\n",(0,o.jsx)(n.h2,{id:"minimal-implementation",children:"Minimal Implementation"}),"\n",(0,o.jsx)(n.h3,{id:"define-and-implement-config",children:"Define and Implement Config"}),"\n",(0,o.jsxs)(n.p,{children:["We will create a new config interface for Cool Player using the ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#config",children:"Common Config"})," and tell MS it is a valid config that can be used."]}),"\n",(0,o.jsx)(n.p,{children:"Create a new file for your config:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="/src/backend/common/infrastructure/config/source/coolplayer.ts"',children:"import { PollingOptions } from \"../common.js\";\nimport { CommonSourceConfig, CommonSourceData } from \"./index.js\";\n\n// all of the required data for the Build Data and Test Auth stages (from Common Development docs)\n// should go here\nexport interface CoolPlayerSourceData extends CommonSourceData, PollingOptions {\n// remember to annotation your properties!\n\n /**\n * The user-generated token for Cool Player auth created in Cool Player -> Settings -> User -> Tokens\n *\n * @example f243331e-cf5b-49d7-846b-0845bdc965b4\n * */\n token: string\n /**\n * The host and port where Cool Player is hosted\n *\n * @example http://192.168.0.100:6969\n * */\n baseUrl: string\n}\n\nexport interface CoolPlayerSourceConfig extends CommonSourceConfig {\n data: CoolPlayerSourceData\n}\n\nexport interface CoolPlayerSourceAIOConfig extends CoolPlayerSourceConfig {\n // when using the all-in-one 'config.json' this is how users will identify this source\n type: 'coolplayer'\n}\n"})}),"\n",(0,o.jsx)(n.p,{children:"Add the new interface to the list of valid interfaces for sources:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/common/infrastructure/config/source/sources.ts"',children:'import { ChromecastSourceAIOConfig, ChromecastSourceConfig } from "./chromecast.js";\n// ...\n// highlight-next-line\nimport { CoolPlayerSourceAIOConfig, CoolPlayerSourceConfig } from "./coolplayer.js";\n\nexport type SourceConfig =\n SpotifySourceConfig\n // ...\n // highlight-next-line\n | CoolPlayerSourceConfig;\n\nexport type SourceAIOConfig =\n SpotifySourceAIOConfig\n // ...\n // highlight-next-line\n | CoolPlayerSourceAIOConfig;\n'})}),"\n",(0,o.jsx)(n.p,{children:"Finally, add the source type identifier to the list of valid identifiers"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/common/infrastructure/Atomic.ts"',children:"export type SourceType =\n 'spotify'\n // ...\n // highlight-next-line\n | 'coolplayer';\n\nexport const sourceTypes: SourceType[] = [\n 'spotify',\n // ...\n // highlight-next-line\n 'coolplayer'\n];\n"})}),"\n",(0,o.jsxs)(n.p,{children:["Now we will create a new Source inheriting from ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#concrete-class",children:(0,o.jsx)(n.code,{children:"AbstractComponent"})})," that:"]}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsx)(n.li,{children:"accepts our config interface"}),"\n",(0,o.jsxs)(n.li,{children:["implements a function to transform CoolPlayer's track data into a ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#play-object",children:(0,o.jsx)(n.strong,{children:"PlayObject"})})]}),"\n",(0,o.jsxs)(n.li,{children:["implements required ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#stages",children:"stages"})]}),"\n",(0,o.jsx)(n.li,{children:"implements required methods to current player state and/or now playing track"}),"\n"]}),"\n",(0,o.jsx)(n.h3,{id:"create-coolplayer-source",children:"Create CoolPlayer Source"}),"\n",(0,o.jsxs)(n.p,{children:["First we create a new Source called ",(0,o.jsx)(n.code,{children:"CoolPlayerSource"})," and setup our constructor to accept the config and ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#stage-test-auth",children:"specify Auth behavior."})]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/SpotifySource.ts"',children:'import { CoolPlayerSourceConfig } from "../common/infrastructure/config/source/coolplayer.js";\nimport MemorySource from "./MemorySource.js";\nimport {\n InternalConfig,\n} from "../common/infrastructure/Atomic.js";\n\n// MemorySource is the base class used for polling-type Sources\nexport default class CoolPlayerSource extends MemorySource {\n\n // type hints for TS to know what the base class config looks like\n declare config: CoolPlayerSourceConfig;\n\n constructor(name: any, config: CoolPlayerSourceConfig, internal: InternalConfig, emitter: EventEmitter) {\n super(\'coolplayer\', name, config, internal, emitter);\n\n // Cool Player required authentication\n this.requiresAuth = true;\n // but does not require user interaction for auth to work\n this.requiresAuthInteraction = false;\n // tells MS this is a Source that can be activity monitored through API\n this.canPoll = true;\n }\n}\n'})}),"\n",(0,o.jsx)(n.h3,{id:"initialize-source-from-config",children:"Initialize Source from Config"}),"\n",(0,o.jsxs)(n.p,{children:["When MS starts it reads all configs and determines which Source to build based on the configs found. We need to tell it to build a ",(0,o.jsx)(n.code,{children:"CoolPlayerSource"})," when a ",(0,o.jsx)(n.code,{children:"coolplayer"})," config type is found."]}),"\n",(0,o.jsxs)(n.p,{children:["We modify ",(0,o.jsx)(n.code,{children:"ScrobbleSources.ts"})," to add ",(0,o.jsx)(n.code,{children:"CoolPlayerSource"})," as an option:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/ScrobbleSources.ts"',children:"// ...\nimport { CoolPlayerSource, CoolPlayerData } from \"./CoolPlayerSource.js\";\n\nexport default class ScrobbleSources {\n // ...\n buildSourcesFromConfig = async (additionalConfigs: ParsedConfig[] = []) => {\n // ...\n\n // if CoolPlayerSource should be able to be built from ENVs only \n // then add it as a case statement here\n for (const sourceType of sourceTypes) {\n switch (sourceType) {\n // ...\n case 'musikcube':\n // ...\n break;\n // highlight-start\n case 'coolplayer':\n const cp = {\n baseUrl: process.env.COOL_URL,\n token: process.env.COOL_TOKEN\n }\n if (!Object.values(cp).every(x => x === undefined)) {\n configs.push({\n type: 'coolplayer',\n name: 'unnamed',\n source: 'ENV',\n mode: 'single',\n configureAs: defaultConfigureAs,\n data: cp as CoolPlayerData\n });\n }\n break;\n // highlight-end\n default:\n break;\n }\n }\n }\n\n // ...\n\n // (required) create new CoolPlayerSource if source config type is 'coolplayer'\n addSource = async (clientConfig: ParsedConfig, defaults: SourceDefaults = {}) => {\n // ...\n let newSource: AbstractSource;\n switch (type) {\n // ...\n case 'musikcube':\n // ...\n break;\n // highlight-start\n case 'coolplayer':\n newSource = await new CoolPlayerSource(name, compositeConfig as CoolPlayerSourceConfig, internal, this.emitter);\n break;\n // highlight-end\n default:\n break;\n }\n }\n}\n"})}),"\n",(0,o.jsx)(n.h3,{id:"implement-play-object-transform",children:"Implement Play Object Transform"}),"\n",(0,o.jsxs)(n.p,{children:["Now we will create a static function that is used to take the track data returned from Cool Player's API and return a standard ",(0,o.jsxs)(n.a,{href:"/multi-scrobbler/development/dev-common#play-object",children:[(0,o.jsx)(n.code,{children:"PlayObject"}),"."]})]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/CoolPlayerSource.ts"',children:'import dayjs from "dayjs";\nimport {\n FormatPlayObjectOptions,\n} from "../common/infrastructure/Atomic.js";\nimport { PlayObject } from "../../core/Atomic.js";\n\nexport default class CoolPlayerSource extends MemorySource {\n // ...\n\n // \'obj\' should ideally be a real TS interface\n // if CoolPlayer has a ts/js client we would use that otherwise\n // we should build our own interfaces to represent track data from Cool Player\'s API\n static formatPlayObj(obj: any, options: FormatPlayObjectOptions = {}): PlayObject {\n const {\n trackName,\n artistName,\n albumName,\n duration,\n playedAt,\n } = obj;\n\n return {\n data: {\n artists: [artistName],\n album: albumName,\n track: trackName,\n // assuming seconds\n duration,\n // assuming playedAt is an ISO8601 timestamp\n playDate: dayjs(playedAt)\n },\n meta: {\n source: \'CoolPlayer\'\n }\n }\n }\n}\n'})}),"\n",(0,o.jsx)(n.h3,{id:"implement-stages",children:"Implement Stages"}),"\n",(0,o.jsxs)(n.p,{children:["Next we will implement the ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#stages",children:"Stages"})," required to get CoolPlayerSource running."]}),"\n",(0,o.jsx)(n.h4,{id:"build-data",children:"Build Data"}),"\n",(0,o.jsxs)(n.p,{children:["First we implement the ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#stage-build-data",children:"Build Data Stage"}),". We will check that the ",(0,o.jsx)(n.code,{children:"token"})," and ",(0,o.jsx)(n.code,{children:"baseUrl"})," properties have been provided by the user. Additionally, we will parse the baseUrl and add default ports/prefix."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/CoolPlayerSource.ts"',children:"import { URL } from \"url\";\n// ...\n\nexport default class CoolPlayerSource extends MemorySource {\n\n baseUrl!: URL;\n\n // ...\n\n static parseConnectionUrl(val: string) {\n const normal = normalizeUrl(val, {removeTrailingSlash: false, normalizeProtocol: true})\n const url = new URL(normal);\n\n if (url.port === null || url.port === '') {\n url.port = '6969';\n }\n if (url.pathname === '/') {\n url.pathname = '/api/';\n }\n return url;\n }\n\n protected async doBuildInitData(): Promise {\n const {\n token,\n baseUrl\n } = this.config;\n if (token === null || token === undefined || (typeof token === 'string' && token.trim() === '')) {\n throw new Error(`'token' must be defined`);\n }\n\n if (baseUrl === null || baseUrl === undefined || (typeof baseUrl === 'string' && baseUrl.trim() === '')) {\n throw new Error(`'baseUrl' must be defined`);\n }\n try {\n this.baseUrl = CoolPlayerSource.parseConnectionUrl(baseUrl);\n } catch (e) {\n throw new Error(`Could not parse baseUrl: ${baseUrl}`, {cause: e});\n }\n\n this.logger.verbose(`Config URL: ${baseUrl} => Normalized: '${this.url.toString()}'`);\n return true;\n }\n}\n"})}),"\n",(0,o.jsx)(n.h4,{id:"check-connection",children:"Check Connection"}),"\n",(0,o.jsxs)(n.p,{children:["Second we will implement the ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#stage-check-connection",children:"Check Connection Stage"}),":"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/CoolPlayerSource.ts"',children:"import request from 'superagent';\nimport { UpstreamError } from \"../common/errors/UpstreamError.js\";\n// ...\nexport default class CoolPlayerSource extends MemorySource {\n\n // ...\n\n protected async doCheckConnection(): Promise {\n try {\n const resp = await request.get(`${this.baseUrl}/health`);\n return true;\n // if /health returned version info we could instead read response and return a string with version info for visibility to the user\n // return `Cool Player Version: ${resp.body.version}`;\n } catch (e) {\n throw e;\n }\n }\n}\n"})}),"\n",(0,o.jsx)(n.h4,{id:"test-auth",children:"Test Auth"}),"\n",(0,o.jsxs)(n.p,{children:["Finally, we will implement ",(0,o.jsx)(n.a,{href:"/multi-scrobbler/development/dev-common#stage-test-auth",children:"Auth Test Stage"}),":"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/CoolPlayerSource.ts"',children:"import request from 'superagent';\nimport { UpstreamError } from \"../common/errors/UpstreamError.js\";\n// ...\nexport default class CoolPlayerSource extends MemorySource {\n\n // ...\n\n doAuthentication = async () => {\n try {\n const resp = await request\n .get(`${this.baseUrl}/recent`)\n .set('Authorization', `Token ${this.config.token}`);\n return true;\n } catch (e) {\n // if Cool Player returned an error as json we could parse it from error body and throw new Error with the message\n throw e;\n }\n }\n}\n"})}),"\n",(0,o.jsx)(n.h3,{id:"implement-polling",children:"Implement Polling"}),"\n",(0,o.jsxs)(n.p,{children:["The majority of Sources MS monitors primarily operate as a source of truth for a ",(0,o.jsx)(n.strong,{children:"music player"})," rather than a ",(0,o.jsx)(n.strong,{children:"played music history."})," Only Listenbrainz and Last.fm operate as a source of music history."]}),"\n",(0,o.jsxs)(n.p,{children:["To this end, MS implements a ",(0,o.jsx)(n.a,{href:"https://www.freecodecamp.org/news/state-machines-basics-of-computer-science-d42855debc66/",children:"state machine"}),' that emulates the behavior of a music player in order to keep track of when a song you are listening to should be scrobbled. It does this by monitoring the "currently playing" track reported by a Source\'s service, with varying degrees of accuracy depending on what information is returned from the service. The state machine is implemented in ',(0,o.jsx)(n.code,{children:"MemorySource"})," which our ",(0,o.jsx)(n.code,{children:"CoolPlayerSource"})," inherits from."]}),"\n",(0,o.jsxs)(n.p,{children:["For a polling Source to work properly we need to implement a function, ",(0,o.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/sources/AbstractSource.ts#L92",children:(0,o.jsx)(n.code,{children:"getRecentlyPlayed"})}),', that returns PlayObjects that are "newly" played. These are then checked against previously "discovered" plays and their timestamp to determine if they should be surfaced to Clients to scrobble.']}),"\n",(0,o.jsxs)(n.p,{children:["To take advantage of the ",(0,o.jsx)(n.code,{children:"MemorySource"})," state machine we will additionally use ",(0,o.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/sources/MemorySource.ts#L113",children:(0,o.jsx)(n.code,{children:"processRecentPlays"})})," from ",(0,o.jsx)(n.code,{children:"MemorySource"})," inside ",(0,o.jsx)(n.code,{children:"getRecentlyPlayed"}),". We pass track and/or player state returned from the Source service to ",(0,o.jsx)(n.code,{children:"processRecentPlayers"}),". It then takes care of deriving Source player state based on how this data changes over time. The advantage to using ",(0,o.jsx)(n.code,{children:"processRecentPlays"})," is that our Source service does not necessarily need to pass any player information -- as long as the track info has a ",(0,o.jsx)(n.strong,{children:"duration"})," we can more-or-less determine if it has been played long enough to scrobble."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/CoolPlayerSource.ts"',children:"import request from 'superagent';\nimport {\n SourceData,\n PlayerStateData,\n SINGLE_USER_PLATFORM_ID\n} from \"../common/infrastructure/Atomic.js\";\n// ...\nexport default class CoolPlayerSource extends MemorySource {\n\n // ...\n\n protected async getRecentlyPlayed(options: RecentlyPlayedOptions = {}): Promise {\n const plays: SourceData[] = [];\n try {\n // currently playing tracks/player state data\n const resp = await request\n .get(`${this.baseUrl}/now-playing`)\n .set('Authorization', `Token ${this.config.token}`);\n const {\n body: {\n playerState, // 'playing' or 'stopped' or 'paused'...\n position, // number of seconds into the track IE at position 48 -> ( 0:48/3:56 in player UI )\n play: { /* track data */}\n }\n } = resp;\n\n // transform into standard player state data\n const playerData: PlayerStateData = {\n platformId: SINGLE_USER_PLATFORM_ID,\n play: CoolPlayerSource.formatPlayObj(play),\n position\n };\n\n // if Cool Player does return player state we can also push a regular PlayObject to this list\n plays.push(playerData);\n } catch (e) {\n throw e;\n }\n\n // process player state through state machine\n // if the track changes or player state changes\n // and currently played track has been listened to long enough to be scrobbled it will return in newPlays\n const newPlays = this.processRecentPlays(plays);\n\n // finally, we return new plays and MS checks to see if they have been previously seen \n // before signalling to Clients that they can be scrobbled\n return newPlays;\n }\n}\n"})}),"\n",(0,o.jsxs)(n.p,{children:["Congratulations! Your ",(0,o.jsx)(n.code,{children:"CoolPlayerSource"})," has been minimally implemented and can now be used in multi-scrobbler."]}),"\n",(0,o.jsx)(n.h2,{id:"further-implementation",children:"Further Implementation"}),"\n",(0,o.jsx)(n.h3,{id:"backlog",children:"Backlog"}),"\n",(0,o.jsx)(n.p,{children:'To have your Source try to scrobble "missed" tracks when MS starts up the Source\'s service must be able to provide:'}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsx)(n.li,{children:"track information"}),"\n",(0,o.jsx)(n.li,{children:"timestamp of when the track was played"}),"\n"]}),"\n",(0,o.jsxs)(n.p,{children:["In your Source implement ",(0,o.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/sources/AbstractSource.ts#L235",children:(0,o.jsx)(n.code,{children:"getBackloggedPlays"})})," and set setting in constructor indicating it has backlogging capabilities:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/CoolPlayerSource.ts"',children:"import request from 'superagent';\nimport {\n PlayObject,\n} from \"../common/infrastructure/Atomic.js\";\n// ...\nexport default class CoolPlayerSource extends MemorySource {\n\n constructor(/* ... */) {\n super(/* ... */);\n // ...\n\n // tell MS it should try to get backlogged tracks on startup\n this.canBacklog = true;\n }\n\n // ...\n\n protected getBackloggedPlays = async (options: RecentlyPlayedOptions): Promise => {\n try {\n const resp = await request\n .get(`${this.baseUrl}/recent`)\n .set('Authorization', `Token ${this.config.token}`);\n\n // assuming list from body looks like track info returned in \n // \"Implement Play Object Transform\" section\n const {\n body = []\n } = resp;\n\n return body.map(x => CoolPlayerSource.formatPlayObj(x));\n } catch (e) {\n throw new Error('Error occurred while getting recently played', {cause: e});\n }\n\n }\n}\n"})}),"\n",(0,o.jsx)(n.h3,{id:"other-source-types",children:"Other Source Types"}),"\n",(0,o.jsx)(n.p,{children:"There are some scenarios where polling and/or state machine is not the right tool to handle determining if incoming data should be scrobbled:"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsx)(n.li,{children:"The Source service handles scrobble threshold internally, the data being received should always be scrobbled (WebScrobbler, Plex, Tautulli, Listenbrainz, Last.fm)"}),"\n",(0,o.jsx)(n.li,{children:"You prefer to handle the scrobble determination yourself"}),"\n"]}),"\n",(0,o.jsx)(n.h4,{id:"music-history-source",children:"Music History Source"}),"\n",(0,o.jsxs)(n.p,{children:["If the Source is still polling but the track returned should always be scrobbled if not already seen IE the Source service is a ",(0,o.jsx)(n.strong,{children:"music history source"})," (Listenbrainz, Last.fm), rather than a music player, then simply indicate to MS the source of truth type by setting it in the constructor. The state machine will always return a track if it is new and not seen, regardless of how recently it was seen:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/CoolPlayerSource.ts"',children:'import { SOURCE_SOT } from "../../core/Atomic.js";\n// ...\nexport default class CoolPlayerSource extends MemorySource {\n\n constructor(/* ... */) {\n super(/* ... */);\n // ...\n\n // tell MS it should immediately scrobble any new, unseen tracks from the upstream service\n this.playerSourceOfTruth = SOURCE_SOT.HISTORY;\n }\n}\n'})}),"\n",(0,o.jsx)(n.h4,{id:"non-polling-source",children:"Non-Polling Source"}),"\n",(0,o.jsxs)(n.p,{children:[(0,o.jsx)(n.strong,{children:"Ingress"})," Sources (like Plex, Tautulli, Webscrobbler, Jellyfin) do not having a polling mechanism because the upstream service contacts MS when there is an event, rather than MS contacting the upstream service."]}),"\n",(0,o.jsxs)(n.p,{children:["For these Sources you will need to implement endpoints in ",(0,o.jsx)(n.code,{children:"src/service/api.ts"})," and corresponding files. See the existing Sources in the project as references for how to do this."]}),"\n",(0,o.jsxs)(n.p,{children:["You may still wish to use the state machine ",(0,o.jsx)(n.code,{children:"MemorySource"}),' (like Jellyfin) if the events received are not "scrobble" events but instead of implementing ',(0,o.jsx)(n.code,{children:"getRecentlyPlayed"})," you will implement your own function in your Source class, like ",(0,o.jsx)(n.code,{children:"handle()"}),", that receives data and then uses ",(0,o.jsx)(n.code,{children:"processRecentPlays"}),"."]}),"\n",(0,o.jsx)(n.p,{children:"After new plays have been determined see the next section for how to scrobble..."}),"\n",(0,o.jsx)(n.h4,{id:"basic-source",children:"Basic Source"}),"\n",(0,o.jsxs)(n.p,{children:["At the core of a Source that implements ",(0,o.jsx)(n.code,{children:"AbstractSource"}),"'s functionality is the ability to ",(0,o.jsx)(n.strong,{children:"Discover"})," and ",(0,o.jsx)(n.strong,{children:"Scrobble"})," plays."]}),"\n",(0,o.jsxs)(n.p,{children:["These functions are not seen in the MVP ",(0,o.jsx)(n.code,{children:"CoolPlayerSource"})," because they are automatically done by the polling functionality after being returned from ",(0,o.jsx)(n.code,{children:"getRecentlyPlayed"}),"."]}),"\n",(0,o.jsx)(n.h5,{id:"discovery",children:"Discovery"}),"\n",(0,o.jsxs)(n.p,{children:['A Source keeps track of all the "plays" that are determined to be valid for scrobbling. When a play is valid it is checked to see if it has already been "discovered" by comparing the track info and timestamp of the play against already discovered plays. This prevents duplicate scrobbling by using the Source\'s own data and simplifies scrobbling for Sources by allowing your implementation to "always" ingest track data without having to worry about whether its new or not -- ',(0,o.jsx)(n.code,{children:"AbstractSource"})," and ",(0,o.jsx)(n.code,{children:"discover()"})," will take care of that for you."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/MyBasicSource.ts"',children:'export default class MyBasicSource extends AbstractSource {\n handle(somePlay: PlayObject) {\n // if the track is "new" and not seen before it is returned in the discovered list\n // we then know it is OK to be sent to Clients for scrobbling\n const discovered: PlayObject[] = this.discover([somePlay]);\n }\n}\n'})}),"\n",(0,o.jsx)(n.p,{children:'This additionally will be surfaced to the user in the Dashboard in the "Tracks Discovered" page.'}),"\n",(0,o.jsx)(n.h5,{id:"scrobbling",children:"Scrobbling"}),"\n",(0,o.jsx)(n.p,{children:"After a play is verified to be discovered we can then scrobble it. This will emit the plays to the ScrobbleClients service which then disseminates the play to all Clients that were configured to listen in the Source's config."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-ts",metastring:'title="src/backend/sources/MyBasicSource.ts"',children:"export default class MyBasicSourceSource extends AbstractSource {\n handle(somePlay: PlayObject) {\n const discovered: PlayObject[] = this.discover([somePlay]);\n // emit plays that can be scrobbled by clients\n this.scrobble(discovered);\n }\n}\n"})}),"\n",(0,o.jsxs)(n.p,{children:["If your service only emits an event when a play is scrobbled you can ",(0,o.jsx)(n.em,{children:"technically"})," skip using ",(0,o.jsx)(n.code,{children:"discover()"})," but it is good practice to use it unless you have a very good reason not to."]}),"\n",(0,o.jsx)(n.admonition,{type:"note",children:(0,o.jsxs)(n.p,{children:["Using ",(0,o.jsx)(n.code,{children:"scrobble()"}),' does not guarantee a track is actually scrobbled! The Scrobble Clients also check the play against their own "recently scrobbled" list to prevent duplicates.']})})]})}function h(e={}){let{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}}}]); \ No newline at end of file diff --git a/assets/js/44b2647e.1da2d676.js b/assets/js/44b2647e.1da2d676.js new file mode 100644 index 00000000..4cea28d2 --- /dev/null +++ b/assets/js/44b2647e.1da2d676.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["6942"],{5077(e,n,s){s.r(n),s.d(n,{metadata:()=>r,default:()=>h,frontMatter:()=>t,contentTitle:()=>l,toc:()=>c,assets:()=>o});var r=JSON.parse('{"id":"configuration/transforms/user","title":"User Stage","description":"Enhancing Scrobbles with Regex Parsing","source":"@site/docs/configuration/transforms/user.mdx","sourceDirName":"configuration/transforms","slug":"/configuration/transforms/user","permalink":"/multi-scrobbler/configuration/transforms/user","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/transforms/user.mdx","tags":[],"version":"current","frontMatter":{"title":"User Stage","description":"Enhancing Scrobbles with Regex Parsing","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Native Stage","permalink":"/multi-scrobbler/configuration/transforms/native"},"next":{"title":"Development","permalink":"/multi-scrobbler/category/development"}}'),i=s(4848),a=s(8453);let t={title:"User Stage",description:"Enhancing Scrobbles with Regex Parsing",toc_min_heading_level:2,toc_max_heading_level:5},l,o={},c=[{value:"Configuration",id:"configuration",level:2},{value:"Default Stage",id:"default-stage",level:4},{value:"Search-And-Replace Expression",id:"search-and-replace-expression",level:3},{value:"Rules",id:"rules",level:3},{value:"Examples",id:"examples",level:2},{value:"Usage with when condition",id:"usage-with-when-condition",level:3},{value:"Remove phrase from Title in all new Plays",id:"remove-phrase-from-title-in-all-new-plays",level:3},{value:"Remove all parenthesized content from the end of a title",id:"remove-all-parenthesized-content-from-the-end-of-a-title",level:3},{value:"Rename misspelled artist in all new Plays",id:"rename-misspelled-artist-in-all-new-plays",level:3},{value:"Remove "Various Artists" albums in all new Plays",id:"remove-various-artists-albums-in-all-new-plays",level:3},{value:"Extract primary Artist from delimited, multi-Artist string",id:"extract-primary-artist-from-delimited-multi-artist-string",level:3}];function d(e){let n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",h4:"h4",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.R)(),...e.components},{Details:s}=n;return s||function(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.strong,{children:"User"})," ",(0,i.jsx)(n.a,{href:"/configuration/transforms#stage",children:"Stage"})," uses ",(0,i.jsxs)(n.a,{href:"#search-and-replace-expression",children:[(0,i.jsx)(n.strong,{children:"search-and-replace"})," expressions"]}),", provided by you, to modify/replace parts of Play data."]}),"\n",(0,i.jsx)(n.p,{children:"This Stage is most useful for correcting individual instances of bad data, or patterns, in your known data. Example scenarios:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Removing ",(0,i.jsx)(n.code,{children:"(Album Version)"})," from all Titles"]}),"\n",(0,i.jsxs)(n.li,{children:["Removing ",(0,i.jsx)(n.code,{children:"Various Artists"})," from Artist data"]}),"\n",(0,i.jsx)(n.li,{children:"Correcting spelling mistakes for individual Artist names"}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["The user ",(0,i.jsxs)(n.a,{href:"/configuration/transforms#stage",children:["Stage ",(0,i.jsx)(n.code,{children:"type"})]})," is ",(0,i.jsx)(n.code,{children:"user"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["All ",(0,i.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," is done using ",(0,i.jsxs)(n.a,{href:"#search-and-replace-expression",children:[(0,i.jsx)(n.strong,{children:"search-and-replace"})," expressions"]})," inside individual ",(0,i.jsx)(n.a,{href:"#rules",children:"rules"}),". Default configuration for all Rules can still be done using Stage ",(0,i.jsx)(n.code,{children:"defaults"}),"."]}),"\n",(0,i.jsxs)(s,{children:[(0,i.jsx)("summary",{children:"Example"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "user",\n "name": "Normalizer",\n "defaults": {\n "title": [\n "(Album Version)"\n ],\n "artists": [\n "Various Artists"\n ]\n }\n }\n ]\n}\n'})})]}),"\n",(0,i.jsx)(n.h4,{id:"default-stage",children:"Default Stage"}),"\n",(0,i.jsxs)(n.p,{children:["For your convenience, if you do not define any User ",(0,i.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configurations"})," then multi-scrobbler automatically builds an empty one for you."]}),"\n",(0,i.jsxs)(n.p,{children:["This can be used by omitting the ",(0,i.jsx)(n.code,{children:"name"})," in your ",(0,i.jsx)(n.a,{href:"/configuration/transforms#stage",children:"modification Stage"}),"."]}),"\n",(0,i.jsxs)(s,{children:[(0,i.jsx)("summary",{children:"Example"}),(0,i.jsxs)(n.p,{children:["In a ",(0,i.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," ",(0,i.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "user",\n // do not include name, the default user stage will be used\n "title": [\n // removes badterm from track title\n "badterm"\n ]\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,i.jsx)(n.h3,{id:"search-and-replace-expression",children:"Search-And-Replace Expression"}),"\n",(0,i.jsx)(n.p,{children:"A Search-And-Replace Expression can be a plain string that matches a literal, then removes it:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'Expression: "badTerm"\n\n"this is badTerm cool string" => "this is a cool string"\n'})}),"\n",(0,i.jsx)(n.p,{children:"or a regular expression that matches and removes the match:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'Expression: "/bad\\w+/i"\n\n"this is badSomething cool string" => "this is a cool string"\n'})}),"\n",(0,i.jsx)(n.p,{children:"Or it may be an object that specifies what to match (using either plain string or regular expression) and what to replace it with:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",children:'{\n "search": "anotherBadTerm", // match all instances of \'anotherBadTerm\'\n "replace": "goodTerm" // replace with the string \'goodTerm\'\n}\n'})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'"this is anotherBadTerm cool string" => "this is goodTerm cool string"\n'})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",children:'{\n "search": "/^\\(\\w+.com)/i", // matches any string that starts with EX \'(YourMusic.com)\'\n "replace": "[MySite.com]" // replace with the string \'[MySite.com]\'\n}\n'})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'"(Foo.com) this is a cool string" => "[MySite.com] this is a cool string"\n'})}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"replace"})," property uses javascript's ",(0,i.jsxs)(n.a,{href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement",children:[(0,i.jsx)(n.code,{children:"replace()"})," function and so can use any special string characters."]})]}),"\n",(0,i.jsx)(n.h3,{id:"rules",children:"Rules"}),"\n",(0,i.jsxs)(n.p,{children:["Each ",(0,i.jsx)(n.a,{href:"/configuration/transforms#stage-rules",children:"Rule"})," must be an array of ",(0,i.jsx)(n.a,{href:"#search-and-replace-expression",children:"search-and-replace expressions"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",metastring:'title="lastfm.json"',children:'[\n {\n "name": "myLastFm",\n "configureAs": "source",\n "data": {\n // ...\n },\n "options": {\n "playTransform": {\n "type": "user",\n "preCompare": [\n {\n "title": [\n // removes "badTerm" from title\n "badTerm",\n {\n // removes "fooTerm" from title and replaces it with "barTerm"\n "search": "fooTerm",\n "replace": "barTerm"\n }\n ]\n }\n ],\n }\n }\n }\n]\n'})}),"\n",(0,i.jsx)(n.admonition,{type:"note",children:(0,i.jsxs)(n.p,{children:["If the value of the field (title, an artist, album) is an empty string after transforming then the field is ",(0,i.jsx)(n.strong,{children:"removed."})]})}),"\n",(0,i.jsx)(n.h2,{id:"examples",children:"Examples"}),"\n",(0,i.jsxs)(n.h3,{id:"usage-with-when-condition",children:["Usage with ",(0,i.jsx)(n.code,{children:"when"})," condition"]}),"\n",(0,i.jsxs)(n.p,{children:["Using ",(0,i.jsx)(n.code,{children:"when"})," for ",(0,i.jsx)(n.a,{href:"/configuration/transforms#conditional-modification",children:"Conditional Modification"})]}),"\n",(0,i.jsxs)(s,{children:[(0,i.jsx)("summary",{children:"Example"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",children:'// On search-replace in title...\n// IF artist matches "Elephant Gym"\n// THEN Run regex search-replace ELSE skip this rule\n//\n// Run live|remastered regex remove on title\n{\n "title": [\n {\n "search": "/\\\\s\\\\-\\\\s\u6EFE\u77F340\\\\s\u6EFE\u77F3\u649E\u6A02\u968A\\\\s40\u5718\u62DA\u7D93\u5178(.+)$/i",\n "replace": "",\n "when": [\n {\n "artist": "/Elephant Gym/"\n }\n ]\n },\n "/(\\\\s\\\\-\\\\s|\\\\s)(feat\\\\.(.+)|live|remastered(.+))$/i"\n ],\n}\n'})})]}),"\n",(0,i.jsx)(n.h3,{id:"remove-phrase-from-title-in-all-new-plays",children:"Remove phrase from Title in all new Plays"}),"\n",(0,i.jsxs)(n.p,{children:["Removes the phrase ",(0,i.jsx)(n.code,{children:"(Album Version)"})," from the Title of a Play"]}),"\n",(0,i.jsxs)(s,{children:[(0,i.jsx)("summary",{children:"Example"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",children:'{\n "title": [\n "(Album Version)"\n ]\n}\n\n'})})]}),"\n",(0,i.jsx)(n.h3,{id:"remove-all-parenthesized-content-from-the-end-of-a-title",children:"Remove all parenthesized content from the end of a title"}),"\n",(0,i.jsxs)(s,{children:[(0,i.jsx)("summary",{children:"Example"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",children:'{\n"compare": {\n "candidate": {\n "title": [\n "/(\\(.+\\))\\s*$/"\n ]\n },\n "existing": {\n "title": [\n "/(\\(.+\\))\\s*$/"\n ]\n },\n },\n}\n'})})]}),"\n",(0,i.jsx)(n.h3,{id:"rename-misspelled-artist-in-all-new-plays",children:"Rename misspelled artist in all new Plays"}),"\n",(0,i.jsxs)(s,{children:[(0,i.jsx)("summary",{children:"Example"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",children:'{\n "artists": [\n {\n "search": "Boz Skaggs",\n "replace": "Boz Scaggs"\n }\n ]\n}\n'})})]}),"\n",(0,i.jsx)(n.h3,{id:"remove-various-artists-albums-in-all-new-plays",children:'Remove "Various Artists" albums in all new Plays'}),"\n",(0,i.jsxs)(s,{children:[(0,i.jsx)("summary",{children:"Example"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",children:'{\n "album": [\n {\n "search": "Various Artists",\n "replace": ""\n }\n ]\n}\n'})})]}),"\n",(0,i.jsx)(n.h3,{id:"extract-primary-artist-from-delimited-multi-artist-string",children:"Extract primary Artist from delimited, multi-Artist string"}),"\n",(0,i.jsxs)(s,{children:[(0,i.jsx)(n.p,{children:"When the Artist string is actually a multi-artist, delimited string, this search-and-replace will replace the string with just the first artist found."}),(0,i.jsx)(n.p,{children:"Ex"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"My Artist One / My Artist Two / Another Guy\nMy Artist One\n"})}),(0,i.jsxs)(n.p,{children:["Artists are delimited with a spaced forward slash (",(0,i.jsx)(n.code,{children:"/"}),") in the regex below. Replace the contents of the ",(0,i.jsx)(n.code,{children:"delim"})," capture group with the delimiter for your use case. Some more common scenarios:"]}),(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"(?\\\\/)"})," No spaces between slash IE ",(0,i.jsx)(n.code,{children:"My Artist One/My Artist Two/Another Guy"})]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"(?\\\\s*\\\\\\\\\\s*)"})," Backslash instead of forward slash IE ",(0,i.jsx)(n.code,{children:"My Artist One \\ My Artist Two \\ Another Guy"})]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"(?,)"})," Comma IE ",(0,i.jsx)(n.code,{children:"My Artist One, My Artist Two, Another Guy"})]}),"\n"]}),(0,i.jsxs)(s,{children:[(0,i.jsx)("summary",{children:"Example"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "artists": [\n {\n "search": "(.*?)(?\\\\s*\\\\/\\\\s*)(.*$)",\n "replace": "$1"\n }\n ]\n}\n'})})]})]})]})}function h(e={}){let{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}}}]); \ No newline at end of file diff --git a/assets/js/44b2647e.e46ccf5c.js b/assets/js/44b2647e.e46ccf5c.js deleted file mode 100644 index 1da7ebe3..00000000 --- a/assets/js/44b2647e.e46ccf5c.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["6942"],{5077(e,n,s){s.r(n),s.d(n,{metadata:()=>r,default:()=>h,frontMatter:()=>t,contentTitle:()=>l,toc:()=>c,assets:()=>o});var r=JSON.parse('{"id":"configuration/transforms/user","title":"User Stage","description":"The User Stage uses search-and-replace expressions, provided by you, to modify/replace parts of Play data.","source":"@site/docs/configuration/transforms/user.mdx","sourceDirName":"configuration/transforms","slug":"/configuration/transforms/user","permalink":"/multi-scrobbler/configuration/transforms/user","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/transforms/user.mdx","tags":[],"version":"current","frontMatter":{"title":"User Stage","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Native Stage","permalink":"/multi-scrobbler/configuration/transforms/native"},"next":{"title":"Development","permalink":"/multi-scrobbler/category/development"}}'),i=s(4848),a=s(8453);let t={title:"User Stage",toc_min_heading_level:2,toc_max_heading_level:5},l,o={},c=[{value:"Configuration",id:"configuration",level:2},{value:"Default Stage",id:"default-stage",level:4},{value:"Search-And-Replace Expression",id:"search-and-replace-expression",level:3},{value:"Rules",id:"rules",level:3},{value:"Examples",id:"examples",level:2},{value:"Usage with when condition",id:"usage-with-when-condition",level:3},{value:"Remove phrase from Title in all new Plays",id:"remove-phrase-from-title-in-all-new-plays",level:3},{value:"Remove all parenthesized content from the end of a title",id:"remove-all-parenthesized-content-from-the-end-of-a-title",level:3},{value:"Rename misspelled artist in all new Plays",id:"rename-misspelled-artist-in-all-new-plays",level:3},{value:"Remove "Various Artists" albums in all new Plays",id:"remove-various-artists-albums-in-all-new-plays",level:3},{value:"Extract primary Artist from delimited, multi-Artist string",id:"extract-primary-artist-from-delimited-multi-artist-string",level:3}];function d(e){let n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",h4:"h4",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.R)(),...e.components},{Details:s}=n;return s||function(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.strong,{children:"User"})," ",(0,i.jsx)(n.a,{href:"/configuration/transforms#stage",children:"Stage"})," uses ",(0,i.jsxs)(n.a,{href:"#search-and-replace-expression",children:[(0,i.jsx)(n.strong,{children:"search-and-replace"})," expressions"]}),", provided by you, to modify/replace parts of Play data."]}),"\n",(0,i.jsx)(n.p,{children:"This Stage is most useful for correcting individual instances of bad data, or patterns, in your known data. Example scenarios:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Removing ",(0,i.jsx)(n.code,{children:"(Album Version)"})," from all Titles"]}),"\n",(0,i.jsxs)(n.li,{children:["Removing ",(0,i.jsx)(n.code,{children:"Various Artists"})," from Artist data"]}),"\n",(0,i.jsx)(n.li,{children:"Correcting spelling mistakes for individual Artist names"}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["The user ",(0,i.jsxs)(n.a,{href:"/configuration/transforms#stage",children:["Stage ",(0,i.jsx)(n.code,{children:"type"})]})," is ",(0,i.jsx)(n.code,{children:"user"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["All ",(0,i.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," is done using ",(0,i.jsxs)(n.a,{href:"#search-and-replace-expression",children:[(0,i.jsx)(n.strong,{children:"search-and-replace"})," expressions"]})," inside individual ",(0,i.jsx)(n.a,{href:"#rules",children:"rules"}),". Default configuration for all Rules can still be done using Stage ",(0,i.jsx)(n.code,{children:"defaults"}),"."]}),"\n",(0,i.jsxs)(s,{children:[(0,i.jsx)("summary",{children:"Example"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "user",\n "name": "Normalizer",\n "defaults": {\n "title": [\n "(Album Version)"\n ],\n "artists": [\n "Various Artists"\n ]\n }\n }\n ]\n}\n'})})]}),"\n",(0,i.jsx)(n.h4,{id:"default-stage",children:"Default Stage"}),"\n",(0,i.jsxs)(n.p,{children:["For your convenience, if you do not define any User ",(0,i.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configurations"})," then multi-scrobbler automatically builds an empty one for you."]}),"\n",(0,i.jsxs)(n.p,{children:["This can be used by omitting the ",(0,i.jsx)(n.code,{children:"name"})," in your ",(0,i.jsx)(n.a,{href:"/configuration/transforms#stage",children:"modification Stage"}),"."]}),"\n",(0,i.jsxs)(s,{children:[(0,i.jsx)("summary",{children:"Example"}),(0,i.jsxs)(n.p,{children:["In a ",(0,i.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," ",(0,i.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "user",\n // do not include name, the default user stage will be used\n "title": [\n // removes badterm from track title\n "badterm"\n ]\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,i.jsx)(n.h3,{id:"search-and-replace-expression",children:"Search-And-Replace Expression"}),"\n",(0,i.jsx)(n.p,{children:"A Search-And-Replace Expression can be a plain string that matches a literal, then removes it:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'Expression: "badTerm"\n\n"this is badTerm cool string" => "this is a cool string"\n'})}),"\n",(0,i.jsx)(n.p,{children:"or a regular expression that matches and removes the match:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'Expression: "/bad\\w+/i"\n\n"this is badSomething cool string" => "this is a cool string"\n'})}),"\n",(0,i.jsx)(n.p,{children:"Or it may be an object that specifies what to match (using either plain string or regular expression) and what to replace it with:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",children:'{\n "search": "anotherBadTerm", // match all instances of \'anotherBadTerm\'\n "replace": "goodTerm" // replace with the string \'goodTerm\'\n}\n'})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'"this is anotherBadTerm cool string" => "this is goodTerm cool string"\n'})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",children:'{\n "search": "/^\\(\\w+.com)/i", // matches any string that starts with EX \'(YourMusic.com)\'\n "replace": "[MySite.com]" // replace with the string \'[MySite.com]\'\n}\n'})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'"(Foo.com) this is a cool string" => "[MySite.com] this is a cool string"\n'})}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"replace"})," property uses javascript's ",(0,i.jsxs)(n.a,{href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement",children:[(0,i.jsx)(n.code,{children:"replace()"})," function and so can use any special string characters."]})]}),"\n",(0,i.jsx)(n.h3,{id:"rules",children:"Rules"}),"\n",(0,i.jsxs)(n.p,{children:["Each ",(0,i.jsx)(n.a,{href:"/configuration/transforms#stage-rules",children:"Rule"})," must be an array of ",(0,i.jsx)(n.a,{href:"#search-and-replace-expression",children:"search-and-replace expressions"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",metastring:'title="lastfm.json"',children:'[\n {\n "name": "myLastFm",\n "configureAs": "source",\n "data": {\n // ...\n },\n "options": {\n "playTransform": {\n "type": "user",\n "preCompare": [\n {\n "title": [\n // removes "badTerm" from title\n "badTerm",\n {\n // removes "fooTerm" from title and replaces it with "barTerm"\n "search": "fooTerm",\n "replace": "barTerm"\n }\n ]\n }\n ],\n }\n }\n }\n]\n'})}),"\n",(0,i.jsx)(n.admonition,{type:"note",children:(0,i.jsxs)(n.p,{children:["If the value of the field (title, an artist, album) is an empty string after transforming then the field is ",(0,i.jsx)(n.strong,{children:"removed."})]})}),"\n",(0,i.jsx)(n.h2,{id:"examples",children:"Examples"}),"\n",(0,i.jsxs)(n.h3,{id:"usage-with-when-condition",children:["Usage with ",(0,i.jsx)(n.code,{children:"when"})," condition"]}),"\n",(0,i.jsxs)(n.p,{children:["Using ",(0,i.jsx)(n.code,{children:"when"})," for ",(0,i.jsx)(n.a,{href:"/configuration/transforms#conditional-modification",children:"Conditional Modification"})]}),"\n",(0,i.jsxs)(s,{children:[(0,i.jsx)("summary",{children:"Example"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",children:'// On search-replace in title...\n// IF artist matches "Elephant Gym"\n// THEN Run regex search-replace ELSE skip this rule\n//\n// Run live|remastered regex remove on title\n{\n "title": [\n {\n "search": "/\\\\s\\\\-\\\\s\u6EFE\u77F340\\\\s\u6EFE\u77F3\u649E\u6A02\u968A\\\\s40\u5718\u62DA\u7D93\u5178(.+)$/i",\n "replace": "",\n "when": [\n {\n "artist": "/Elephant Gym/"\n }\n ]\n },\n "/(\\\\s\\\\-\\\\s|\\\\s)(feat\\\\.(.+)|live|remastered(.+))$/i"\n ],\n}\n'})})]}),"\n",(0,i.jsx)(n.h3,{id:"remove-phrase-from-title-in-all-new-plays",children:"Remove phrase from Title in all new Plays"}),"\n",(0,i.jsxs)(n.p,{children:["Removes the phrase ",(0,i.jsx)(n.code,{children:"(Album Version)"})," from the Title of a Play"]}),"\n",(0,i.jsxs)(s,{children:[(0,i.jsx)("summary",{children:"Example"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",children:'{\n "title": [\n "(Album Version)"\n ]\n}\n\n'})})]}),"\n",(0,i.jsx)(n.h3,{id:"remove-all-parenthesized-content-from-the-end-of-a-title",children:"Remove all parenthesized content from the end of a title"}),"\n",(0,i.jsxs)(s,{children:[(0,i.jsx)("summary",{children:"Example"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",children:'{\n"compare": {\n "candidate": {\n "title": [\n "/(\\(.+\\))\\s*$/"\n ]\n },\n "existing": {\n "title": [\n "/(\\(.+\\))\\s*$/"\n ]\n },\n },\n}\n'})})]}),"\n",(0,i.jsx)(n.h3,{id:"rename-misspelled-artist-in-all-new-plays",children:"Rename misspelled artist in all new Plays"}),"\n",(0,i.jsxs)(s,{children:[(0,i.jsx)("summary",{children:"Example"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",children:'{\n "artists": [\n {\n "search": "Boz Skaggs",\n "replace": "Boz Scaggs"\n }\n ]\n}\n'})})]}),"\n",(0,i.jsx)(n.h3,{id:"remove-various-artists-albums-in-all-new-plays",children:'Remove "Various Artists" albums in all new Plays'}),"\n",(0,i.jsxs)(s,{children:[(0,i.jsx)("summary",{children:"Example"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json5",children:'{\n "album": [\n {\n "search": "Various Artists",\n "replace": ""\n }\n ]\n}\n'})})]}),"\n",(0,i.jsx)(n.h3,{id:"extract-primary-artist-from-delimited-multi-artist-string",children:"Extract primary Artist from delimited, multi-Artist string"}),"\n",(0,i.jsxs)(s,{children:[(0,i.jsx)(n.p,{children:"When the Artist string is actually a multi-artist, delimited string, this search-and-replace will replace the string with just the first artist found."}),(0,i.jsx)(n.p,{children:"Ex"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"My Artist One / My Artist Two / Another Guy\nMy Artist One\n"})}),(0,i.jsxs)(n.p,{children:["Artists are delimited with a spaced forward slash (",(0,i.jsx)(n.code,{children:"/"}),") in the regex below. Replace the contents of the ",(0,i.jsx)(n.code,{children:"delim"})," capture group with the delimiter for your use case. Some more common scenarios:"]}),(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"(?\\\\/)"})," No spaces between slash IE ",(0,i.jsx)(n.code,{children:"My Artist One/My Artist Two/Another Guy"})]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"(?\\\\s*\\\\\\\\\\s*)"})," Backslash instead of forward slash IE ",(0,i.jsx)(n.code,{children:"My Artist One \\ My Artist Two \\ Another Guy"})]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"(?,)"})," Comma IE ",(0,i.jsx)(n.code,{children:"My Artist One, My Artist Two, Another Guy"})]}),"\n"]}),(0,i.jsxs)(s,{children:[(0,i.jsx)("summary",{children:"Example"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "artists": [\n {\n "search": "(.*?)(?\\\\s*\\\\/\\\\s*)(.*$)",\n "replace": "$1"\n }\n ]\n}\n'})})]})]})]})}function h(e={}){let{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}}}]); \ No newline at end of file diff --git a/assets/js/5a6ebb5c.3a6e9beb.js b/assets/js/5a6ebb5c.3a6e9beb.js new file mode 100644 index 00000000..bedaabb7 --- /dev/null +++ b/assets/js/5a6ebb5c.3a6e9beb.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["7211"],{2558(e,i,s){s.r(i),s.d(i,{metadata:()=>n,default:()=>h,frontMatter:()=>o,contentTitle:()=>t,toc:()=>l,assets:()=>d});var n=JSON.parse('{"id":"configuration/clients/discord/discord","title":"Discord","description":"Discord Client Overview","source":"@site/docs/configuration/clients/discord/discord.mdx","sourceDirName":"configuration/clients/discord","slug":"/configuration/clients/discord/","permalink":"/multi-scrobbler/configuration/clients/discord/","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/clients/discord/discord.mdx","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"sidebar_position":2,"title":"Discord","description":"Discord Client Overview","toc_max_heading_level":4},"sidebar":"tutorialSidebar","previous":{"title":"Overview","permalink":"/multi-scrobbler/configuration/clients/"},"next":{"title":"Discord (App)","permalink":"/multi-scrobbler/configuration/clients/discord/discord-app"}}'),r=s(4848),c=s(8453);let o={sidebar_position:2,title:"Discord",description:"Discord Client Overview",toc_max_heading_level:4},t,d={},l=[{value:"App",id:"app",level:2},{value:"Headless",id:"headless",level:2}];function a(e){let i={a:"a",h2:"h2",li:"li",p:"p",strong:"strong",ul:"ul",...(0,c.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsxs)(i.p,{children:["This scrobbler uses ",(0,r.jsx)(i.strong,{children:(0,r.jsx)(i.a,{href:"/configuration/clients#now-playing",children:"Now Playing"})})," functionality to set your Discord ",(0,r.jsx)(i.a,{href:"https://docs.discord.com/developers/rich-presence/overview",children:"Rich Presence"})," to the music you are currently monitoring with multi-scrobbler."]}),"\n",(0,r.jsx)("img",{src:s(6170).A,height:"127"}),"\n",(0,r.jsx)(i.p,{children:"There are two types of Discord integrations to choose from:"}),"\n",(0,r.jsx)(i.h2,{id:"app",children:"App"}),"\n",(0,r.jsxs)(i.p,{children:["The ",(0,r.jsx)(i.strong,{children:"App"})," integration uses an ",(0,r.jsx)(i.strong,{children:"existing"})," Discord application on a desktop computer to set Rich Presence."]}),"\n",(0,r.jsxs)(i.ul,{children:["\n",(0,r.jsx)(i.li,{children:'Uses "permitted" access to Discord, no TOS issues'}),"\n",(0,r.jsx)(i.li,{children:"More restricted customization compared to Headless"}),"\n",(0,r.jsx)(i.li,{children:"Only active when your existing Discord application is open"}),"\n",(0,r.jsxs)(i.li,{children:["For most scenarios it requires additional, OS-specific steps to setup correctly","\n",(0,r.jsxs)(i.ul,{children:["\n",(0,r.jsx)(i.li,{children:"May require an additional program on your machine to relay the Discord connection"}),"\n",(0,r.jsx)(i.li,{children:"This is potentially insecure, or less secure, then Headless"}),"\n",(0,r.jsxs)(i.li,{children:[(0,r.jsxs)(i.strong,{children:["Can use ",(0,r.jsx)(i.a,{href:"/configuration/clients/discord/discord-app#discord-connection",children:"discord in a container"})," as an easy alternative"]}),"\n",(0,r.jsxs)(i.ul,{children:["\n",(0,r.jsx)(i.li,{children:"This is a Headless-like experience"}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(i.p,{children:(0,r.jsx)(i.a,{href:"/configuration/clients/discord/discord-app",children:(0,r.jsx)(i.strong,{children:"App Docs"})})}),"\n",(0,r.jsx)(i.h2,{id:"headless",children:"Headless"}),"\n",(0,r.jsxs)(i.p,{children:["The ",(0,r.jsx)(i.strong,{children:"Headless"})," integration uses Discord cloud Gateway/API, directly, to set Rich Presence."]}),"\n",(0,r.jsxs)(i.ul,{children:["\n",(0,r.jsx)(i.li,{children:"Does not require an existing Discord application (uses Discord cloud API)"}),"\n",(0,r.jsxs)(i.li,{children:["Using this ",(0,r.jsx)(i.strong,{children:"Violates Discord TOS"})," because it requires a user token"]}),"\n",(0,r.jsx)(i.li,{children:"More customization than App integration"}),"\n",(0,r.jsxs)(i.li,{children:["Active if ",(0,r.jsx)(i.strong,{children:"any"})," Discord application is online"]}),"\n"]}),"\n",(0,r.jsx)(i.p,{children:(0,r.jsx)(i.a,{href:"/configuration/clients/discord/discord-headless",children:(0,r.jsx)(i.strong,{children:"Headless Docs"})})})]})}function h(e={}){let{wrapper:i}={...(0,c.R)(),...e.components};return i?(0,r.jsx)(i,{...e,children:(0,r.jsx)(a,{...e})}):a(e)}},6170(e,i,s){s.d(i,{A:()=>n});let n=s.p+"assets/images/discord_presence_art-3260bd6302b2f0d1f13b45762cc908de.png"}}]); \ No newline at end of file diff --git a/assets/js/5a6ebb5c.5242bf8e.js b/assets/js/5a6ebb5c.5242bf8e.js deleted file mode 100644 index 249d53df..00000000 --- a/assets/js/5a6ebb5c.5242bf8e.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["7211"],{2558(e,i,s){s.r(i),s.d(i,{metadata:()=>r,default:()=>h,frontMatter:()=>c,contentTitle:()=>t,toc:()=>d,assets:()=>l});var r=JSON.parse('{"id":"configuration/clients/discord/discord","title":"Discord","description":"This scrobbler uses Now Playing functionality to set your Discord Rich Presence to the music you are currently monitoring with multi-scrobbler.","source":"@site/docs/configuration/clients/discord/discord.mdx","sourceDirName":"configuration/clients/discord","slug":"/configuration/clients/discord/","permalink":"/multi-scrobbler/configuration/clients/discord/","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/clients/discord/discord.mdx","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"sidebar_position":2,"title":"Discord","toc_max_heading_level":4},"sidebar":"tutorialSidebar","previous":{"title":"Overview","permalink":"/multi-scrobbler/configuration/clients/"},"next":{"title":"Discord (App)","permalink":"/multi-scrobbler/configuration/clients/discord/discord-app"}}'),n=s(4848),o=s(8453);let c={sidebar_position:2,title:"Discord",toc_max_heading_level:4},t,l={},d=[{value:"App",id:"app",level:2},{value:"Headless",id:"headless",level:2}];function a(e){let i={a:"a",h2:"h2",li:"li",p:"p",strong:"strong",ul:"ul",...(0,o.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)(i.p,{children:["This scrobbler uses ",(0,n.jsx)(i.strong,{children:(0,n.jsx)(i.a,{href:"/configuration/clients#now-playing",children:"Now Playing"})})," functionality to set your Discord ",(0,n.jsx)(i.a,{href:"https://docs.discord.com/developers/rich-presence/overview",children:"Rich Presence"})," to the music you are currently monitoring with multi-scrobbler."]}),"\n",(0,n.jsx)("img",{src:s(6170).A,height:"127"}),"\n",(0,n.jsx)(i.p,{children:"There are two types of Discord integrations to choose from:"}),"\n",(0,n.jsx)(i.h2,{id:"app",children:"App"}),"\n",(0,n.jsxs)(i.p,{children:["The ",(0,n.jsx)(i.strong,{children:"App"})," integration uses an ",(0,n.jsx)(i.strong,{children:"existing"})," Discord application on a desktop computer to set Rich Presence."]}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:'Uses "permitted" access to Discord, no TOS issues'}),"\n",(0,n.jsx)(i.li,{children:"More restricted customization compared to Headless"}),"\n",(0,n.jsx)(i.li,{children:"Only active when your existing Discord application is open"}),"\n",(0,n.jsxs)(i.li,{children:["For most scenarios it requires additional, OS-specific steps to setup correctly","\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:"May require an additional program on your machine to relay the Discord connection"}),"\n",(0,n.jsx)(i.li,{children:"This is potentially insecure, or less secure, then Headless"}),"\n",(0,n.jsxs)(i.li,{children:[(0,n.jsxs)(i.strong,{children:["Can use ",(0,n.jsx)(i.a,{href:"/configuration/clients/discord/discord-app#discord-connection",children:"discord in a container"})," as an easy alternative"]}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:"This is a Headless-like experience"}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,n.jsx)(i.p,{children:(0,n.jsx)(i.a,{href:"/configuration/clients/discord/discord-app",children:(0,n.jsx)(i.strong,{children:"App Docs"})})}),"\n",(0,n.jsx)(i.h2,{id:"headless",children:"Headless"}),"\n",(0,n.jsxs)(i.p,{children:["The ",(0,n.jsx)(i.strong,{children:"Headless"})," integration uses Discord cloud Gateway/API, directly, to set Rich Presence."]}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:"Does not require an existing Discord application (uses Discord cloud API)"}),"\n",(0,n.jsxs)(i.li,{children:["Using this ",(0,n.jsx)(i.strong,{children:"Violates Discord TOS"})," because it requires a user token"]}),"\n",(0,n.jsx)(i.li,{children:"More customization than App integration"}),"\n",(0,n.jsxs)(i.li,{children:["Active if ",(0,n.jsx)(i.strong,{children:"any"})," Discord application is online"]}),"\n"]}),"\n",(0,n.jsx)(i.p,{children:(0,n.jsx)(i.a,{href:"/configuration/clients/discord/discord-headless",children:(0,n.jsx)(i.strong,{children:"Headless Docs"})})})]})}function h(e={}){let{wrapper:i}={...(0,o.R)(),...e.components};return i?(0,n.jsx)(i,{...e,children:(0,n.jsx)(a,{...e})}):a(e)}},6170(e,i,s){s.d(i,{A:()=>r});let r=s.p+"assets/images/discord_presence_art-3260bd6302b2f0d1f13b45762cc908de.png"}}]); \ No newline at end of file diff --git a/assets/js/73287945.da597764.js b/assets/js/73287945.da597764.js deleted file mode 100644 index 32f73bb5..00000000 --- a/assets/js/73287945.da597764.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["8123"],{7075(e,i,n){n.r(i),n.d(i,{metadata:()=>r,default:()=>u,frontMatter:()=>c,contentTitle:()=>l,toc:()=>a,assets:()=>d});var r=JSON.parse('{"id":"configuration/sources/librefm-source","title":"Libre.fm","description":"This Source monitors your Libre.fm scrobble history and re-scrobbles new activity to configured Clients.","source":"@site/docs/configuration/sources/librefm-source.mdx","sourceDirName":"configuration/sources","slug":"/configuration/sources/librefm-source","permalink":"/multi-scrobbler/configuration/sources/librefm-source","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/sources/librefm-source.mdx","tags":[],"version":"current","frontMatter":{"title":"Libre.fm","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Last.fm","permalink":"/multi-scrobbler/configuration/sources/lastfm-source"},"next":{"title":"Listenbrainz (Endpoint)","permalink":"/multi-scrobbler/configuration/sources/listenbrainz-endpoint"}}'),t=n(4848),s=n(8453);n(8194),n(698),n(2170);var o=n(4828);let c={title:"Libre.fm",toc_min_heading_level:2,toc_max_heading_level:5},l,d={},a=[{value:"Configuration",id:"configuration",level:2}];function h(e){let i={a:"a",admonition:"admonition",code:"code",h2:"h2",li:"li",p:"p",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components},{Config:n}=i;return n||function(e,i){throw Error("Expected "+(i?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Config",!0),(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(i.p,{children:["This Source ",(0,t.jsx)(i.strong,{children:"monitors your Libre.fm scrobble history"})," and re-scrobbles new activity to configured ",(0,t.jsx)(i.a,{href:"/configuration/clients",children:"Clients."})]}),"\n",(0,t.jsx)(i.admonition,{title:"Other Uses",type:"tip",children:(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:["To ",(0,t.jsx)(i.strong,{children:"scrobble to"})," Libre.fm, create a ",(0,t.jsx)(i.a,{href:"/configuration/clients/librefm",children:"Libre.fm (Client)"})]}),"\n"]})}),"\n",(0,t.jsxs)(i.p,{children:["To configure this Source use the same registration instructions and configuration data shown in ",(0,t.jsx)(i.a,{href:"/configuration/clients/librefm",children:"Libre.fm (Client)"})," with the difference being ",(0,t.jsx)(i.code,{children:'"configureAs": "source"'})," defined in the File/AIO configuration below."]}),"\n",(0,t.jsx)(i.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsx)(n,{config:"LibrefmSourceConfig",fileContent:o.A,name:"librefm",children:(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Environmental Variable"}),(0,t.jsx)(i.th,{children:"Required?"}),(0,t.jsx)(i.th,{children:"Default"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"SOURCE_LIBREFM_ENABLE"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{}),(0,t.jsxs)(i.td,{children:["Set to ",(0,t.jsx)(i.code,{children:"true"})," to enable Libre.fm, if not using any of the below variables"]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"SOURCE_LIBREFM_API_KEY"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"anything"})}),(0,t.jsx)(i.td,{children:"Optional API Key"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"SOURCE_LIBREFM_SECRET"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"anything"})}),(0,t.jsx)(i.td,{children:"Optional shared secret"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"SOURCE_LIBREFM_REDIRECT_URI"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"http://localhost:9078/librefm/callback"})}),(0,t.jsxs)(i.td,{children:["Url to use for authentication. Must include ",(0,t.jsx)(i.code,{children:"librefm/callback"})," somewhere in it"]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"SOURCE_LIBREFM_SESSION"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{}),(0,t.jsx)(i.td,{children:"Session id. Will be generated by authentication flow if not provided."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"SOURCE_LIBREFM_URLBASE"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{}),(0,t.jsxs)(i.td,{children:["Use for custom Libre.fm instance only. The host + path prefix EX ",(0,t.jsx)(i.code,{children:"https://my.libre.example/2.0/"})]})]})]})]})})]})}function u(e={}){let{wrapper:i}={...(0,s.R)(),...e.components};return i?(0,t.jsx)(i,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},4828(e,i,n){n.d(i,{A:()=>r});let r='[\n {\n "name": "myLibreFmClient",\n "enable": true,\n "configureAs": "client",\n "data": {\n "apiKey": "anything",\n "secret": "anything",\n "redirectUri": "http://localhost:9078/librefm/callback"\n }\n },\n {\n "name": "myLibreFmSource",\n "enable": true,\n "configureAs": "source",\n "data": {\n "apiKey": "anything",\n "secret": "anything",\n "redirectUri": "http://localhost:9078/librefm/callback"\n }\n }\n]\n'}}]); \ No newline at end of file diff --git a/assets/js/73287945.e1a50a07.js b/assets/js/73287945.e1a50a07.js new file mode 100644 index 00000000..62e2ec0c --- /dev/null +++ b/assets/js/73287945.e1a50a07.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["8123"],{7075(e,i,n){n.r(i),n.d(i,{metadata:()=>r,default:()=>u,frontMatter:()=>c,contentTitle:()=>l,toc:()=>a,assets:()=>d});var r=JSON.parse('{"id":"configuration/sources/librefm-source","title":"Libre.fm","description":"Libre.fm (Source)","source":"@site/docs/configuration/sources/librefm-source.mdx","sourceDirName":"configuration/sources","slug":"/configuration/sources/librefm-source","permalink":"/multi-scrobbler/configuration/sources/librefm-source","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/sources/librefm-source.mdx","tags":[],"version":"current","frontMatter":{"title":"Libre.fm","description":"Libre.fm (Source)","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Last.fm","permalink":"/multi-scrobbler/configuration/sources/lastfm-source"},"next":{"title":"Listenbrainz (Endpoint)","permalink":"/multi-scrobbler/configuration/sources/listenbrainz-endpoint"}}'),t=n(4848),s=n(8453);n(8194),n(698),n(2170);var o=n(4828);let c={title:"Libre.fm",description:"Libre.fm (Source)",toc_min_heading_level:2,toc_max_heading_level:5},l,d={},a=[{value:"Configuration",id:"configuration",level:2}];function h(e){let i={a:"a",admonition:"admonition",code:"code",h2:"h2",li:"li",p:"p",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components},{Config:n}=i;return n||function(e,i){throw Error("Expected "+(i?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Config",!0),(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(i.p,{children:["This Source ",(0,t.jsx)(i.strong,{children:"monitors your Libre.fm scrobble history"})," and re-scrobbles new activity to configured ",(0,t.jsx)(i.a,{href:"/configuration/clients",children:"Clients."})]}),"\n",(0,t.jsx)(i.admonition,{title:"Other Uses",type:"tip",children:(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:["To ",(0,t.jsx)(i.strong,{children:"scrobble to"})," Libre.fm, create a ",(0,t.jsx)(i.a,{href:"/configuration/clients/librefm",children:"Libre.fm (Client)"})]}),"\n"]})}),"\n",(0,t.jsxs)(i.p,{children:["To configure this Source use the same registration instructions and configuration data shown in ",(0,t.jsx)(i.a,{href:"/configuration/clients/librefm",children:"Libre.fm (Client)"})," with the difference being ",(0,t.jsx)(i.code,{children:'"configureAs": "source"'})," defined in the File/AIO configuration below."]}),"\n",(0,t.jsx)(i.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsx)(n,{config:"LibrefmSourceConfig",fileContent:o.A,name:"librefm",children:(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Environmental Variable"}),(0,t.jsx)(i.th,{children:"Required?"}),(0,t.jsx)(i.th,{children:"Default"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"SOURCE_LIBREFM_ENABLE"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{}),(0,t.jsxs)(i.td,{children:["Set to ",(0,t.jsx)(i.code,{children:"true"})," to enable Libre.fm, if not using any of the below variables"]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"SOURCE_LIBREFM_API_KEY"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"anything"})}),(0,t.jsx)(i.td,{children:"Optional API Key"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"SOURCE_LIBREFM_SECRET"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"anything"})}),(0,t.jsx)(i.td,{children:"Optional shared secret"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"SOURCE_LIBREFM_REDIRECT_URI"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"http://localhost:9078/librefm/callback"})}),(0,t.jsxs)(i.td,{children:["Url to use for authentication. Must include ",(0,t.jsx)(i.code,{children:"librefm/callback"})," somewhere in it"]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"SOURCE_LIBREFM_SESSION"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{}),(0,t.jsx)(i.td,{children:"Session id. Will be generated by authentication flow if not provided."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"SOURCE_LIBREFM_URLBASE"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{}),(0,t.jsxs)(i.td,{children:["Use for custom Libre.fm instance only. The host + path prefix EX ",(0,t.jsx)(i.code,{children:"https://my.libre.example/2.0/"})]})]})]})]})})]})}function u(e={}){let{wrapper:i}={...(0,s.R)(),...e.components};return i?(0,t.jsx)(i,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},4828(e,i,n){n.d(i,{A:()=>r});let r='[\n {\n "name": "myLibreFmClient",\n "enable": true,\n "configureAs": "client",\n "data": {\n "apiKey": "anything",\n "secret": "anything",\n "redirectUri": "http://localhost:9078/librefm/callback"\n }\n },\n {\n "name": "myLibreFmSource",\n "enable": true,\n "configureAs": "source",\n "data": {\n "apiKey": "anything",\n "secret": "anything",\n "redirectUri": "http://localhost:9078/librefm/callback"\n }\n }\n]\n'}}]); \ No newline at end of file diff --git a/assets/js/7d837a1d.9c33ad61.js b/assets/js/7d837a1d.9c33ad61.js deleted file mode 100644 index 2aa6dec2..00000000 --- a/assets/js/7d837a1d.9c33ad61.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["4787"],{476(e){e.exports=JSON.parse('{"version":{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"tutorialSidebar":[{"type":"link","href":"/multi-scrobbler/","label":"Overview","docId":"index","unlisted":false},{"type":"link","href":"/multi-scrobbler/quickstart","label":"Quickstart","docId":"quickstart","unlisted":false},{"type":"category","label":"Installation","collapsible":true,"collapsed":true,"items":[{"type":"link","href":"/multi-scrobbler/installation/service","label":"As a Service","docId":"installation/service","unlisted":false}],"href":"/multi-scrobbler/installation/"},{"type":"category","label":"Updating","collapsible":true,"collapsed":true,"items":[{"type":"link","href":"/multi-scrobbler/updating/","label":"Overview","docId":"updating/updating","unlisted":false}],"href":"/multi-scrobbler/category/updating"},{"type":"category","label":"Configuration","collapsible":true,"collapsed":true,"items":[{"type":"category","label":"Scrobble Clients","collapsible":true,"collapsed":true,"items":[{"type":"category","label":"Discord","collapsible":true,"collapsed":true,"items":[{"type":"link","href":"/multi-scrobbler/configuration/clients/discord/discord-app","label":"Discord (App)","docId":"configuration/clients/discord/discord-app","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/clients/discord/discord-headless","label":"Discord (Headless)","docId":"configuration/clients/discord/discord-headless","unlisted":false}],"href":"/multi-scrobbler/configuration/clients/discord/"},{"type":"link","href":"/multi-scrobbler/configuration/clients/koito","label":"Koito","docId":"configuration/clients/koito","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/clients/lastfm","label":"Last.fm","docId":"configuration/clients/lastfm","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/clients/librefm","label":"Libre.fm","docId":"configuration/clients/librefm","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/clients/listenbrainz","label":"Listenbrainz","docId":"configuration/clients/listenbrainz","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/clients/maloja","label":"Maloja","docId":"configuration/clients/maloja","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/clients/rocksky","label":"Rocksky","docId":"configuration/clients/rocksky","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/clients/tealfm","label":"Teal.fm","docId":"configuration/clients/tealfm","unlisted":false}],"href":"/multi-scrobbler/configuration/clients/"},{"type":"category","label":"Scrobble Sources","collapsible":true,"collapsed":true,"items":[{"type":"link","href":"/multi-scrobbler/configuration/sources/azuracast","label":"Azuracast","docId":"configuration/sources/azuracast","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/deezer","label":"Deezer","docId":"configuration/sources/deezer","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/google-cast","label":"Google Cast (Chromecast)","docId":"configuration/sources/google-cast","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/icecast","label":"Icecast","docId":"configuration/sources/icecast","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/jellyfin","label":"Jellyfin","docId":"configuration/sources/jellyfin","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/jriver","label":"JRiver","docId":"configuration/sources/jriver","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/kodi","label":"Kodi","docId":"configuration/sources/kodi","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/koito-source","label":"Koito","docId":"configuration/sources/koito-source","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/lastfm-endpoint","label":"Lastfm (Endpoint)","docId":"configuration/sources/lastfm-endpoint","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/lastfm-source","label":"Last.fm","docId":"configuration/sources/lastfm-source","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/librefm-source","label":"Libre.fm","docId":"configuration/sources/librefm-source","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/listenbrainz-endpoint","label":"Listenbrainz (Endpoint)","docId":"configuration/sources/listenbrainz-endpoint","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/listenbrainz-source","label":"Listenbrainz","docId":"configuration/sources/listenbrainz-source","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/maloja","label":"Maloja","docId":"configuration/sources/maloja","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/mopidy","label":"Mopidy","docId":"configuration/sources/mopidy","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/mpd","label":"MPD (Music Player Daemon)","docId":"configuration/sources/mpd","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/mpris","label":"MPRIS","docId":"configuration/sources/mpris","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/musikcube","label":"Musikcube","docId":"configuration/sources/musikcube","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/plex","label":"Plex","docId":"configuration/sources/plex","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/rocksky-source","label":"Rocksky","docId":"configuration/sources/rocksky-source","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/sonos","label":"Sonos","docId":"configuration/sources/sonos","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/spotify","label":"Spotify","docId":"configuration/sources/spotify","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/subsonic","label":"Subsonic","docId":"configuration/sources/subsonic","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/tealfm-source","label":"Teal.fm","docId":"configuration/sources/tealfm-source","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/vlc","label":"VLC","docId":"configuration/sources/vlc","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/webscrobbler","label":"Webscrobbler","docId":"configuration/sources/webscrobbler","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/yamaha-musiccast","label":"Yamaha MusicCast","docId":"configuration/sources/yamaha-musiccast","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/yandex-music","label":"Yandex Music","docId":"configuration/sources/yandex-music","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/youtube-music","label":"Youtube Music","docId":"configuration/sources/youtube-music","unlisted":false}],"href":"/multi-scrobbler/configuration/sources/"},{"type":"link","href":"/multi-scrobbler/configuration/kitchensink","label":"Kitchen Sink","docId":"configuration/kitchensink","unlisted":false},{"type":"category","label":"Enhance Scrobbles","collapsible":true,"collapsed":true,"items":[{"type":"link","href":"/multi-scrobbler/configuration/transforms/musicbrainz","label":"Musicbrainz Stage","docId":"configuration/transforms/musicbrainz","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/transforms/native","label":"Native Stage","docId":"configuration/transforms/native","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/transforms/user","label":"User Stage","docId":"configuration/transforms/user","unlisted":false}],"href":"/multi-scrobbler/configuration/transforms/"}],"href":"/multi-scrobbler/configuration/"},{"type":"category","label":"Development","collapsible":true,"collapsed":true,"items":[{"type":"link","href":"/multi-scrobbler/development/dev-common","label":"Common Development","docId":"development/dev-common","unlisted":false},{"type":"link","href":"/multi-scrobbler/development/dev-source","label":"Source Development/Tutorial","docId":"development/dev-source","unlisted":false},{"type":"link","href":"/multi-scrobbler/development/dev-client","label":"Client Development/Tutorial","docId":"development/dev-client","unlisted":false},{"type":"link","href":"/multi-scrobbler/development/flatpak","label":"Flatpak","docId":"development/flatpak","unlisted":false}],"href":"/multi-scrobbler/category/development"},{"type":"link","href":"/multi-scrobbler/FAQ","label":"FAQ","docId":"FAQ","unlisted":false},{"type":"link","href":"/multi-scrobbler/help","label":"Help","docId":"help","unlisted":false},{"type":"link","label":"Config Playground","href":"/playground"}]},"docs":{"configuration/clients/clients":{"id":"configuration/clients/clients","title":"Overview","description":"A Client is an application that stores the historical information about what songs you have played (scrobbles). Examples are Maloja, Last.fm, Listenbrainz...","sidebar":"tutorialSidebar"},"configuration/clients/discord/discord":{"id":"configuration/clients/discord/discord","title":"Discord","description":"This scrobbler uses Now Playing functionality to set your Discord Rich Presence to the music you are currently monitoring with multi-scrobbler.","sidebar":"tutorialSidebar"},"configuration/clients/discord/discord-app":{"id":"configuration/clients/discord/discord-app","title":"Discord (App)","description":"This scrobbler uses Now Playing functionality to set your Discord Rich Presence to the music you are currently monitoring with multi-scrobbler.","sidebar":"tutorialSidebar"},"configuration/clients/discord/discord-headless":{"id":"configuration/clients/discord/discord-headless","title":"Discord (Headless)","description":"This scrobbler uses Now Playing functionality to set your Discord Rich Presence to the music you are currently monitoring with multi-scrobbler.","sidebar":"tutorialSidebar"},"configuration/clients/koito":{"id":"configuration/clients/koito","title":"Koito","description":"To monitor and re-scrobble activity from a Koito account create a Koito (Source)","sidebar":"tutorialSidebar"},"configuration/clients/lastfm":{"id":"configuration/clients/lastfm","title":"Last.fm","description":"* To monitor and re-scrobble activity from your Last.fm/Libre.fm account create a Last.fm (Source)","sidebar":"tutorialSidebar"},"configuration/clients/librefm":{"id":"configuration/clients/librefm","title":"Libre.fm","description":"* To monitor and re-scrobble activity from your Libre.fm account create a Libre.fm (Source)","sidebar":"tutorialSidebar"},"configuration/clients/listenbrainz":{"id":"configuration/clients/listenbrainz","title":"Listenbrainz","description":"* To monitor and re-scrobble activity from a Listenbrainz account create a Listenbrainz (Source)","sidebar":"tutorialSidebar"},"configuration/clients/maloja":{"id":"configuration/clients/maloja","title":"Maloja","description":"Setup a Maloja server if you have not already done this.","sidebar":"tutorialSidebar"},"configuration/clients/rocksky":{"id":"configuration/clients/rocksky","title":"Rocksky","description":"To monitor and re-scrobble activity from Rocksky create a Rocksky (Source)","sidebar":"tutorialSidebar"},"configuration/clients/tealfm":{"id":"configuration/clients/tealfm","title":"Teal.fm","description":"To monitor and re-scrobble activity from a ATProto Profile create a teal.fm (Source)","sidebar":"tutorialSidebar"},"configuration/configuration":{"id":"configuration/configuration","title":"Overview","description":"Check the FAQ if you have any issues after configuration!","sidebar":"tutorialSidebar"},"configuration/kitchensink":{"id":"configuration/kitchensink","title":"Kitchen Sink","description":"Scenario:","sidebar":"tutorialSidebar"},"configuration/sources/azuracast":{"id":"configuration/sources/azuracast","title":"Azuracast","description":"Azuracast may not correctly report track length or position. If this is the case you should reduce the \\"listened to\\" duration using Scrobble Thresholds so that multi-scrobbler scrobbles correctly.","sidebar":"tutorialSidebar"},"configuration/sources/deezer":{"id":"configuration/sources/deezer","title":"Deezer","description":"This Source uses unofficial methods to access Deezer data that are likely in violation of Deezer\'s TOS. Deezer may change or remove these methods at any time, breaking functionality. Use this Source at your own risk.","sidebar":"tutorialSidebar"},"configuration/sources/google-cast":{"id":"configuration/sources/google-cast","title":"Google Cast (Chromecast)","description":"If your media device can be Cast to using this button Chromecast Icon on your phone/computer then multi-scrobbler can monitor it in order to scrobble music you play.","sidebar":"tutorialSidebar"},"configuration/sources/icecast":{"id":"configuration/sources/icecast","title":"Icecast","description":"Parse metadata from any Icecast Station (streams) to use for scrobbling.","sidebar":"tutorialSidebar"},"configuration/sources/jellyfin":{"id":"configuration/sources/jellyfin","title":"Jellyfin","description":"Must be using Jellyfin 10.7 or greater","sidebar":"tutorialSidebar"},"configuration/sources/jriver":{"id":"configuration/sources/jriver","title":"JRiver","description":"In order for multi-scrobbler to communicate with JRiver you must have Web Server Interface enabled. This can can be in the JRiver GUI:","sidebar":"tutorialSidebar"},"configuration/sources/kodi":{"id":"configuration/sources/kodi","title":"Kodi","description":"In order for multi-scrobbler to communicate with Kodi you must have the Web Interface enabled. This can be done in the Kodi GUI:","sidebar":"tutorialSidebar"},"configuration/sources/koito-source":{"id":"configuration/sources/koito-source","title":"Koito","description":"This Source monitors a Koito account\'s scrobble history and then re-scrobbles discovered tracks to configured Clients.","sidebar":"tutorialSidebar"},"configuration/sources/lastfm-endpoint":{"id":"configuration/sources/lastfm-endpoint","title":"Lastfm (Endpoint)","description":"This Source enables multi-scrobbler to accept scrobbles from outside applications as if it was a Last.fm server.","sidebar":"tutorialSidebar"},"configuration/sources/lastfm-source":{"id":"configuration/sources/lastfm-source","title":"Last.fm","description":"This Source monitors your Last.fm scrobble history and re-scrobbles new activity to configured Clients.","sidebar":"tutorialSidebar"},"configuration/sources/librefm-source":{"id":"configuration/sources/librefm-source","title":"Libre.fm","description":"This Source monitors your Libre.fm scrobble history and re-scrobbles new activity to configured Clients.","sidebar":"tutorialSidebar"},"configuration/sources/listenbrainz-endpoint":{"id":"configuration/sources/listenbrainz-endpoint","title":"Listenbrainz (Endpoint)","description":"This Source enables multi-scrobbler to accept scrobbles from outside applications as if it was a Listenbrainz server.","sidebar":"tutorialSidebar"},"configuration/sources/listenbrainz-source":{"id":"configuration/sources/listenbrainz-source","title":"Listenbrainz","description":"This Source monitors a Listenbrainz account\'s scrobble history and then re-scrobbles discovered tracks to configured clients.","sidebar":"tutorialSidebar"},"configuration/sources/maloja":{"id":"configuration/sources/maloja","title":"Maloja","description":"To scrobble to a Maloja server, create a Maloja (Client)","sidebar":"tutorialSidebar"},"configuration/sources/mopidy":{"id":"configuration/sources/mopidy","title":"Mopidy","description":"Mopidy is a headless music server that supports playing music from many standard and non-standard sources such as Pandora, Bandcamp, and Tunein.","sidebar":"tutorialSidebar"},"configuration/sources/mpd":{"id":"configuration/sources/mpd","title":"MPD (Music Player Daemon)","description":"MS communicates with MPD using the TCP client connection.","sidebar":"tutorialSidebar"},"configuration/sources/mpris":{"id":"configuration/sources/mpris","title":"MPRIS","description":"MPRIS is a standard interface for communicating with Music Players on linux operating systems.","sidebar":"tutorialSidebar"},"configuration/sources/musikcube":{"id":"configuration/sources/musikcube","title":"Musikcube","description":"In order to use Musikcube configure it to accept websocket connections in server setup:","sidebar":"tutorialSidebar"},"configuration/sources/plex":{"id":"configuration/sources/plex","title":"Plex","description":"Find your Plex Token and make note of the URL and Port used to connect to your Plex instance.","sidebar":"tutorialSidebar"},"configuration/sources/rocksky-source":{"id":"configuration/sources/rocksky-source","title":"Rocksky","description":"To scrobble to* Rocksky, create a Rocksky (Client)","sidebar":"tutorialSidebar"},"configuration/sources/sonos":{"id":"configuration/sources/sonos","title":"Sonos","description":"Multi-scrobbler can use the Sonos local API to monitor what your Sonos system is playing.","sidebar":"tutorialSidebar"},"configuration/sources/sources":{"id":"configuration/sources/sources","title":"Overview","description":"A Source is a data source that contains information about tracks you are playing like a music player or platform. Examples are Spotify, Jellyfin, Plex, Youtube Music, Airsonic, etc...","sidebar":"tutorialSidebar"},"configuration/sources/spotify":{"id":"configuration/sources/spotify","title":"Spotify","description":"If your Spotify player has Automix enabled and Spotify uses it for your playlist/queue then MS cannot accurately determine when a track will end. This is because the track is \\"mixed\\" in your queue with a shorter play time than its actual length and Spotify does not report this modified play time in its API. This does not affect MS\'s ability to scrobble from Spotify but it will affect the accuracy of the duration MS reports was played.","sidebar":"tutorialSidebar"},"configuration/sources/subsonic":{"id":"configuration/sources/subsonic","title":"Subsonic","description":"This Source can be used for any application that implements the Subsonic API and supports the getNowPlaying endpoint (such as Airsonic and Navidrome)","sidebar":"tutorialSidebar"},"configuration/sources/tealfm-source":{"id":"configuration/sources/tealfm-source","title":"Teal.fm","description":"To scrobble to* an ATProto PDS, create a teal.fm (Client)","sidebar":"tutorialSidebar"},"configuration/sources/vlc":{"id":"configuration/sources/vlc","title":"VLC","description":"MS communicates with VLC using the Web (http) interface module","sidebar":"tutorialSidebar"},"configuration/sources/webscrobbler":{"id":"configuration/sources/webscrobbler","title":"Webscrobbler","description":"After installing the Webscrobbler extension open the preferences/settings for it:","sidebar":"tutorialSidebar"},"configuration/sources/yamaha-musiccast":{"id":"configuration/sources/yamaha-musiccast","title":"Yamaha MusicCast","description":"Monitor MusicCast device/receivers for music played on Network/USB/CD inputs.","sidebar":"tutorialSidebar"},"configuration/sources/yandex-music":{"id":"configuration/sources/yandex-music","title":"Yandex Music","description":"Monitor your Yandex Music listening activity in real-time.","sidebar":"tutorialSidebar"},"configuration/sources/youtube-music":{"id":"configuration/sources/youtube-music","title":"Youtube Music","description":"Communication with YT Music is unofficial and not supported or endorsed by Google. This means that this integration may stop working at any time if Google decides to change how YT Music works in the browser.","sidebar":"tutorialSidebar"},"configuration/transforms/musicbrainz":{"id":"configuration/transforms/musicbrainz","title":"Musicbrainz Stage","description":"The Musicbrainz Stage matches your Play data with the MusicBrainz database. If the match score is high enough then Multi-Scrobbler uses the match to correct and fill-in missing information in your Play data.","sidebar":"tutorialSidebar"},"configuration/transforms/native":{"id":"configuration/transforms/native","title":"Native Stage","description":"The Native Stage uses built-in heuristics to try to extract Artists from Play artist/track data.","sidebar":"tutorialSidebar"},"configuration/transforms/transforms":{"id":"configuration/transforms/transforms","title":"Enhancing Scrobbles","description":"Multi-scrobbler configs support the ability to enhance scrobble data in an automated fashion by matching and replacing strings in title, artists, and album at many different times in multi-scrobbler\'s lifecycle.","sidebar":"tutorialSidebar"},"configuration/transforms/user":{"id":"configuration/transforms/user","title":"User Stage","description":"The User Stage uses search-and-replace expressions, provided by you, to modify/replace parts of Play data.","sidebar":"tutorialSidebar"},"development/dev-client":{"id":"development/dev-client","title":"Client Development/Tutorial","description":"To do...","sidebar":"tutorialSidebar"},"development/dev-common":{"id":"development/dev-common","title":"Common Development","description":"Start here for MS development","sidebar":"tutorialSidebar"},"development/dev-source":{"id":"development/dev-source","title":"Source Development/Tutorial","description":"This document will provide a step-by-step guide for creating a (trivial) new Source in MS alongside describing what aspects of the Source need to be implemented based on the service you use. Before using this document you should review Common Development.","sidebar":"tutorialSidebar"},"development/flatpak":{"id":"development/flatpak","title":"Flatpak","description":"Building Flatpak App locally","sidebar":"tutorialSidebar"},"FAQ":{"id":"FAQ","title":"FAQ","description":"Connection Issues","sidebar":"tutorialSidebar"},"help":{"id":"help","title":"Help","description":"Getting Useful Data","sidebar":"tutorialSidebar"},"index":{"id":"index","title":"Overview","description":"Latest Release","sidebar":"tutorialSidebar"},"installation/installation":{"id":"installation/installation","title":"Overview","description":"For the difference between ENV and File examples in this document see Configuration Types.","sidebar":"tutorialSidebar"},"installation/service":{"id":"installation/service","title":"As a Service","description":"If you have multi-scrobbler installed locally you can enable it to run as a background service when you login.","sidebar":"tutorialSidebar"},"quickstart":{"id":"quickstart","title":"Quickstart","description":"This guide will get you up and running with multi-scrobbler using Docker and docker compose. At the end of the guide you will have:","sidebar":"tutorialSidebar"},"updating/updating":{"id":"updating/updating","title":"Overview","description":"Updating","sidebar":"tutorialSidebar"}}}}')}}]); \ No newline at end of file diff --git a/assets/js/7d837a1d.c3c741b7.js b/assets/js/7d837a1d.c3c741b7.js new file mode 100644 index 00000000..4e14022d --- /dev/null +++ b/assets/js/7d837a1d.c3c741b7.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["4787"],{476(i){i.exports=JSON.parse('{"version":{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"tutorialSidebar":[{"type":"link","href":"/multi-scrobbler/","label":"Overview","docId":"index","unlisted":false},{"type":"link","href":"/multi-scrobbler/quickstart","label":"Quickstart","docId":"quickstart","unlisted":false},{"type":"category","label":"Installation","collapsible":true,"collapsed":true,"items":[{"type":"link","href":"/multi-scrobbler/installation/service","label":"As a Service","docId":"installation/service","unlisted":false}],"href":"/multi-scrobbler/installation/"},{"type":"category","label":"Updating","collapsible":true,"collapsed":true,"items":[{"type":"link","href":"/multi-scrobbler/updating/","label":"Overview","docId":"updating/updating","unlisted":false}],"href":"/multi-scrobbler/category/updating"},{"type":"category","label":"Configuration","collapsible":true,"collapsed":true,"items":[{"type":"category","label":"Scrobble Clients","collapsible":true,"collapsed":true,"items":[{"type":"category","label":"Discord","collapsible":true,"collapsed":true,"items":[{"type":"link","href":"/multi-scrobbler/configuration/clients/discord/discord-app","label":"Discord (App)","docId":"configuration/clients/discord/discord-app","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/clients/discord/discord-headless","label":"Discord (Headless)","docId":"configuration/clients/discord/discord-headless","unlisted":false}],"href":"/multi-scrobbler/configuration/clients/discord/"},{"type":"link","href":"/multi-scrobbler/configuration/clients/koito","label":"Koito","docId":"configuration/clients/koito","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/clients/lastfm","label":"Last.fm","docId":"configuration/clients/lastfm","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/clients/librefm","label":"Libre.fm","docId":"configuration/clients/librefm","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/clients/listenbrainz","label":"Listenbrainz","docId":"configuration/clients/listenbrainz","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/clients/maloja","label":"Maloja","docId":"configuration/clients/maloja","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/clients/rocksky","label":"Rocksky","docId":"configuration/clients/rocksky","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/clients/tealfm","label":"Teal.fm","docId":"configuration/clients/tealfm","unlisted":false}],"href":"/multi-scrobbler/configuration/clients/"},{"type":"category","label":"Scrobble Sources","collapsible":true,"collapsed":true,"items":[{"type":"link","href":"/multi-scrobbler/configuration/sources/azuracast","label":"Azuracast","docId":"configuration/sources/azuracast","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/deezer","label":"Deezer","docId":"configuration/sources/deezer","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/google-cast","label":"Google Cast (Chromecast)","docId":"configuration/sources/google-cast","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/icecast","label":"Icecast","docId":"configuration/sources/icecast","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/jellyfin","label":"Jellyfin","docId":"configuration/sources/jellyfin","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/jriver","label":"JRiver","docId":"configuration/sources/jriver","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/kodi","label":"Kodi","docId":"configuration/sources/kodi","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/koito-source","label":"Koito","docId":"configuration/sources/koito-source","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/lastfm-endpoint","label":"Lastfm (Endpoint)","docId":"configuration/sources/lastfm-endpoint","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/lastfm-source","label":"Last.fm","docId":"configuration/sources/lastfm-source","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/librefm-source","label":"Libre.fm","docId":"configuration/sources/librefm-source","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/listenbrainz-endpoint","label":"Listenbrainz (Endpoint)","docId":"configuration/sources/listenbrainz-endpoint","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/listenbrainz-source","label":"Listenbrainz","docId":"configuration/sources/listenbrainz-source","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/maloja","label":"Maloja","docId":"configuration/sources/maloja","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/mopidy","label":"Mopidy","docId":"configuration/sources/mopidy","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/mpd","label":"MPD (Music Player Daemon)","docId":"configuration/sources/mpd","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/mpris","label":"MPRIS","docId":"configuration/sources/mpris","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/musikcube","label":"Musikcube","docId":"configuration/sources/musikcube","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/plex","label":"Plex","docId":"configuration/sources/plex","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/rocksky-source","label":"Rocksky","docId":"configuration/sources/rocksky-source","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/sonos","label":"Sonos","docId":"configuration/sources/sonos","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/spotify","label":"Spotify","docId":"configuration/sources/spotify","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/subsonic","label":"Subsonic","docId":"configuration/sources/subsonic","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/tealfm-source","label":"Teal.fm","docId":"configuration/sources/tealfm-source","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/vlc","label":"VLC","docId":"configuration/sources/vlc","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/webscrobbler","label":"Webscrobbler","docId":"configuration/sources/webscrobbler","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/yamaha-musiccast","label":"Yamaha MusicCast","docId":"configuration/sources/yamaha-musiccast","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/yandex-music","label":"Yandex Music","docId":"configuration/sources/yandex-music","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/sources/youtube-music","label":"Youtube Music","docId":"configuration/sources/youtube-music","unlisted":false}],"href":"/multi-scrobbler/configuration/sources/"},{"type":"link","href":"/multi-scrobbler/configuration/kitchensink","label":"Kitchen Sink","docId":"configuration/kitchensink","unlisted":false},{"type":"category","label":"Enhance Scrobbles","collapsible":true,"collapsed":true,"items":[{"type":"link","href":"/multi-scrobbler/configuration/transforms/musicbrainz","label":"Musicbrainz Stage","docId":"configuration/transforms/musicbrainz","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/transforms/native","label":"Native Stage","docId":"configuration/transforms/native","unlisted":false},{"type":"link","href":"/multi-scrobbler/configuration/transforms/user","label":"User Stage","docId":"configuration/transforms/user","unlisted":false}],"href":"/multi-scrobbler/configuration/transforms/"}],"href":"/multi-scrobbler/configuration/"},{"type":"category","label":"Development","collapsible":true,"collapsed":true,"items":[{"type":"link","href":"/multi-scrobbler/development/dev-common","label":"Common Development","docId":"development/dev-common","unlisted":false},{"type":"link","href":"/multi-scrobbler/development/dev-source","label":"Source Development/Tutorial","docId":"development/dev-source","unlisted":false},{"type":"link","href":"/multi-scrobbler/development/flatpak","label":"Flatpak","docId":"development/flatpak","unlisted":false}],"href":"/multi-scrobbler/category/development"},{"type":"link","href":"/multi-scrobbler/FAQ","label":"FAQ","docId":"FAQ","unlisted":false},{"type":"link","href":"/multi-scrobbler/help","label":"Help","docId":"help","unlisted":false},{"type":"link","label":"Config Playground","href":"/playground"}]},"docs":{"configuration/clients/clients":{"id":"configuration/clients/clients","title":"Overview","description":"Scrobble Clients","sidebar":"tutorialSidebar"},"configuration/clients/discord/discord":{"id":"configuration/clients/discord/discord","title":"Discord","description":"Discord Client Overview","sidebar":"tutorialSidebar"},"configuration/clients/discord/discord-app":{"id":"configuration/clients/discord/discord-app","title":"Discord (App)","description":"This scrobbler uses Now Playing functionality to set your Discord Rich Presence to the music you are currently monitoring with multi-scrobbler.","sidebar":"tutorialSidebar"},"configuration/clients/discord/discord-headless":{"id":"configuration/clients/discord/discord-headless","title":"Discord (Headless)","description":"This scrobbler uses Now Playing functionality to set your Discord Rich Presence to the music you are currently monitoring with multi-scrobbler.","sidebar":"tutorialSidebar"},"configuration/clients/koito":{"id":"configuration/clients/koito","title":"Koito","description":"To monitor and re-scrobble activity from a Koito account create a Koito (Source)","sidebar":"tutorialSidebar"},"configuration/clients/lastfm":{"id":"configuration/clients/lastfm","title":"Last.fm","description":"* To monitor and re-scrobble activity from your Last.fm/Libre.fm account create a Last.fm (Source)","sidebar":"tutorialSidebar"},"configuration/clients/librefm":{"id":"configuration/clients/librefm","title":"Libre.fm","description":"* To monitor and re-scrobble activity from your Libre.fm account create a Libre.fm (Source)","sidebar":"tutorialSidebar"},"configuration/clients/listenbrainz":{"id":"configuration/clients/listenbrainz","title":"Listenbrainz","description":"* To monitor and re-scrobble activity from a Listenbrainz account create a Listenbrainz (Source)","sidebar":"tutorialSidebar"},"configuration/clients/maloja":{"id":"configuration/clients/maloja","title":"Maloja","description":"Setup a Maloja server if you have not already done this.","sidebar":"tutorialSidebar"},"configuration/clients/rocksky":{"id":"configuration/clients/rocksky","title":"Rocksky","description":"To monitor and re-scrobble activity from Rocksky create a Rocksky (Source)","sidebar":"tutorialSidebar"},"configuration/clients/tealfm":{"id":"configuration/clients/tealfm","title":"Teal.fm","description":"To monitor and re-scrobble activity from a ATProto Profile create a teal.fm (Source)","sidebar":"tutorialSidebar"},"configuration/configuration":{"id":"configuration/configuration","title":"Overview","description":"Configuration","sidebar":"tutorialSidebar"},"configuration/kitchensink":{"id":"configuration/kitchensink","title":"Kitchen Sink","description":"Kitchen Sink Example","sidebar":"tutorialSidebar"},"configuration/sources/azuracast":{"id":"configuration/sources/azuracast","title":"Azuracast","description":"Azuracast may not correctly report track length or position. If this is the case you should reduce the \\"listened to\\" duration using Scrobble Thresholds so that multi-scrobbler scrobbles correctly.","sidebar":"tutorialSidebar"},"configuration/sources/deezer":{"id":"configuration/sources/deezer","title":"Deezer","description":"This Source uses unofficial methods to access Deezer data that are likely in violation of Deezer\'s TOS. Deezer may change or remove these methods at any time, breaking functionality. Use this Source at your own risk.","sidebar":"tutorialSidebar"},"configuration/sources/google-cast":{"id":"configuration/sources/google-cast","title":"Google Cast (Chromecast)","description":"If your media device can be Cast to using this button Chromecast Icon on your phone/computer then multi-scrobbler can monitor it in order to scrobble music you play.","sidebar":"tutorialSidebar"},"configuration/sources/icecast":{"id":"configuration/sources/icecast","title":"Icecast","description":"Icecast Source Configuration","sidebar":"tutorialSidebar"},"configuration/sources/jellyfin":{"id":"configuration/sources/jellyfin","title":"Jellyfin","description":"Must be using Jellyfin 10.7 or greater","sidebar":"tutorialSidebar"},"configuration/sources/jriver":{"id":"configuration/sources/jriver","title":"JRiver","description":"In order for multi-scrobbler to communicate with JRiver you must have Web Server Interface enabled. This can can be in the JRiver GUI:","sidebar":"tutorialSidebar"},"configuration/sources/kodi":{"id":"configuration/sources/kodi","title":"Kodi","description":"In order for multi-scrobbler to communicate with Kodi you must have the Web Interface enabled. This can be done in the Kodi GUI:","sidebar":"tutorialSidebar"},"configuration/sources/koito-source":{"id":"configuration/sources/koito-source","title":"Koito","description":"Koito (Source)","sidebar":"tutorialSidebar"},"configuration/sources/lastfm-endpoint":{"id":"configuration/sources/lastfm-endpoint","title":"Lastfm (Endpoint)","description":"This Source enables multi-scrobbler to accept scrobbles from outside applications as if it was a Last.fm server.","sidebar":"tutorialSidebar"},"configuration/sources/lastfm-source":{"id":"configuration/sources/lastfm-source","title":"Last.fm","description":"Last.fm (Source)","sidebar":"tutorialSidebar"},"configuration/sources/librefm-source":{"id":"configuration/sources/librefm-source","title":"Libre.fm","description":"Libre.fm (Source)","sidebar":"tutorialSidebar"},"configuration/sources/listenbrainz-endpoint":{"id":"configuration/sources/listenbrainz-endpoint","title":"Listenbrainz (Endpoint)","description":"This Source enables multi-scrobbler to accept scrobbles from outside applications as if it was a Listenbrainz server.","sidebar":"tutorialSidebar"},"configuration/sources/listenbrainz-source":{"id":"configuration/sources/listenbrainz-source","title":"Listenbrainz","description":"Listenbrainz (Source)","sidebar":"tutorialSidebar"},"configuration/sources/maloja":{"id":"configuration/sources/maloja","title":"Maloja","description":"To scrobble to a Maloja server, create a Maloja (Client)","sidebar":"tutorialSidebar"},"configuration/sources/mopidy":{"id":"configuration/sources/mopidy","title":"Mopidy","description":"Mopidy is a headless music server that supports playing music from many standard and non-standard sources such as Pandora, Bandcamp, and Tunein.","sidebar":"tutorialSidebar"},"configuration/sources/mpd":{"id":"configuration/sources/mpd","title":"MPD (Music Player Daemon)","description":"MS communicates with MPD using the TCP client connection.","sidebar":"tutorialSidebar"},"configuration/sources/mpris":{"id":"configuration/sources/mpris","title":"MPRIS","description":"MPRIS is a standard interface for communicating with Music Players on linux operating systems.","sidebar":"tutorialSidebar"},"configuration/sources/musikcube":{"id":"configuration/sources/musikcube","title":"Musikcube","description":"In order to use Musikcube configure it to accept websocket connections in server setup:","sidebar":"tutorialSidebar"},"configuration/sources/plex":{"id":"configuration/sources/plex","title":"Plex","description":"Find your Plex Token and make note of the URL and Port used to connect to your Plex instance.","sidebar":"tutorialSidebar"},"configuration/sources/rocksky-source":{"id":"configuration/sources/rocksky-source","title":"Rocksky","description":"Rocksky (Source)","sidebar":"tutorialSidebar"},"configuration/sources/sonos":{"id":"configuration/sources/sonos","title":"Sonos","description":"Multi-scrobbler can use the Sonos local API to monitor what your Sonos system is playing.","sidebar":"tutorialSidebar"},"configuration/sources/sources":{"id":"configuration/sources/sources","title":"Overview","description":"Scrobble Sources","sidebar":"tutorialSidebar"},"configuration/sources/spotify":{"id":"configuration/sources/spotify","title":"Spotify","description":"If your Spotify player has Automix enabled and Spotify uses it for your playlist/queue then MS cannot accurately determine when a track will end. This is because the track is \\"mixed\\" in your queue with a shorter play time than its actual length and Spotify does not report this modified play time in its API. This does not affect MS\'s ability to scrobble from Spotify but it will affect the accuracy of the duration MS reports was played.","sidebar":"tutorialSidebar"},"configuration/sources/subsonic":{"id":"configuration/sources/subsonic","title":"Subsonic","description":"This Source can be used for any application that implements the Subsonic API and supports the getNowPlaying endpoint (such as Airsonic and Navidrome)","sidebar":"tutorialSidebar"},"configuration/sources/tealfm-source":{"id":"configuration/sources/tealfm-source","title":"Teal.fm","description":"Teal.fm (Source)","sidebar":"tutorialSidebar"},"configuration/sources/vlc":{"id":"configuration/sources/vlc","title":"VLC","description":"MS communicates with VLC using the Web (http) interface module","sidebar":"tutorialSidebar"},"configuration/sources/webscrobbler":{"id":"configuration/sources/webscrobbler","title":"Webscrobbler","description":"After installing the Webscrobbler extension open the preferences/settings for it:","sidebar":"tutorialSidebar"},"configuration/sources/yamaha-musiccast":{"id":"configuration/sources/yamaha-musiccast","title":"Yamaha MusicCast","description":"Monitor MusicCast device/receivers for music played on Network/USB/CD inputs.","sidebar":"tutorialSidebar"},"configuration/sources/yandex-music":{"id":"configuration/sources/yandex-music","title":"Yandex Music","description":"Monitor your Yandex Music listening activity in real-time.","sidebar":"tutorialSidebar"},"configuration/sources/youtube-music":{"id":"configuration/sources/youtube-music","title":"Youtube Music","description":"Communication with YT Music is unofficial and not supported or endorsed by Google. This means that this integration may stop working at any time if Google decides to change how YT Music works in the browser.","sidebar":"tutorialSidebar"},"configuration/transforms/musicbrainz":{"id":"configuration/transforms/musicbrainz","title":"Musicbrainz Stage","description":"Enhancing Scrobbles with Musicbrainz","sidebar":"tutorialSidebar"},"configuration/transforms/native":{"id":"configuration/transforms/native","title":"Native Stage","description":"Enhancing Scrobbles with Built-in Parsing","sidebar":"tutorialSidebar"},"configuration/transforms/transforms":{"id":"configuration/transforms/transforms","title":"Enhancing Scrobbles","description":"Multi-scrobbler configs support the ability to enhance scrobble data in an automated fashion by matching and replacing strings in title, artists, and album at many different times in multi-scrobbler\'s lifecycle.","sidebar":"tutorialSidebar"},"configuration/transforms/user":{"id":"configuration/transforms/user","title":"User Stage","description":"Enhancing Scrobbles with Regex Parsing","sidebar":"tutorialSidebar"},"development/dev-common":{"id":"development/dev-common","title":"Common Development","description":"Common Development Knowledge","sidebar":"tutorialSidebar"},"development/dev-source":{"id":"development/dev-source","title":"Source Development/Tutorial","description":"Developing a new Source","sidebar":"tutorialSidebar"},"development/flatpak":{"id":"development/flatpak","title":"Flatpak","description":"Building Flatpak App","sidebar":"tutorialSidebar"},"FAQ":{"id":"FAQ","title":"FAQ","description":"Connection Issues","sidebar":"tutorialSidebar"},"help":{"id":"help","title":"Help","description":"Getting Useful Data","sidebar":"tutorialSidebar"},"index":{"id":"index","title":"Overview","description":"Latest Release","sidebar":"tutorialSidebar"},"installation/installation":{"id":"installation/installation","title":"Overview","description":"Installation","sidebar":"tutorialSidebar"},"installation/service":{"id":"installation/service","title":"As a Service","description":"Local Install as a Service","sidebar":"tutorialSidebar"},"quickstart":{"id":"quickstart","title":"Quickstart","description":"This guide will get you up and running with multi-scrobbler using Docker and docker compose. At the end of the guide you will have:","sidebar":"tutorialSidebar"},"updating/updating":{"id":"updating/updating","title":"Overview","description":"Updating","sidebar":"tutorialSidebar"}}}}')}}]); \ No newline at end of file diff --git a/assets/js/8b39be93.1e9d575b.js b/assets/js/8b39be93.1e9d575b.js new file mode 100644 index 00000000..ae383275 --- /dev/null +++ b/assets/js/8b39be93.1e9d575b.js @@ -0,0 +1,12 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["3963"],{7360(e,t,s){s.d(t,{Ay:()=>h,RM:()=>l});var n=s(4848),o=s(8453),i=s(2170),r=s.n(i);function a(e){let t={a:"a",code:"code",p:"p",strong:"strong",...(0,o.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)(t.p,{children:["This ",(0,n.jsx)(t.strong,{children:"Source"})," supports toggling scrobbling on or off manually. This action can be taken on the Web Dashboard by clicking the link next to ",(0,n.jsx)(t.strong,{children:"Should Scrobble"})," found in each Source's status card."]}),"\n",(0,n.jsxs)(t.p,{children:["Additionally, the default behavior for scrobbling can be explicitly set by adding the following value to a Source's ",(0,n.jsx)(t.code,{children:"options"})," in ",(0,n.jsx)(t.a,{href:"/configuration?configType=file#configuration-types",children:"file or aio config"}),":"]}),"\n",(0,n.jsx)(r(),{language:"json5",title:`${e.filename}.json`,children:`{ + "data": { + // ... + }, + "options": { + // highlight-start + // set to true to always scrobble automatically + // set to false to never scrobble automatically + "systemScrobble": true + // highlight-end + } +}}`})]})}function c(e={}){let{wrapper:t}={...(0,o.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(a,{...e})}):a(e)}let l=[];function d(e){let{DetailsAdmo:t}={...(0,o.R)(),...e.components};return t||function(e,t){throw Error("Expected "+(t?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("DetailsAdmo",!0),(0,n.jsx)(t,{type:"note",summary:"Should Scrobble Behavior",children:(0,n.jsx)(c,{...e})})}function h(e={}){let{wrapper:t}={...(0,o.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},1724(e,t,s){s.r(t),s.d(t,{metadata:()=>n,default:()=>u,frontMatter:()=>a,contentTitle:()=>c,toc:()=>d,assets:()=>l});var n=JSON.parse('{"id":"configuration/sources/icecast","title":"Icecast","description":"Icecast Source Configuration","source":"@site/docs/configuration/sources/icecast.mdx","sourceDirName":"configuration/sources","slug":"/configuration/sources/icecast","permalink":"/multi-scrobbler/configuration/sources/icecast","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/sources/icecast.mdx","tags":[],"version":"current","frontMatter":{"title":"Icecast","toc_min_heading_level":2,"toc_max_heading_level":5,"description":"Icecast Source Configuration"},"sidebar":"tutorialSidebar","previous":{"title":"Google Cast (Chromecast)","permalink":"/multi-scrobbler/configuration/sources/google-cast"},"next":{"title":"Jellyfin","permalink":"/multi-scrobbler/configuration/sources/jellyfin"}}'),o=s(4848),i=s(8453);s(8194),s(698),s(2170);var r=s(7360);let a={title:"Icecast",toc_min_heading_level:2,toc_max_heading_level:5,description:"Icecast Source Configuration"},c,l={},d=[...r.RM,{value:"URL and Sources",id:"url-and-sources",level:4},{value:"Configuration",id:"configuration",level:2}];function h(e){let t={a:"a",admonition:"admonition",code:"code",h2:"h2",h4:"h4",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,i.R)(),...e.components},{Config:s,Details:n}=t;return s||m("Config",!0),n||m("Details",!0),(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.p,{children:"Parse metadata from any Icecast Station (streams) to use for scrobbling."}),"\n",(0,o.jsxs)(t.admonition,{type:"note",children:[(0,o.jsxs)(t.p,{children:["There is no standard format for presenting track information in an Icecast stream and the majority of streams that do include metadata only use a single string (",(0,o.jsx)(t.code,{children:"StreamTitle"}),") from which metadata can be parsed."]}),(0,o.jsx)(t.p,{children:"Multi-scrobbler will do its best to parse Artist/Album/Track information from this data but the quality and consistency for this is solely based on how the Icecast broadcaster chooses to format their tracks."})]}),"\n",(0,o.jsxs)(t.p,{children:["Multi-scrobbler cannot automatically determine if you are listening to a Station. Therefore, ",(0,o.jsx)(t.strong,{children:"automatic scrobbling for Icecast Sources is off by default."})," Use ",(0,o.jsx)(t.strong,{children:"Should Scrobble Behavior"})," to initiate scrobbling or configure automatic scrobble."]}),"\n",(0,o.jsx)(r.Ay,{filename:"icecast"}),"\n",(0,o.jsx)(t.admonition,{title:"Scrobbling Threshold",type:"tip",children:(0,o.jsxs)(t.p,{children:['Icecast does not report track lengths so multi-scrobbler cannot determine if it should scrobble based on percent played. You should instead set a shorter "listened to" ',(0,o.jsx)(t.strong,{children:"duration"})," using ",(0,o.jsx)(t.a,{href:"/configuration/sources#scrobble-thresholds",children:"Scrobble Thresholds."})]})}),"\n",(0,o.jsx)(t.h4,{id:"url-and-sources",children:"URL and Sources"}),"\n",(0,o.jsx)(t.p,{children:"The URL should be the URL you would use to stream audio."}),"\n",(0,o.jsxs)(t.p,{children:["Additional sources are supported based on those found in ",(0,o.jsx)(t.a,{href:"https://github.com/eshaz/icecast-metadata-js/tree/main/src/icecast-metadata-stats#supported-status-apis",children:(0,o.jsx)(t.code,{children:"icecast-metadata-stats"})}),". To use these with automatic parsing ",(0,o.jsx)(t.a,{href:"https://github.com/eshaz/icecast-metadata-js/tree/main/src/icecast-metadata-stats#options",children:"based on the regular URL"})," include a ",(0,o.jsx)(t.code,{children:"sources"})," array in your config the same way as shown in ",(0,o.jsx)(t.code,{children:"icecast-metadata-stats"}),":"]}),"\n",(0,o.jsxs)(n,{children:[(0,o.jsx)("summary",{children:"Example"}),(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-json5",metastring:'title="config.json"',children:'[\n {\n "name": "myIcy",\n "enable": true,\n "data": {\n "url": "http://myIcecast.com/radio",\n "sources": ["icy", "ogg", "sevenhtml"]\n }\n }\n ]\n \n'})})]}),"\n",(0,o.jsxs)(t.p,{children:["To manually set these sources include the same ",(0,o.jsxs)(t.a,{href:"https://github.com/eshaz/icecast-metadata-js/tree/main/src/icecast-metadata-stats#options",children:[(0,o.jsx)(t.code,{children:"*Endpoint"})," options"]})," found in ",(0,o.jsx)(t.code,{children:"icecast-metadata-stats"}),"."]}),"\n",(0,o.jsxs)(t.p,{children:["If no ",(0,o.jsx)(t.code,{children:"sources"})," are configured for multi-scrobbler it will automatically try to use ",(0,o.jsx)(t.code,{children:"icy"})," and ",(0,o.jsx)(t.code,{children:"ogg"}),"."]}),"\n",(0,o.jsx)(t.h2,{id:"configuration",children:"Configuration"}),"\n",(0,o.jsx)(s,{config:"IcecastSourceConfig",fileContent:'[\n {\n "name": "myIcy",\n "enable": true,\n "data": {\n "url": "http://myIcecast.com/radio"\n }\n }\n ]\n ',name:"icecast",children:(0,o.jsxs)(t.table,{children:[(0,o.jsx)(t.thead,{children:(0,o.jsxs)(t.tr,{children:[(0,o.jsx)(t.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,o.jsx)(t.th,{style:{textAlign:"left"},children:"Required?"}),(0,o.jsx)(t.th,{style:{textAlign:"left"},children:"Default"}),(0,o.jsx)(t.th,{children:"Description"})]})}),(0,o.jsx)(t.tbody,{children:(0,o.jsxs)(t.tr,{children:[(0,o.jsx)(t.td,{style:{textAlign:"left"},children:(0,o.jsx)(t.code,{children:"ICECAST_URL"})}),(0,o.jsx)(t.td,{style:{textAlign:"left"},children:"Yes"}),(0,o.jsx)(t.td,{style:{textAlign:"left"}}),(0,o.jsx)(t.td,{children:"Icecast URL"})]})})]})})]})}function u(e={}){let{wrapper:t}={...(0,i.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(h,{...e})}):h(e)}function m(e,t){throw Error("Expected "+(t?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}}}]); \ No newline at end of file diff --git a/assets/js/8b39be93.a9a5493f.js b/assets/js/8b39be93.a9a5493f.js deleted file mode 100644 index ba3a2321..00000000 --- a/assets/js/8b39be93.a9a5493f.js +++ /dev/null @@ -1,12 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["3963"],{7360(e,t,s){s.d(t,{Ay:()=>h,RM:()=>l});var n=s(4848),o=s(8453),r=s(2170),i=s.n(r);function a(e){let t={a:"a",code:"code",p:"p",strong:"strong",...(0,o.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)(t.p,{children:["This ",(0,n.jsx)(t.strong,{children:"Source"})," supports toggling scrobbling on or off manually. This action can be taken on the Web Dashboard by clicking the link next to ",(0,n.jsx)(t.strong,{children:"Should Scrobble"})," found in each Source's status card."]}),"\n",(0,n.jsxs)(t.p,{children:["Additionally, the default behavior for scrobbling can be explicitly set by adding the following value to a Source's ",(0,n.jsx)(t.code,{children:"options"})," in ",(0,n.jsx)(t.a,{href:"/configuration?configType=file#configuration-types",children:"file or aio config"}),":"]}),"\n",(0,n.jsx)(i(),{language:"json5",title:`${e.filename}.json`,children:`{ - "data": { - // ... - }, - "options": { - // highlight-start - // set to true to always scrobble automatically - // set to false to never scrobble automatically - "systemScrobble": true - // highlight-end - } -}}`})]})}function c(e={}){let{wrapper:t}={...(0,o.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(a,{...e})}):a(e)}let l=[];function d(e){let{DetailsAdmo:t}={...(0,o.R)(),...e.components};return t||function(e,t){throw Error("Expected "+(t?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("DetailsAdmo",!0),(0,n.jsx)(t,{type:"note",summary:"Should Scrobble Behavior",children:(0,n.jsx)(c,{...e})})}function h(e={}){let{wrapper:t}={...(0,o.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},1724(e,t,s){s.r(t),s.d(t,{metadata:()=>n,default:()=>u,frontMatter:()=>a,contentTitle:()=>c,toc:()=>d,assets:()=>l});var n=JSON.parse('{"id":"configuration/sources/icecast","title":"Icecast","description":"Parse metadata from any Icecast Station (streams) to use for scrobbling.","source":"@site/docs/configuration/sources/icecast.mdx","sourceDirName":"configuration/sources","slug":"/configuration/sources/icecast","permalink":"/multi-scrobbler/configuration/sources/icecast","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/sources/icecast.mdx","tags":[],"version":"current","frontMatter":{"title":"Icecast","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Google Cast (Chromecast)","permalink":"/multi-scrobbler/configuration/sources/google-cast"},"next":{"title":"Jellyfin","permalink":"/multi-scrobbler/configuration/sources/jellyfin"}}'),o=s(4848),r=s(8453);s(8194),s(698),s(2170);var i=s(7360);let a={title:"Icecast",toc_min_heading_level:2,toc_max_heading_level:5},c,l={},d=[...i.RM,{value:"URL and Sources",id:"url-and-sources",level:4},{value:"Configuration",id:"configuration",level:2}];function h(e){let t={a:"a",admonition:"admonition",code:"code",h2:"h2",h4:"h4",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,r.R)(),...e.components},{Config:s,Details:n}=t;return s||m("Config",!0),n||m("Details",!0),(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.p,{children:"Parse metadata from any Icecast Station (streams) to use for scrobbling."}),"\n",(0,o.jsxs)(t.admonition,{type:"note",children:[(0,o.jsxs)(t.p,{children:["There is no standard format for presenting track information in an Icecast stream and the majority of streams that do include metadata only use a single string (",(0,o.jsx)(t.code,{children:"StreamTitle"}),") from which metadata can be parsed."]}),(0,o.jsx)(t.p,{children:"Multi-scrobbler will do its best to parse Artist/Album/Track information from this data but the quality and consistency for this is solely based on how the Icecast broadcaster chooses to format their tracks."})]}),"\n",(0,o.jsxs)(t.p,{children:["Multi-scrobbler cannot automatically determine if you are listening to a Station. Therefore, ",(0,o.jsx)(t.strong,{children:"automatic scrobbling for Icecast Sources is off by default."})," Use ",(0,o.jsx)(t.strong,{children:"Should Scrobble Behavior"})," to initiate scrobbling or configure automatic scrobble."]}),"\n",(0,o.jsx)(i.Ay,{filename:"icecast"}),"\n",(0,o.jsx)(t.admonition,{title:"Scrobbling Threshold",type:"tip",children:(0,o.jsxs)(t.p,{children:['Icecast does not report track lengths so multi-scrobbler cannot determine if it should scrobble based on percent played. You should instead set a shorter "listened to" ',(0,o.jsx)(t.strong,{children:"duration"})," using ",(0,o.jsx)(t.a,{href:"/configuration/sources#scrobble-thresholds",children:"Scrobble Thresholds."})]})}),"\n",(0,o.jsx)(t.h4,{id:"url-and-sources",children:"URL and Sources"}),"\n",(0,o.jsx)(t.p,{children:"The URL should be the URL you would use to stream audio."}),"\n",(0,o.jsxs)(t.p,{children:["Additional sources are supported based on those found in ",(0,o.jsx)(t.a,{href:"https://github.com/eshaz/icecast-metadata-js/tree/main/src/icecast-metadata-stats#supported-status-apis",children:(0,o.jsx)(t.code,{children:"icecast-metadata-stats"})}),". To use these with automatic parsing ",(0,o.jsx)(t.a,{href:"https://github.com/eshaz/icecast-metadata-js/tree/main/src/icecast-metadata-stats#options",children:"based on the regular URL"})," include a ",(0,o.jsx)(t.code,{children:"sources"})," array in your config the same way as shown in ",(0,o.jsx)(t.code,{children:"icecast-metadata-stats"}),":"]}),"\n",(0,o.jsxs)(n,{children:[(0,o.jsx)("summary",{children:"Example"}),(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-json5",metastring:'title="config.json"',children:'[\n {\n "name": "myIcy",\n "enable": true,\n "data": {\n "url": "http://myIcecast.com/radio",\n "sources": ["icy", "ogg", "sevenhtml"]\n }\n }\n ]\n \n'})})]}),"\n",(0,o.jsxs)(t.p,{children:["To manually set these sources include the same ",(0,o.jsxs)(t.a,{href:"https://github.com/eshaz/icecast-metadata-js/tree/main/src/icecast-metadata-stats#options",children:[(0,o.jsx)(t.code,{children:"*Endpoint"})," options"]})," found in ",(0,o.jsx)(t.code,{children:"icecast-metadata-stats"}),"."]}),"\n",(0,o.jsxs)(t.p,{children:["If no ",(0,o.jsx)(t.code,{children:"sources"})," are configured for multi-scrobbler it will automatically try to use ",(0,o.jsx)(t.code,{children:"icy"})," and ",(0,o.jsx)(t.code,{children:"ogg"}),"."]}),"\n",(0,o.jsx)(t.h2,{id:"configuration",children:"Configuration"}),"\n",(0,o.jsx)(s,{config:"IcecastSourceConfig",fileContent:'[\n {\n "name": "myIcy",\n "enable": true,\n "data": {\n "url": "http://myIcecast.com/radio"\n }\n }\n ]\n ',name:"icecast",children:(0,o.jsxs)(t.table,{children:[(0,o.jsx)(t.thead,{children:(0,o.jsxs)(t.tr,{children:[(0,o.jsx)(t.th,{style:{textAlign:"left"},children:"Environmental Variable"}),(0,o.jsx)(t.th,{style:{textAlign:"left"},children:"Required?"}),(0,o.jsx)(t.th,{style:{textAlign:"left"},children:"Default"}),(0,o.jsx)(t.th,{children:"Description"})]})}),(0,o.jsx)(t.tbody,{children:(0,o.jsxs)(t.tr,{children:[(0,o.jsx)(t.td,{style:{textAlign:"left"},children:(0,o.jsx)(t.code,{children:"ICECAST_URL"})}),(0,o.jsx)(t.td,{style:{textAlign:"left"},children:"Yes"}),(0,o.jsx)(t.td,{style:{textAlign:"left"}}),(0,o.jsx)(t.td,{children:"Icecast URL"})]})})]})})]})}function u(e={}){let{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(h,{...e})}):h(e)}function m(e,t){throw Error("Expected "+(t?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}}}]); \ No newline at end of file diff --git a/assets/js/9618.f9eb3fe0.js b/assets/js/9618.d22735ab.js similarity index 95% rename from assets/js/9618.f9eb3fe0.js rename to assets/js/9618.d22735ab.js index c63939ea..0ae4a682 100644 --- a/assets/js/9618.f9eb3fe0.js +++ b/assets/js/9618.d22735ab.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?_=41ac8f91",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?_=33d0c060",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/9bcab3db.98c754f9.js b/assets/js/9bcab3db.98c754f9.js new file mode 100644 index 00000000..d809cdf9 --- /dev/null +++ b/assets/js/9bcab3db.98c754f9.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["490"],{5345(e,t,n){n.r(t),n.d(t,{metadata:()=>i,default:()=>h,frontMatter:()=>c,contentTitle:()=>a,toc:()=>d,assets:()=>l});var i=JSON.parse('{"id":"configuration/sources/lastfm-source","title":"Last.fm","description":"Last.fm (Source)","source":"@site/docs/configuration/sources/lastfm-source.mdx","sourceDirName":"configuration/sources","slug":"/configuration/sources/lastfm-source","permalink":"/multi-scrobbler/configuration/sources/lastfm-source","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/sources/lastfm-source.mdx","tags":[],"version":"current","frontMatter":{"title":"Last.fm","description":"Last.fm (Source)","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Lastfm (Endpoint)","permalink":"/multi-scrobbler/configuration/sources/lastfm-endpoint"},"next":{"title":"Libre.fm","permalink":"/multi-scrobbler/configuration/sources/librefm-source"}}'),s=n(4848),r=n(8453);n(8194),n(698),n(2170);var o=n(1432);let c={title:"Last.fm",description:"Last.fm (Source)",toc_min_heading_level:2,toc_max_heading_level:5},a,l={},d=[{value:"Configuration",id:"configuration",level:2}];function u(e){let t={a:"a",admonition:"admonition",code:"code",h2:"h2",li:"li",p:"p",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components},{Config:n}=t;return n||function(e,t){throw Error("Expected "+(t?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Config",!0),(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(t.p,{children:["This Source ",(0,s.jsx)(t.strong,{children:"monitors your Last.fm scrobble history"})," and re-scrobbles new activity to configured ",(0,s.jsx)(t.a,{href:"/configuration/clients",children:"Clients."})]}),"\n",(0,s.jsx)(t.admonition,{title:"Other Uses",type:"tip",children:(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsxs)(t.li,{children:["To ",(0,s.jsx)(t.strong,{children:"scrobble to"})," Last.fm/Libre.fm, create a ",(0,s.jsx)(t.a,{href:"/configuration/clients/lastfm",children:"Last.fm (Client)"})]}),"\n",(0,s.jsxs)(t.li,{children:["To accept scrobbles from outside applications as if multi-scrobbler was Last.fm, create a ",(0,s.jsx)(t.a,{href:"/configuration/sources/lastfm-endpoint",children:"Last.fm (Endpoint)"})]}),"\n"]})}),"\n",(0,s.jsxs)(t.p,{children:["To configure this Source use the same registration instructions and configuration data shown in ",(0,s.jsx)(t.a,{href:"/configuration/clients/lastfm",children:"Last.fm (Client)"})," with the difference being ",(0,s.jsx)(t.code,{children:'"configureAs": "source"'})," defined in the File/AIO configuration below."]}),"\n",(0,s.jsx)(t.h2,{id:"configuration",children:"Configuration"}),"\n",(0,s.jsx)(n,{config:"LastfmSourceConfig",fileContent:o.A,name:"lastfm",children:(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Environmental Variable"}),(0,s.jsx)(t.th,{children:"Required?"}),(0,s.jsx)(t.th,{children:"Default"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"SOURCE_LASTFM_API_KEY"})}),(0,s.jsx)(t.td,{children:"Yes"}),(0,s.jsx)(t.td,{}),(0,s.jsx)(t.td,{children:"Api Key from your API Account"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"SOURCE_LASTFM_SECRET"})}),(0,s.jsx)(t.td,{children:"Yes"}),(0,s.jsx)(t.td,{}),(0,s.jsx)(t.td,{children:"Shared secret from your API Account"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"SOURCE_LASTFM_REDIRECT_URI"})}),(0,s.jsx)(t.td,{children:"No"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"http://localhost:9078/lastfm/callback"})}),(0,s.jsxs)(t.td,{children:["Url to use for authentication. Must include ",(0,s.jsx)(t.code,{children:"lastfm/callback"})," somewhere in it"]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"SOURCE_LASTFM_SESSION"})}),(0,s.jsx)(t.td,{children:"No"}),(0,s.jsx)(t.td,{}),(0,s.jsx)(t.td,{children:"Session id. Will be generated by authentication flow if not provided."})]})]})]})})]})}function h(e={}){let{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(u,{...e})}):u(e)}},1432(e,t,n){n.d(t,{A:()=>i});let i='[\n {\n "name": "myLastFmClient",\n "enable": true,\n "configureAs": "client",\n "data": {\n "apiKey": "a89cba1569901a0671d5a9875fed4be1",\n "secret": "ec42e09d5ae0ee0f0816ca151008412a",\n "redirectUri": "http://localhost:9078/lastfm/callback"\n }\n },\n {\n "name": "myLastFmSource",\n "enable": true,\n "configureAs": "source",\n "data": {\n "apiKey": "a89cba1569901a0671d5a9875fed4be1",\n "secret": "ec42e09d5ae0ee0f0816ca151008412a",\n "redirectUri": "http://localhost:9078/lastfm/callback"\n }\n }\n]\n'}}]); \ No newline at end of file diff --git a/assets/js/9bcab3db.c96bc0e0.js b/assets/js/9bcab3db.c96bc0e0.js deleted file mode 100644 index c3490b1b..00000000 --- a/assets/js/9bcab3db.c96bc0e0.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["490"],{5345(e,t,n){n.r(t),n.d(t,{metadata:()=>i,default:()=>h,frontMatter:()=>c,contentTitle:()=>a,toc:()=>d,assets:()=>l});var i=JSON.parse('{"id":"configuration/sources/lastfm-source","title":"Last.fm","description":"This Source monitors your Last.fm scrobble history and re-scrobbles new activity to configured Clients.","source":"@site/docs/configuration/sources/lastfm-source.mdx","sourceDirName":"configuration/sources","slug":"/configuration/sources/lastfm-source","permalink":"/multi-scrobbler/configuration/sources/lastfm-source","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/sources/lastfm-source.mdx","tags":[],"version":"current","frontMatter":{"title":"Last.fm","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Lastfm (Endpoint)","permalink":"/multi-scrobbler/configuration/sources/lastfm-endpoint"},"next":{"title":"Libre.fm","permalink":"/multi-scrobbler/configuration/sources/librefm-source"}}'),s=n(4848),r=n(8453);n(8194),n(698),n(2170);var o=n(1432);let c={title:"Last.fm",toc_min_heading_level:2,toc_max_heading_level:5},a,l={},d=[{value:"Configuration",id:"configuration",level:2}];function u(e){let t={a:"a",admonition:"admonition",code:"code",h2:"h2",li:"li",p:"p",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components},{Config:n}=t;return n||function(e,t){throw Error("Expected "+(t?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Config",!0),(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(t.p,{children:["This Source ",(0,s.jsx)(t.strong,{children:"monitors your Last.fm scrobble history"})," and re-scrobbles new activity to configured ",(0,s.jsx)(t.a,{href:"/configuration/clients",children:"Clients."})]}),"\n",(0,s.jsx)(t.admonition,{title:"Other Uses",type:"tip",children:(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsxs)(t.li,{children:["To ",(0,s.jsx)(t.strong,{children:"scrobble to"})," Last.fm/Libre.fm, create a ",(0,s.jsx)(t.a,{href:"/configuration/clients/lastfm",children:"Last.fm (Client)"})]}),"\n",(0,s.jsxs)(t.li,{children:["To accept scrobbles from outside applications as if multi-scrobbler was Last.fm, create a ",(0,s.jsx)(t.a,{href:"/configuration/sources/lastfm-endpoint",children:"Last.fm (Endpoint)"})]}),"\n"]})}),"\n",(0,s.jsxs)(t.p,{children:["To configure this Source use the same registration instructions and configuration data shown in ",(0,s.jsx)(t.a,{href:"/configuration/clients/lastfm",children:"Last.fm (Client)"})," with the difference being ",(0,s.jsx)(t.code,{children:'"configureAs": "source"'})," defined in the File/AIO configuration below."]}),"\n",(0,s.jsx)(t.h2,{id:"configuration",children:"Configuration"}),"\n",(0,s.jsx)(n,{config:"LastfmSourceConfig",fileContent:o.A,name:"lastfm",children:(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Environmental Variable"}),(0,s.jsx)(t.th,{children:"Required?"}),(0,s.jsx)(t.th,{children:"Default"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"SOURCE_LASTFM_API_KEY"})}),(0,s.jsx)(t.td,{children:"Yes"}),(0,s.jsx)(t.td,{}),(0,s.jsx)(t.td,{children:"Api Key from your API Account"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"SOURCE_LASTFM_SECRET"})}),(0,s.jsx)(t.td,{children:"Yes"}),(0,s.jsx)(t.td,{}),(0,s.jsx)(t.td,{children:"Shared secret from your API Account"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"SOURCE_LASTFM_REDIRECT_URI"})}),(0,s.jsx)(t.td,{children:"No"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"http://localhost:9078/lastfm/callback"})}),(0,s.jsxs)(t.td,{children:["Url to use for authentication. Must include ",(0,s.jsx)(t.code,{children:"lastfm/callback"})," somewhere in it"]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"SOURCE_LASTFM_SESSION"})}),(0,s.jsx)(t.td,{children:"No"}),(0,s.jsx)(t.td,{}),(0,s.jsx)(t.td,{children:"Session id. Will be generated by authentication flow if not provided."})]})]})]})})]})}function h(e={}){let{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(u,{...e})}):u(e)}},1432(e,t,n){n.d(t,{A:()=>i});let i='[\n {\n "name": "myLastFmClient",\n "enable": true,\n "configureAs": "client",\n "data": {\n "apiKey": "a89cba1569901a0671d5a9875fed4be1",\n "secret": "ec42e09d5ae0ee0f0816ca151008412a",\n "redirectUri": "http://localhost:9078/lastfm/callback"\n }\n },\n {\n "name": "myLastFmSource",\n "enable": true,\n "configureAs": "source",\n "data": {\n "apiKey": "a89cba1569901a0671d5a9875fed4be1",\n "secret": "ec42e09d5ae0ee0f0816ca151008412a",\n "redirectUri": "http://localhost:9078/lastfm/callback"\n }\n }\n]\n'}}]); \ No newline at end of file diff --git a/assets/js/a16703cd.092bf30d.js b/assets/js/a16703cd.092bf30d.js new file mode 100644 index 00000000..92da7836 --- /dev/null +++ b/assets/js/a16703cd.092bf30d.js @@ -0,0 +1,13 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["5515"],{7265(e,i,s){s.d(i,{Ay:()=>c,RM:()=>o});var t=s(4848),n=s(8453),l=s(2170),r=s.n(l);let o=[];function a(e){let i={a:"a",code:"code",li:"li",p:"p",strong:"strong",ul:"ul",...(0,n.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(i.p,{children:["A ",(0,t.jsx)(i.strong,{children:"Source"})," that monitors active listening uses one of two metrics to determine if a Song is scrobbable based on your listening:"]}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"percent"})," of track listened to (50% or more)"]}),"\n",(0,t.jsxs)(i.li,{children:["or amount of time (",(0,t.jsx)(i.strong,{children:"duration"}),") track was listened to (4 minutes or more)"]}),"\n"]}),"\n",(0,t.jsxs)(i.p,{children:["These default values are based on ",(0,t.jsx)(i.a,{href:"https://www.last.fm/api/scrobbling#when-is-a-scrobble-a-scrobble",children:"last.fm's scrobble guidance."})]}),"\n",(0,t.jsxs)(i.p,{children:["These values can be customized in the ",(0,t.jsx)(i.code,{children:"options"})," of Source's ",(0,t.jsx)(i.a,{href:"/configuration#configuration-types",children:"file or aio config"}),":"]}),"\n",(0,t.jsx)(r(),{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:i}={...(0,n.R)(),...e.components};return i?(0,t.jsx)(i,{...e,children:(0,t.jsx)(a,{...e})}):a(e)}},662(e,i,s){s.r(i),s.d(i,{metadata:()=>t,default:()=>h,frontMatter:()=>r,contentTitle:()=>o,toc:()=>c,assets:()=>a});var t=JSON.parse('{"id":"configuration/clients/clients","title":"Overview","description":"Scrobble Clients","source":"@site/docs/configuration/clients/clients.mdx","sourceDirName":"configuration/clients","slug":"/configuration/clients/","permalink":"/multi-scrobbler/configuration/clients/","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/clients/clients.mdx","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"sidebar_position":2,"title":"Overview","description":"Scrobble Clients","toc_max_heading_level":4},"sidebar":"tutorialSidebar","previous":{"title":"Overview","permalink":"/multi-scrobbler/configuration/"},"next":{"title":"Discord","permalink":"/multi-scrobbler/configuration/clients/discord/"}}'),n=s(4848),l=s(8453);s(7265);let r={sidebar_position:2,title:"Overview",description:"Scrobble Clients",toc_max_heading_level:4},o,a={},c=[{value:"Clients",id:"clients",level:2},{value:"Features",id:"features",level:2},{value:"Duplicate Detection",id:"duplicate-detection",level:3},{value:"Dead Scrobbles",id:"dead-scrobbles",level:3},{value:"Now Playing",id:"now-playing",level:3}];function d(e){let i={a:"a",code:"code",em:"em",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,l.R)(),...e.components},{Details:s}=i;return s||function(e,i){throw Error("Expected "+(i?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)(i.p,{children:["A ",(0,n.jsx)(i.strong,{children:"Client"})," is an application that stores the historical information about what songs you have played (scrobbles). Examples are ",(0,n.jsx)(i.strong,{children:"Maloja, Last.fm, Listenbrainz"}),"..."]}),"\n",(0,n.jsx)(i.h2,{id:"clients",children:"Clients"}),"\n",(0,n.jsxs)(i.table,{children:[(0,n.jsx)(i.thead,{children:(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.th,{style:{textAlign:"left"},children:"Name"}),(0,n.jsx)(i.th,{style:{textAlign:"left"},children:"Now Playing"})]})}),(0,n.jsxs)(i.tbody,{children:[(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{style:{textAlign:"left"},children:(0,n.jsx)(i.a,{href:"/configuration/clients/discord",children:"Discord"})}),(0,n.jsx)(i.td,{style:{textAlign:"left"},children:"\u2705"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{style:{textAlign:"left"},children:(0,n.jsx)(i.a,{href:"/configuration/clients/lastfm",children:"Last.fm"})}),(0,n.jsx)(i.td,{style:{textAlign:"left"},children:"\u2705"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{style:{textAlign:"left"},children:(0,n.jsx)(i.a,{href:"/configuration/clients/librefm",children:"Libre.fm"})}),(0,n.jsx)(i.td,{style:{textAlign:"left"},children:"\u2705"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{style:{textAlign:"left"},children:(0,n.jsx)(i.a,{href:"/configuration/clients/listenbrainz",children:"Listenbrainz"})}),(0,n.jsx)(i.td,{style:{textAlign:"left"},children:"\u2705"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{style:{textAlign:"left"},children:(0,n.jsx)(i.a,{href:"/configuration/clients/koito",children:"Koito"})}),(0,n.jsx)(i.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{style:{textAlign:"left"},children:(0,n.jsx)(i.a,{href:"/configuration/clients/maloja",children:"Maloja"})}),(0,n.jsx)(i.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{style:{textAlign:"left"},children:(0,n.jsx)(i.a,{href:"/configuration/clients/rocksky",children:"Rocksky"})}),(0,n.jsx)(i.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{style:{textAlign:"left"},children:(0,n.jsx)(i.a,{href:"/configuration/clients/tealfm",children:"Teal.fm"})}),(0,n.jsx)(i.td,{style:{textAlign:"left"},children:"\u274C"})]})]})]}),"\n",(0,n.jsx)(i.h2,{id:"features",children:"Features"}),"\n",(0,n.jsx)(i.h3,{id:"duplicate-detection",children:"Duplicate Detection"}),"\n",(0,n.jsx)(i.p,{children:"Before a Play is scrobbled to a client MS checks existing scrobbles from the Client's API to see if the Play has already been scrobbled."}),"\n",(0,n.jsx)(i.p,{children:"For each Play, MS fetches (cached) scrobbles from the Client in a time range inclusive of the Play's listening timestamp and then scores existing scrobbles against the Play based on:"}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:"Similarity of Title, Artists, and Album"}),"\n",(0,n.jsx)(i.li,{children:"Temporal closeness of the Play's timestamp to the existing scrobble's timestamp"}),"\n",(0,n.jsx)(i.li,{children:"Whether MS detected the Play as a repeat (applicable to Sources that report realtime Player data)"}),"\n"]}),"\n",(0,n.jsxs)(i.p,{children:["Detailed scoring breakdowns against each existing scrobble are logged at the ",(0,n.jsx)(i.code,{children:"TRACE"})," level."]}),"\n",(0,n.jsxs)(s,{children:[(0,n.jsx)("summary",{children:"Detailed Explanation"}),(0,n.jsxs)(i.p,{children:["A ",(0,n.jsx)(i.strong,{children:"candidate"})," (to be scrobbled) Play is first transformed using the configured ",(0,n.jsxs)(i.a,{href:"/configuration/transforms/#hook",children:[(0,n.jsx)(i.code,{children:"compare.candidate"})," Hook"]}),", if any exists."]}),(0,n.jsxs)(i.p,{children:["Next, MS checks (up to) the last 100 scrobbles ",(0,n.jsx)(i.em,{children:"in-memory"})," scrobbles that it has made. These are ",(0,n.jsx)(i.em,{children:"not"})," from the Client but the actual scrobbles MS made while it has been running. The data from these scrobbles is much richer than what is usually parsed from the Client which makes it easier to detect duplicates from."]}),(0,n.jsx)(i.p,{children:"If no in-memory scrobble matches then MS starts comparing the candidate against historical scrobbles fetched from an inclusive time range of the candidate's timestamp."}),(0,n.jsx)("h3",{children:"Matching Title/Artist/Album"}),(0,n.jsxs)(i.p,{children:["For these string-based values MS uses an ",(0,n.jsx)(i.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/0dfa4c7aad6df98e13aee6d395827665c2414adb/src/backend/utils/StringUtils.ts#L299",children:"token-order-invariant"})," method that scores string similarity based on a (token count) weighted average of two ",(0,n.jsx)(i.a,{href:"https://foxxmd.github.io/string-sameness/#md:strategies",children:"similarity"})," algorithms, ",(0,n.jsx)(i.a,{href:"https://en.wikipedia.org/wiki/Levenshtein_distance",children:"Levenshtien Distance"})," and ",(0,n.jsx)(i.a,{href:"https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient",children:"Dice's Coefficient"}),". The weighted average ensures that very long strings (Track titles) are scored with a confidence proportional to their length."]}),(0,n.jsx)("h3",{children:"Matching Timestamp"}),(0,n.jsx)(i.p,{children:"Timestamps are scored on how temporally close they are. There are four possible scores with decreasing value:"}),(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"Exact"})," - Timestamps are within 1 second of each other"]}),"\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"Close"})," - Timestamps are within ",(0,n.jsx)(i.code,{children:"threshold"})," seconds of each other","\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:["This is determined by the smallest update interval of Source","\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:['Some Sources (subsonic) only update every 60 seconds so this is the smallest "close" value possible. Most are 10 seconds. Signified by ',(0,n.jsx)(i.code,{children:"(Needed <10s)"})," in the breakdown example below."]}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"Fuzzy"})," - One timestamp is within ",(0,n.jsx)(i.code,{children:"threshold"})," seconds of the ",(0,n.jsx)(i.em,{children:"end"})," of the other timestamp","\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:["Sources can set the scrobble timestamp at different times. Some do it when the track is started listening to, some when it ends, some when the ",(0,n.jsx)(i.em,{children:"player"})," stops."]}),"\n",(0,n.jsxs)(i.li,{children:["Where possible, MS knows and keeps track of when this timestamp ",(0,n.jsx)(i.em,{children:"should"})," be, for each Source. If it's not possible then Fuzzy may be allowed."]}),"\n"]}),"\n"]}),"\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"None"})," - There is no correlation between timestamps"]}),"\n"]}),(0,n.jsx)("h3",{children:"Scoring and Breakdows"}),(0,n.jsx)(i.p,{children:"A candidate Play must score >= 1 to be detected as a duplicate of an existing scrobble."}),(0,n.jsxs)(i.p,{children:["Each score and a breakdown of the scores for its individual components can be see at the ",(0,n.jsx)(i.code,{children:"TRACE"})," logging level or in the ",(0,n.jsx)(i.a,{href:"/help#copy-play-debug-data",children:"debug data"})," for a scrobble. An example:"]}),(0,n.jsx)(i.pre,{children:(0,n.jsx)(i.code,{children:"* Artist: 0.06 * 0.3 = 0.02\n* Title: 0.04 * 0.4 = 0.02\n* Time: (Exact) 1 * 0.5 = 0.50\n * Existing: 19:13:33-04:00 - Candidate: 19:13:33-04:00\n * Temporal Sameness: Exact\n * Play Diff: 0s (Needed <10s)\n * Range Comparison N/A\nScore 0.54 => No Match\n"})}),(0,n.jsx)(i.p,{children:"In each component equation, the first number is the similarity (or temporal closeness)."}),(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:"0 = no correlation"}),"\n",(0,n.jsx)(i.li,{children:"1 = exactly the same"}),"\n"]}),(0,n.jsxs)(i.p,{children:["The second number is the ",(0,n.jsx)(i.em,{children:"weight"})," of that component in the final score."]})]}),"\n",(0,n.jsx)(i.h3,{id:"dead-scrobbles",children:"Dead Scrobbles"}),"\n",(0,n.jsx)(i.p,{children:"If multi-scrobbler is unable to submit a scrobble to a Client then it places the scrobble into a queue which is retried every 5 minutes for a number of times before it gives up."}),"\n",(0,n.jsxs)(i.p,{children:["After it stops ",(0,n.jsx)(i.em,{children:"automatically"})," retrying the scrobble still exists and can be retried from the dashboard. Dead scrobbles persist between restart so you will never lose a scrobble that hasn't been successfully submitted yet."]}),"\n",(0,n.jsx)(i.h3,{id:"now-playing",children:"Now Playing"}),"\n",(0,n.jsxs)(i.p,{children:["Multi-scrobbler can report the currently ",(0,n.jsx)(i.strong,{children:"playing"})," tracks it is monitoring to some Scrobble Clients via their individual ",(0,n.jsx)(i.strong,{children:"Now Playing"})," functionality."]}),"\n",(0,n.jsxs)(i.p,{children:["Now Playing is ",(0,n.jsx)(i.strong,{children:"default enabled"})," for all Clients that support it."]}),"\n",(0,n.jsx)(i.p,{children:"The behavior multi-scrobbler uses for determining Now Playing reporting:"}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:["Now Playing can be explicitly enabled or disabled globally using ENV ",(0,n.jsx)(i.code,{children:"NOW_PLAYING=true"})," or ",(0,n.jsx)(i.code,{children:"NOW_PLAYING=false"}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:"This only affects Clients that don't have behavior set via File/AIO (below)"}),"\n"]}),"\n"]}),"\n",(0,n.jsx)(i.li,{children:"Now Playing will be only be reported to the same Clients a Source is configured to scrobble to"}),"\n",(0,n.jsxs)(i.li,{children:["A Source's Player status must be ",(0,n.jsx)(i.strong,{children:"Playing"}),", IE ",(0,n.jsx)(i.strong,{children:"active"}),", for reporting to occur","\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:"This status can be verified via Player UI on the Dashboard"}),"\n"]}),"\n"]}),"\n",(0,n.jsxs)(i.li,{children:["If multiple Sources having active Players then the scrobble Client will default to reporting the track based on Source ",(0,n.jsx)(i.strong,{children:"configuration"})," name, alphabetically","\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:["This is the ",(0,n.jsx)(i.code,{children:"name"})," property set in ",(0,n.jsx)(i.a,{href:"/configuration?configType=file#configuration-types",children:"File or AIO source configs"})]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,n.jsxs)(i.p,{children:["Clients can customize the Now Playing behavior individually using ",(0,n.jsx)(i.a,{href:"/configuration?configType=file#configuration-types",children:"File or AIO source configs:"})]}),"\n",(0,n.jsx)(i.pre,{children:(0,n.jsx)(i.code,{className:"language-json5",metastring:'title="CONFIG_DIR/lastfm.json"',children:'[\n {\n "name": "myLastFmClient",\n "configureAs": "client",\n "data": {\n // ...\n },\n "options": {\n // disable or enable\n //"nowPlaying": true\n //\n // OR define a list of Source *config* names that should be allowed to report Now Playing\n // order of the list determines priority for reporting Now Playing\n //"nowPlaying": ["mySpotify1","myJellyfin2"]\n }\n },\n]\n'})})]})}function h(e={}){let{wrapper:i}={...(0,l.R)(),...e.components};return i?(0,n.jsx)(i,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}}}]); \ No newline at end of file diff --git a/assets/js/a16703cd.d559f1ae.js b/assets/js/a16703cd.d559f1ae.js deleted file mode 100644 index 36d33014..00000000 --- a/assets/js/a16703cd.d559f1ae.js +++ /dev/null @@ -1,13 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["5515"],{7265(e,t,s){s.d(t,{Ay:()=>c,RM:()=>o});var i=s(4848),n=s(8453),l=s(2170),r=s.n(l);let o=[];function a(e){let t={a:"a",code:"code",li:"li",p:"p",strong:"strong",ul:"ul",...(0,n.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(t.p,{children:["A ",(0,i.jsx)(t.strong,{children:"Source"})," that monitors active listening uses one of two metrics to determine if a Song is scrobbable based on your listening:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"percent"})," of track listened to (50% or more)"]}),"\n",(0,i.jsxs)(t.li,{children:["or amount of time (",(0,i.jsx)(t.strong,{children:"duration"}),") track was listened to (4 minutes or more)"]}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["These default values are based on ",(0,i.jsx)(t.a,{href:"https://www.last.fm/api/scrobbling#when-is-a-scrobble-a-scrobble",children:"last.fm's scrobble guidance."})]}),"\n",(0,i.jsxs)(t.p,{children:["These values can be customized in the ",(0,i.jsx)(t.code,{children:"options"})," of Source's ",(0,i.jsx)(t.a,{href:"/configuration#configuration-types",children:"file or aio config"}),":"]}),"\n",(0,i.jsx)(r(),{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:t}={...(0,n.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(a,{...e})}):a(e)}},662(e,t,s){s.r(t),s.d(t,{metadata:()=>i,default:()=>h,frontMatter:()=>r,contentTitle:()=>o,toc:()=>c,assets:()=>a});var i=JSON.parse('{"id":"configuration/clients/clients","title":"Overview","description":"A Client is an application that stores the historical information about what songs you have played (scrobbles). Examples are Maloja, Last.fm, Listenbrainz...","source":"@site/docs/configuration/clients/clients.mdx","sourceDirName":"configuration/clients","slug":"/configuration/clients/","permalink":"/multi-scrobbler/configuration/clients/","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/clients/clients.mdx","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"sidebar_position":2,"title":"Overview","toc_max_heading_level":4},"sidebar":"tutorialSidebar","previous":{"title":"Overview","permalink":"/multi-scrobbler/configuration/"},"next":{"title":"Discord","permalink":"/multi-scrobbler/configuration/clients/discord/"}}'),n=s(4848),l=s(8453);s(7265);let r={sidebar_position:2,title:"Overview",toc_max_heading_level:4},o,a={},c=[{value:"Clients",id:"clients",level:2},{value:"Features",id:"features",level:2},{value:"Duplicate Detection",id:"duplicate-detection",level:3},{value:"Dead Scrobbles",id:"dead-scrobbles",level:3},{value:"Now Playing",id:"now-playing",level:3}];function d(e){let t={a:"a",code:"code",em:"em",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,l.R)(),...e.components},{Details:s}=t;return s||function(e,t){throw Error("Expected "+(t?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)(t.p,{children:["A ",(0,n.jsx)(t.strong,{children:"Client"})," is an application that stores the historical information about what songs you have played (scrobbles). Examples are ",(0,n.jsx)(t.strong,{children:"Maloja, Last.fm, Listenbrainz"}),"..."]}),"\n",(0,n.jsx)(t.h2,{id:"clients",children:"Clients"}),"\n",(0,n.jsxs)(t.table,{children:[(0,n.jsx)(t.thead,{children:(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.th,{style:{textAlign:"left"},children:"Name"}),(0,n.jsx)(t.th,{style:{textAlign:"left"},children:"Now Playing"})]})}),(0,n.jsxs)(t.tbody,{children:[(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{style:{textAlign:"left"},children:(0,n.jsx)(t.a,{href:"/configuration/clients/discord",children:"Discord"})}),(0,n.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{style:{textAlign:"left"},children:(0,n.jsx)(t.a,{href:"/configuration/clients/lastfm",children:"Last.fm"})}),(0,n.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{style:{textAlign:"left"},children:(0,n.jsx)(t.a,{href:"/configuration/clients/librefm",children:"Libre.fm"})}),(0,n.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{style:{textAlign:"left"},children:(0,n.jsx)(t.a,{href:"/configuration/clients/listenbrainz",children:"Listenbrainz"})}),(0,n.jsx)(t.td,{style:{textAlign:"left"},children:"\u2705"})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{style:{textAlign:"left"},children:(0,n.jsx)(t.a,{href:"/configuration/clients/koito",children:"Koito"})}),(0,n.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{style:{textAlign:"left"},children:(0,n.jsx)(t.a,{href:"/configuration/clients/maloja",children:"Maloja"})}),(0,n.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{style:{textAlign:"left"},children:(0,n.jsx)(t.a,{href:"/configuration/clients/rocksky",children:"Rocksky"})}),(0,n.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{style:{textAlign:"left"},children:(0,n.jsx)(t.a,{href:"/configuration/clients/tealfm",children:"Teal.fm"})}),(0,n.jsx)(t.td,{style:{textAlign:"left"},children:"\u274C"})]})]})]}),"\n",(0,n.jsx)(t.h2,{id:"features",children:"Features"}),"\n",(0,n.jsx)(t.h3,{id:"duplicate-detection",children:"Duplicate Detection"}),"\n",(0,n.jsx)(t.p,{children:"Before a Play is scrobbled to a client MS checks existing scrobbles from the Client's API to see if the Play has already been scrobbled."}),"\n",(0,n.jsx)(t.p,{children:"For each Play, MS fetches (cached) scrobbles from the Client in a time range inclusive of the Play's listening timestamp and then scores existing scrobbles against the Play based on:"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:"Similarity of Title, Artists, and Album"}),"\n",(0,n.jsx)(t.li,{children:"Temporal closeness of the Play's timestamp to the existing scrobble's timestamp"}),"\n",(0,n.jsx)(t.li,{children:"Whether MS detected the Play as a repeat (applicable to Sources that report realtime Player data)"}),"\n"]}),"\n",(0,n.jsxs)(t.p,{children:["Detailed scoring breakdowns against each existing scrobble are logged at the ",(0,n.jsx)(t.code,{children:"TRACE"})," level."]}),"\n",(0,n.jsxs)(s,{children:[(0,n.jsx)("summary",{children:"Detailed Explanation"}),(0,n.jsxs)(t.p,{children:["A ",(0,n.jsx)(t.strong,{children:"candidate"})," (to be scrobbled) Play is first transformed using the configured ",(0,n.jsxs)(t.a,{href:"/configuration/transforms/#hook",children:[(0,n.jsx)(t.code,{children:"compare.candidate"})," Hook"]}),", if any exists."]}),(0,n.jsxs)(t.p,{children:["Next, MS checks (up to) the last 100 scrobbles ",(0,n.jsx)(t.em,{children:"in-memory"})," scrobbles that it has made. These are ",(0,n.jsx)(t.em,{children:"not"})," from the Client but the actual scrobbles MS made while it has been running. The data from these scrobbles is much richer than what is usually parsed from the Client which makes it easier to detect duplicates from."]}),(0,n.jsx)(t.p,{children:"If no in-memory scrobble matches then MS starts comparing the candidate against historical scrobbles fetched from an inclusive time range of the candidate's timestamp."}),(0,n.jsx)("h3",{children:"Matching Title/Artist/Album"}),(0,n.jsxs)(t.p,{children:["For these string-based values MS uses an ",(0,n.jsx)(t.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/0dfa4c7aad6df98e13aee6d395827665c2414adb/src/backend/utils/StringUtils.ts#L299",children:"token-order-invariant"})," method that scores string similarity based on a (token count) weighted average of two ",(0,n.jsx)(t.a,{href:"https://foxxmd.github.io/string-sameness/#md:strategies",children:"similarity"})," algorithms, ",(0,n.jsx)(t.a,{href:"https://en.wikipedia.org/wiki/Levenshtein_distance",children:"Levenshtien Distance"})," and ",(0,n.jsx)(t.a,{href:"https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient",children:"Dice's Coefficient"}),". The weighted average ensures that very long strings (Track titles) are scored with a confidence proportional to their length."]}),(0,n.jsx)("h3",{children:"Matching Timestamp"}),(0,n.jsx)(t.p,{children:"Timestamps are scored on how temporally close they are. There are four possible scores with decreasing value:"}),(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsxs)(t.li,{children:[(0,n.jsx)(t.strong,{children:"Exact"})," - Timestamps are within 1 second of each other"]}),"\n",(0,n.jsxs)(t.li,{children:[(0,n.jsx)(t.strong,{children:"Close"})," - Timestamps are within ",(0,n.jsx)(t.code,{children:"threshold"})," seconds of each other","\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsxs)(t.li,{children:["This is determined by the smallest update interval of Source","\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsxs)(t.li,{children:['Some Sources (subsonic) only update every 60 seconds so this is the smallest "close" value possible. Most are 10 seconds. Signified by ',(0,n.jsx)(t.code,{children:"(Needed <10s)"})," in the breakdown example below."]}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,n.jsxs)(t.li,{children:[(0,n.jsx)(t.strong,{children:"Fuzzy"})," - One timestamp is within ",(0,n.jsx)(t.code,{children:"threshold"})," seconds of the ",(0,n.jsx)(t.em,{children:"end"})," of the other timestamp","\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsxs)(t.li,{children:["Sources can set the scrobble timestamp at different times. Some do it when the track is started listening to, some when it ends, some when the ",(0,n.jsx)(t.em,{children:"player"})," stops."]}),"\n",(0,n.jsxs)(t.li,{children:["Where possible, MS knows and keeps track of when this timestamp ",(0,n.jsx)(t.em,{children:"should"})," be, for each Source. If it's not possible then Fuzzy may be allowed."]}),"\n"]}),"\n"]}),"\n",(0,n.jsxs)(t.li,{children:[(0,n.jsx)(t.strong,{children:"None"})," - There is no correlation between timestamps"]}),"\n"]}),(0,n.jsx)("h3",{children:"Scoring and Breakdows"}),(0,n.jsx)(t.p,{children:"A candidate Play must score >= 1 to be detected as a duplicate of an existing scrobble."}),(0,n.jsxs)(t.p,{children:["Each score and a breakdown of the scores for its individual components can be see at the ",(0,n.jsx)(t.code,{children:"TRACE"})," logging level or in the ",(0,n.jsx)(t.a,{href:"/help#copy-play-debug-data",children:"debug data"})," for a scrobble. An example:"]}),(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{children:"* Artist: 0.06 * 0.3 = 0.02\n* Title: 0.04 * 0.4 = 0.02\n* Time: (Exact) 1 * 0.5 = 0.50\n * Existing: 19:13:33-04:00 - Candidate: 19:13:33-04:00\n * Temporal Sameness: Exact\n * Play Diff: 0s (Needed <10s)\n * Range Comparison N/A\nScore 0.54 => No Match\n"})}),(0,n.jsx)(t.p,{children:"In each component equation, the first number is the similarity (or temporal closeness)."}),(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:"0 = no correlation"}),"\n",(0,n.jsx)(t.li,{children:"1 = exactly the same"}),"\n"]}),(0,n.jsxs)(t.p,{children:["The second number is the ",(0,n.jsx)(t.em,{children:"weight"})," of that component in the final score."]})]}),"\n",(0,n.jsx)(t.h3,{id:"dead-scrobbles",children:"Dead Scrobbles"}),"\n",(0,n.jsx)(t.p,{children:"If multi-scrobbler is unable to submit a scrobble to a Client then it places the scrobble into a queue which is retried every 5 minutes for a number of times before it gives up."}),"\n",(0,n.jsxs)(t.p,{children:["After it stops ",(0,n.jsx)(t.em,{children:"automatically"})," retrying the scrobble still exists and can be retried from the dashboard. Dead scrobbles persist between restart so you will never lose a scrobble that hasn't been successfully submitted yet."]}),"\n",(0,n.jsx)(t.h3,{id:"now-playing",children:"Now Playing"}),"\n",(0,n.jsxs)(t.p,{children:["Multi-scrobbler can report the currently ",(0,n.jsx)(t.strong,{children:"playing"})," tracks it is monitoring to some Scrobble Clients via their individual ",(0,n.jsx)(t.strong,{children:"Now Playing"})," functionality."]}),"\n",(0,n.jsxs)(t.p,{children:["Now Playing is ",(0,n.jsx)(t.strong,{children:"default enabled"})," for all Clients that support it."]}),"\n",(0,n.jsx)(t.p,{children:"The behavior multi-scrobbler uses for determining Now Playing reporting:"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsxs)(t.li,{children:["Now Playing can be explicitly enabled or disabled globally using ENV ",(0,n.jsx)(t.code,{children:"NOW_PLAYING=true"})," or ",(0,n.jsx)(t.code,{children:"NOW_PLAYING=false"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:"This only affects Clients that don't have behavior set via File/AIO (below)"}),"\n"]}),"\n"]}),"\n",(0,n.jsx)(t.li,{children:"Now Playing will be only be reported to the same Clients a Source is configured to scrobble to"}),"\n",(0,n.jsxs)(t.li,{children:["A Source's Player status must be ",(0,n.jsx)(t.strong,{children:"Playing"}),", IE ",(0,n.jsx)(t.strong,{children:"active"}),", for reporting to occur","\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:"This status can be verified via Player UI on the Dashboard"}),"\n"]}),"\n"]}),"\n",(0,n.jsxs)(t.li,{children:["If multiple Sources having active Players then the scrobble Client will default to reporting the track based on Source ",(0,n.jsx)(t.strong,{children:"configuration"})," name, alphabetically","\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsxs)(t.li,{children:["This is the ",(0,n.jsx)(t.code,{children:"name"})," property set in ",(0,n.jsx)(t.a,{href:"/configuration?configType=file#configuration-types",children:"File or AIO source configs"})]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,n.jsxs)(t.p,{children:["Clients can customize the Now Playing behavior individually using ",(0,n.jsx)(t.a,{href:"/configuration?configType=file#configuration-types",children:"File or AIO source configs:"})]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-json5",metastring:'title="CONFIG_DIR/lastfm.json"',children:'[\n {\n "name": "myLastFmClient",\n "configureAs": "client",\n "data": {\n // ...\n },\n "options": {\n // disable or enable\n //"nowPlaying": true\n //\n // OR define a list of Source *config* names that should be allowed to report Now Playing\n // order of the list determines priority for reporting Now Playing\n //"nowPlaying": ["mySpotify1","myJellyfin2"]\n }\n },\n]\n'})})]})}function h(e={}){let{wrapper:t}={...(0,l.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}}}]); \ No newline at end of file diff --git a/assets/js/ad895e75.295bae88.js b/assets/js/ad895e75.295bae88.js deleted file mode 100644 index 12fac907..00000000 --- a/assets/js/ad895e75.295bae88.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["8160"],{1943(e,n,i){i.r(n),i.d(n,{metadata:()=>s,default:()=>d,frontMatter:()=>r,contentTitle:()=>l,toc:()=>a,assets:()=>c});var s=JSON.parse('{"id":"FAQ","title":"FAQ","description":"Connection Issues","source":"@site/docs/FAQ.md","sourceDirName":".","slug":"/FAQ","permalink":"/multi-scrobbler/FAQ","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/FAQ.md","tags":[],"version":"current","frontMatter":{"toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Flatpak","permalink":"/multi-scrobbler/development/flatpak"},"next":{"title":"Help","permalink":"/multi-scrobbler/help"}}'),t=i(4848),o=i(8453);let r={toc_min_heading_level:2,toc_max_heading_level:5},l,c={},a=[{value:"Connection Issues",id:"connection-issues",level:2},{value:"Webscrobbler/Listenbrainz Endpoint doesn't connect",id:"ingress-connection",level:3},{value:"Troubleshooting",id:"troubleshooting",level:4},{value:"Turn on Debug Logging",id:"turn-on-debug-logging",level:5},{value:"Check Host name and URL",id:"check-host-name-and-url",level:5},{value:"Check Firewall and Port Forwarding",id:"check-firewall-and-port-forwarding",level:5},{value:"Check Source Service Logs",id:"check-source-service-logs",level:5},{value:"Webscrobbler",id:"webscrobbler",level:6},{value:"Spotify/Deezer/LastFM won't authenticate",id:"spotifydeezerlastfm-wont-authenticate",level:3},{value:"I get errors with ECONNRESET ETIMEDOUT ENOTFOUND EAI_AGAIN",id:"i-get-errors-with-econnreset-etimedout-enotfound-eai_again",level:3},{value:"Youtube Music fails after some time",id:"youtube-music-fails-after-some-time",level:3},{value:"When using OAuth Client Authentication",id:"when-using-oauth-client-authentication",level:4},{value:"When using Cookies Authentication",id:"when-using-cookies-authentication",level:4},{value:"Configuration Issues",id:"configuration-issues",level:2},{value:"Config could not be parsed",id:"config-could-not-be-parsed",level:3},{value:"Scrobbling Issues",id:"scrobbling-issues",level:2},{value:"Last.fm does not scrobble tracks with multiple artists correctly",id:"lastfm-does-not-scrobble-tracks-with-multiple-artists-correctly",level:3},{value:"Google Cast track information is missing/incorrect or MS player has weird times",id:"google-cast-track-information-is-missingincorrect-or-ms-player-has-weird-times",level:3},{value:"Google Cast device does not track media",id:"google-cast-device-does-not-track-media",level:3},{value:"VLC is not scrobbling fields correctly",id:"vlc-is-not-scrobbling-fields-correctly",level:3},{value:"Youtube Music misses or duplicates scrobbles",id:"youtube-music-misses-or-duplicates-scrobbles",level:3},{value:"Reporting YTM scrobble issues",id:"reporting-ytm-scrobble-issues",level:4},{value:"Turn on Change Detection",id:"turn-on-change-detection",level:5},{value:"Turn on File Logging",id:"turn-on-file-logging",level:5},{value:"Provide Detail and Context",id:"provide-detail-and-context",level:5}];function h(e){let n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",h5:"h5",h6:"h6",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,o.R)(),...e.components},{Details:i}=n;return i||function(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h2,{id:"connection-issues",children:"Connection Issues"}),"\n",(0,t.jsx)(n.h3,{id:"ingress-connection",children:"Webscrobbler/Listenbrainz Endpoint doesn't connect"}),"\n",(0,t.jsxs)(n.p,{children:["These sources are ",(0,t.jsx)(n.strong,{children:"ingress-based"})," which means that multi-scrobbler waits for the Webscrobbler extension or app using Listenbrainz Endpoint to contact multi-scrobbler, as opposed to multi-scrobbler contacting them."]}),"\n",(0,t.jsx)(n.p,{children:"multi-scrobbler will log information about any server that connects to it for these services. In the logs it looks something like this:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"[2025-07-10 12:32:21.417 -0400] INFO : [App] [API] [Ingress] [WebScrobbler] [Ingress] [WebScrobbler] Received request from a new remote address: ::ffff:127.0.0.1 (192.168.0.100) (UA: Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0)\n[2025-07-10 12:32:21.418 -0400] INFO : [App] [API] [Ingress] [WebScrobbler] [Ingress] [WebScrobbler] ::ffff:127.0.0.1 (192.168.0.100) (UA: Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0) Received data for API URL slug '(none)' for the first time.\n"})}),"\n",(0,t.jsx)(n.p,{children:"It also logs if a server tries to connect to a URL that it does not recognize:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"2023-02-22T11:16:12-05:00 debug : [App ] Server received POST request from ::ffff:192.168.0.100 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59) to unknown route: /plkex\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"So, if you do not see either of these in your logs then the Source is not able to connect to your multi-scrobbler instance at all."})}),"\n",(0,t.jsx)(n.p,{children:"This is not something multi-scrobbler can fix and means you have an issue in your network."}),"\n",(0,t.jsx)(n.h4,{id:"troubleshooting",children:"Troubleshooting"}),"\n",(0,t.jsx)(n.p,{children:"Check or try all these steps before submitting an issue:"}),"\n",(0,t.jsx)(n.h5,{id:"turn-on-debug-logging",children:"Turn on Debug Logging"}),"\n",(0,t.jsxs)(n.p,{children:["First, turn on ",(0,t.jsx)(n.strong,{children:"debug"})," logging for multi-scrobbler by setting the environmental variable ",(0,t.jsx)(n.code,{children:"DEBUG_MODE=true"}),". Then, check the output for any additional information."]}),"\n",(0,t.jsx)(n.h5,{id:"check-host-name-and-url",children:"Check Host name and URL"}),"\n",(0,t.jsxs)(n.p,{children:["The URLs examples in the ",(0,t.jsx)(n.a,{href:"/configuration#base-url",children:"configuration"})," documentation assume you are running Webscrobbler/LFM Endpoint on the same server as multi-scrobbler. If these are not the same machine then you need to determine the IP address or domain name that multi-scrobbler is reachable at and use that instead of ",(0,t.jsx)(n.code,{children:"localhost"})," when configuring these sources. ",(0,t.jsx)(n.strong,{children:"This is likely the same host name that you would use to access the web interface for multi-scrobbler."})]}),"\n",(0,t.jsxs)(n.p,{children:["EX ",(0,t.jsx)(n.code,{children:"http://localhost:9078/api/webscrobbler"})," -> ",(0,t.jsx)(n.code,{children:"http://192.168.0.140:9078/api/webscrobbler"})]}),"\n",(0,t.jsx)(n.h5,{id:"check-firewall-and-port-forwarding",children:"Check Firewall and Port Forwarding"}),"\n",(0,t.jsxs)(n.p,{children:["If the machine multi-scrobbler is running on has a firewall ensure that port ",(0,t.jsx)(n.strong,{children:"9078"})," is open. Or if it is in another network entirely make sure your router is forwarding this port and it is open to the correct machine."]}),"\n",(0,t.jsx)(n.h5,{id:"check-source-service-logs",children:"Check Source Service Logs"}),"\n",(0,t.jsx)(n.h6,{id:"webscrobbler",children:"Webscrobbler"}),"\n",(0,t.jsxs)(n.p,{children:["See ",(0,t.jsx)(n.a,{href:"https://github.com/web-scrobbler/web-scrobbler/wiki/Debug-the-extension",children:"Debugging the extension"})," to get logs which should have information about failed requests."]}),"\n",(0,t.jsx)(n.h3,{id:"spotifydeezerlastfm-wont-authenticate",children:"Spotify/Deezer/LastFM won't authenticate"}),"\n",(0,t.jsxs)(n.p,{children:["Ensure any ",(0,t.jsx)(n.strong,{children:"client id"})," or ",(0,t.jsx)(n.strong,{children:"secrets"})," are correct in your configuration."]}),"\n",(0,t.jsx)(n.p,{children:"The callback/redirect URL for these services must be:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["the same address you would use to access the multi-scrobbler web interface","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"the web-interface must be accessible from the browser you are completing authentication from."}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["If multi-scrobbler is not running on the same machine your browser is on then the default/example addresses (",(0,t.jsx)(n.code,{children:"http://localhost..."}),") ",(0,t.jsx)(n.strong,{children:"will not work."})," You must determine the address you can reach the web interface at (such as ",(0,t.jsx)(n.code,{children:"http://192.168.0.140:9078"}),") then use that in place of ",(0,t.jsx)(n.code,{children:"localhost"})," in the callback URLs."]}),"\n",(0,t.jsxs)(n.p,{children:["EX ",(0,t.jsx)(n.code,{children:"http://localhost:9078/lastfm/callback"})," -> ",(0,t.jsx)(n.code,{children:"http://192.168.0.220:9078/lastfm/callback"})]}),"\n",(0,t.jsxs)(n.h3,{id:"i-get-errors-with-econnreset-etimedout-enotfound-eai_again",children:["I get errors with ",(0,t.jsx)(n.code,{children:"ECONNRESET"})," ",(0,t.jsx)(n.code,{children:"ETIMEDOUT"})," ",(0,t.jsx)(n.code,{children:"ENOTFOUND"})," ",(0,t.jsx)(n.code,{children:"EAI_AGAIN"})]}),"\n",(0,t.jsx)(n.p,{children:"These are errors indicating a network issue is occuring between you and the Source/Client."}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"ECONNRESET"})," means the upstream service unexpectedly closed the connection"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"ETIMEDOUT"})," means the upstream service did not respond to a request MS"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"ENOTFOUND"})," means that MS could not find a DNS record for the domain of the upstream service"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"EAI_AGAIN"})," means that a DNS server did not respond to MS's request to lookup the record for a domain"]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"ECONNRESET"})," and timeout issues can occur when you have a poor connection or if the upstream service is overloaded or is generally not behaving well. This is a common occurrence for Listenbrainz/Musicbrainz related requests."]}),"\n",(0,t.jsxs)(n.p,{children:["Please check the ",(0,t.jsx)(n.a,{href:"https://status.multi-scrobbler.app",children:"Upstream Services Status Page"})," to independently verify if the service is misbehaving. If the service is up and has good uptime then the issue may be in your network."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"ENOTFOUND"})," and ",(0,t.jsx)(n.code,{children:"EAI_AGAIN"})," and generally caused by DNS caching (stale records) or misbehaving DNS servers. Though this can happen at the upstream service's end, it is ",(0,t.jsx)(n.em,{children:"usually"})," an issue in your network. If you run a Pihole, AdGaurd, or other DNS solution try flushing the cache for the mishaving domain and check that your DNS application is working correctly."]}),"\n",(0,t.jsx)(n.h3,{id:"youtube-music-fails-after-some-time",children:"Youtube Music fails after some time"}),"\n",(0,t.jsxs)(n.p,{children:["If you see errors in multi-scrobbler for YTM that contain ",(0,t.jsx)(n.strong,{children:"401"})," or ",(0,t.jsx)(n.strong,{children:"403"})," like"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"Error: Could not send the specified request to browse. Status code: 401\n"})}),"\n",(0,t.jsx)(n.p,{children:"then YTM has invalidated your authentication."}),"\n",(0,t.jsxs)(n.p,{children:["First, ensure you are NOT using ",(0,t.jsx)(n.a,{href:"/configuration/sources/youtube-music?ytmAuth=ytt#ytm-auth",children:"YoutubeTV authentication."}),' If you completed authentication by entering a "User Code" you are using YoutubeTV which has stopped working. You should reauthenticate using ',(0,t.jsx)(n.strong,{children:"Cookies"})," or ",(0,t.jsx)(n.strong,{children:"Custom OAuth."})]}),"\n",(0,t.jsx)(n.h4,{id:"when-using-oauth-client-authentication",children:"When using OAuth Client Authentication"}),"\n",(0,t.jsxs)(n.p,{children:["Refresh your authentication by using the ",(0,t.jsx)(n.strong,{children:"(Re)authenticate"})," link from MS's web dashboard."]}),"\n",(0,t.jsx)(n.h4,{id:"when-using-cookies-authentication",children:"When using Cookies Authentication"}),"\n",(0,t.jsxs)(n.p,{children:["The library MS uses relies on scraping the YTM site by using cookies from your actual browser to pretend it is a browser. It does its best to keep these up to date but since this is not an official way to access the service YTM may invalidate your access ",(0,t.jsx)(n.em,{children:"to the authenticated session"})," at any time. How this is triggered is unknown and not something multi-scrobbler can control. You can help limit the chance of your session being invalidated by ",(0,t.jsx)(n.a,{href:"https://github.com/LuanRT/YouTube.js/issues/803#issuecomment-2504032666",children:"getting the cookie from an Incognito/Private Session"})," and then immediately closing the browser afterwards."]}),"\n",(0,t.jsxs)(n.p,{children:["To re-authenticate MS ",(0,t.jsx)(n.a,{href:"/configuration/sources/youtube-music?ytmAuth=cookie#ytm-auth",children:"follow the YTM instructions to retrieve a new set of cookies for multi-scrobbler"})," and then restart MS to potentially resolve the problem."]}),"\n",(0,t.jsx)(n.h2,{id:"configuration-issues",children:"Configuration Issues"}),"\n",(0,t.jsx)(n.h3,{id:"config-could-not-be-parsed",children:"Config could not be parsed"}),"\n",(0,t.jsx)(n.p,{children:"If you see something like this in your logs:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"2023-02-19T10:05:42-06:00 warn : [App] App config file exists but could not be parsed!\n2023-02-19T10:05:42-06:00 error : [App] Exited with uncaught error\n2023-02-19T10:05:42-06:00 error : [App] Error: config.json could not be parsed\n"})}),"\n",(0,t.jsxs)(n.p,{children:["It means the JSON in your configuration file is not valid. Copy and paste your configuration into a site like ",(0,t.jsx)(n.a,{href:"https://jsonlint.com/",children:"JSONLint"})," to find out where errors you have and fix them."]}),"\n",(0,t.jsx)(n.h2,{id:"scrobbling-issues",children:"Scrobbling Issues"}),"\n",(0,t.jsx)(n.h3,{id:"lastfm-does-not-scrobble-tracks-with-multiple-artists-correctly",children:"Last.fm does not scrobble tracks with multiple artists correctly"}),"\n",(0,t.jsxs)(n.p,{children:["This is a limitation of the ",(0,t.jsx)(n.a,{href:"https://www.last.fm/api/show/track.scrobble",children:"Last.fm API"})," where the ",(0,t.jsx)(n.strong,{children:"artist"}),' field is only one string and Last.fm does not recognize (play well) with "combined" artists.']}),"\n",(0,t.jsxs)(n.p,{children:["Multi-scrobbler works the same was the official Spotify-Last.fm integration works -- it only scrobbles the ",(0,t.jsx)(n.strong,{children:"first"})," artist on a multi-artist track."]}),"\n",(0,t.jsx)(n.h3,{id:"google-cast-track-information-is-missingincorrect-or-ms-player-has-weird-times",children:"Google Cast track information is missing/incorrect or MS player has weird times"}),"\n",(0,t.jsxs)(n.p,{children:["The Google Cast integration relies on a few common fields in the data it receives from your casting device. Every platform that can cast (Spotify, Pandora, etc...) ",(0,t.jsx)(n.em,{children:"should"})," use these fields the same but there are slight differences between their implementations that may confuse multi-scrobbler. Specific platforms may also return more information in non-common fields that are undocumented."]}),"\n",(0,t.jsxs)(n.p,{children:["To diagnose these issues you ",(0,t.jsx)(n.a,{href:"/configuration/sources/google-cast#cast-troubleshooting",children:(0,t.jsx)(n.strong,{children:"must enable payload logging"})})," for your google cast Source, run MS, and then include logs with this output from that run. Without the raw data logged from your cast device it will be nearly impossible to resolve your issue."]}),"\n",(0,t.jsx)(n.h3,{id:"google-cast-device-does-not-track-media",children:"Google Cast device does not track media"}),"\n",(0,t.jsxs)(n.p,{children:["It is likely the app playing on the cast device is incorrectly reporting the media type as ",(0,t.jsx)(n.strong,{children:"not music"}),"."]}),"\n",(0,t.jsx)(n.p,{children:"MS logs will tell you what type the media is reported as with lines like:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"My Artist - Example Track has 'unknown' media type and allowUnknownMedia=false, will not track\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Refer to ",(0,t.jsx)(n.a,{href:"/configuration/sources/google-cast#allow-unknown-media-type",children:"Allow Unknown Media Type"})," section to fix this"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"My Artist - Example Track has 'movie' media type so will not track\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Refer to ",(0,t.jsx)(n.a,{href:"/configuration/sources/google-cast#forcing-media-tracking",children:"Force Media Tracking"})," section to fix this"]}),"\n",(0,t.jsx)(n.h3,{id:"vlc-is-not-scrobbling-fields-correctly",children:"VLC is not scrobbling fields correctly"}),"\n",(0,t.jsxs)(n.p,{children:["Before reporting an issue turn on metadata logging in the MS VLC configuration, ",(0,t.jsx)(n.a,{href:"/configuration/sources/vlc#vlc-information-reporting",children:"see the VLC documentation."})]}),"\n",(0,t.jsx)(n.h3,{id:"youtube-music-misses-or-duplicates-scrobbles",children:"Youtube Music misses or duplicates scrobbles"}),"\n",(0,t.jsxs)(i,{children:[(0,t.jsx)("summary",{children:"How MS detects YTM history"}),(0,t.jsxs)(n.p,{children:["In order for multi-scrobbler to accurately determine if a song has been scrobbled it needs ",(0,t.jsx)(n.strong,{children:"a source of truth."}),' For YTM this is a "history" list scraped from the YTM website. Unfortunately, the data in this list can be (often) inconsistent which makes it hard for multi-scrobbler to "trust" that it is correct and determine when/if new scrobbles occur. This inconsistency is not something multi-scrobbler can control -- it is a side-effect of having to use an unofficial method to access YTM (scraping).']}),(0,t.jsx)(n.p,{children:'To compensate for this multi-scrobbler resets when it considers this list the "source of truth" based on if the list changes in an inconsistent way between consecutive checks. New scrobbles can only be detected when this list is "OK" as a source of truth for N+1 checks. Therefore, any new tracks that appear when the list is inconsistent will be ignored.'}),(0,t.jsx)(n.p,{children:'Duplicate scrobbles can also occur if the change between two checks is technically consistent. For instance, if you listen to a track twice in some period, separated by other music, YTM will sometimes "remove" the track from the earlier time (further down in your history) and "re-add" it at the top of the history.'})]}),"\n",(0,t.jsx)(n.h4,{id:"reporting-ytm-scrobble-issues",children:"Reporting YTM scrobble issues"}),"\n",(0,t.jsxs)(n.p,{children:["If you experience scrobble detection issues you can help improve MS's YTM heureistic by providing thorough feedback as ",(0,t.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/issues/new?assignees=&labels=bug&projects=&template=01-bug-report.yml&title=bug%3A+",children:"an issue."})," ",(0,t.jsx)(n.strong,{children:"Please do the following to provide the most useful report:"})]}),"\n",(0,t.jsx)(n.h5,{id:"turn-on-change-detection",children:"Turn on Change Detection"}),"\n",(0,t.jsxs)(n.p,{children:["In your YTM configuration (",(0,t.jsx)(n.code,{children:"ytmusic.json"}),") add ",(0,t.jsx)(n.code,{children:"logDiff"})," under ",(0,t.jsx)(n.code,{children:"options"})," like this:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "type": "ytmusic",\n "name": "MyYTM",\n "data": { ... },\n "options": {\n "logDiff": true\n }\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"or set either ENVs:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"YTM_LOG_DIFF=true"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"/configuration#debug-mode",children:(0,t.jsx)(n.code,{children:"DEBUG_MODE=true"})})}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"This will cause MS to log YTM history changes similar to this:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"[Ytmusic - MyYTM] Changes from last seen list:\n1. (tuhe1CpHRxY) KNOWER - I\u2019m The President --- undefined => Moved - Originally at 6\n2. (Mtg8V6Xa2nc) Vulfpeck - Romanian Drinking Song --- Schvitz => Moved - Originally at 1\n3. (rxbCaiyYSXM) Nightmares On Wax - You Wish --- In A Space Outta Sound => Moved - Originally at 2\n4. (tMt_YXr90AM) Gorillaz - O Green World --- undefined => Moved - Originally at 3\n...\n"})}),"\n",(0,t.jsx)(n.p,{children:"Which are essential to troubleshooting this behavior."}),"\n",(0,t.jsx)(n.h5,{id:"turn-on-file-logging",children:"Turn on File Logging"}),"\n",(0,t.jsxs)(n.p,{children:["Optionally, if your logs are busy due to many other sources, you can log ",(0,t.jsx)(n.strong,{children:"only"})," YTM logs to a file. Turn this on with the ",(0,t.jsx)(n.code,{children:"logToFile"})," config option:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "type": "ytmusic",\n "name": "MyYTM",\n "data": { ... },\n "options": {\n "logDiff": true,\n "logToFile": true\n }\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["This will cause MS to write YTM logs to a file in your ",(0,t.jsx)(n.code,{children:"logs"})," folder named ",(0,t.jsx)(n.code,{children:"ytmusic-MyYTM.log"})," (based on your source name)."]}),"\n",(0,t.jsx)(n.h5,{id:"provide-detail-and-context",children:"Provide Detail and Context"}),"\n",(0,t.jsx)(n.p,{children:"Provide a detailed account of how you were using YTM when the issue occurred, including things like:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"the platform listening on (desktop, mobile, 3rd party client, etc...)"}),"\n",(0,t.jsxs)(n.li,{children:["any changes in platform","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"I switched from desktop to listening on my phone..."}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["how you were listening to music","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"I was playing an album start to finish"}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"I listened to two songs in a row, then browsed for a new song in library by artist, then went back to a song in the queue..."}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"Explain the expected behavior (it should have scrobbled songs x, y, then z) and what actually happened (it scrobbled songs x, then y, then x again, then z)"}),"\n",(0,t.jsx)(n.p,{children:"Provide ALL logs from the time when the issue occurred including logs from BEFORE (ideally 2-3 minutes of logs) and AFTER the issue."})]})}function d(e={}){let{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}}}]); \ No newline at end of file diff --git a/assets/js/ad895e75.6c922c3c.js b/assets/js/ad895e75.6c922c3c.js new file mode 100644 index 00000000..d50412eb --- /dev/null +++ b/assets/js/ad895e75.6c922c3c.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["8160"],{1943(e,n,i){i.r(n),i.d(n,{metadata:()=>s,default:()=>d,frontMatter:()=>r,contentTitle:()=>l,toc:()=>a,assets:()=>c});var s=JSON.parse('{"id":"FAQ","title":"FAQ","description":"Connection Issues","source":"@site/docs/FAQ.md","sourceDirName":".","slug":"/FAQ","permalink":"/multi-scrobbler/FAQ","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/FAQ.md","tags":[],"version":"current","frontMatter":{"toc_min_heading_level":2,"toc_max_heading_level":5,"title":"FAQ"},"sidebar":"tutorialSidebar","previous":{"title":"Flatpak","permalink":"/multi-scrobbler/development/flatpak"},"next":{"title":"Help","permalink":"/multi-scrobbler/help"}}'),t=i(4848),o=i(8453);let r={toc_min_heading_level:2,toc_max_heading_level:5,title:"FAQ"},l,c={},a=[{value:"Connection Issues",id:"connection-issues",level:2},{value:"Webscrobbler/Listenbrainz Endpoint doesn't connect",id:"ingress-connection",level:3},{value:"Troubleshooting",id:"troubleshooting",level:4},{value:"Turn on Debug Logging",id:"turn-on-debug-logging",level:5},{value:"Check Host name and URL",id:"check-host-name-and-url",level:5},{value:"Check Firewall and Port Forwarding",id:"check-firewall-and-port-forwarding",level:5},{value:"Check Source Service Logs",id:"check-source-service-logs",level:5},{value:"Webscrobbler",id:"webscrobbler",level:6},{value:"Spotify/Deezer/LastFM won't authenticate",id:"spotifydeezerlastfm-wont-authenticate",level:3},{value:"I get errors with ECONNRESET ETIMEDOUT ENOTFOUND EAI_AGAIN",id:"i-get-errors-with-econnreset-etimedout-enotfound-eai_again",level:3},{value:"Youtube Music fails after some time",id:"youtube-music-fails-after-some-time",level:3},{value:"When using OAuth Client Authentication",id:"when-using-oauth-client-authentication",level:4},{value:"When using Cookies Authentication",id:"when-using-cookies-authentication",level:4},{value:"Configuration Issues",id:"configuration-issues",level:2},{value:"Config could not be parsed",id:"config-could-not-be-parsed",level:3},{value:"Scrobbling Issues",id:"scrobbling-issues",level:2},{value:"Last.fm does not scrobble tracks with multiple artists correctly",id:"lastfm-does-not-scrobble-tracks-with-multiple-artists-correctly",level:3},{value:"Google Cast track information is missing/incorrect or MS player has weird times",id:"google-cast-track-information-is-missingincorrect-or-ms-player-has-weird-times",level:3},{value:"Google Cast device does not track media",id:"google-cast-device-does-not-track-media",level:3},{value:"VLC is not scrobbling fields correctly",id:"vlc-is-not-scrobbling-fields-correctly",level:3},{value:"Youtube Music misses or duplicates scrobbles",id:"youtube-music-misses-or-duplicates-scrobbles",level:3},{value:"Reporting YTM scrobble issues",id:"reporting-ytm-scrobble-issues",level:4},{value:"Turn on Change Detection",id:"turn-on-change-detection",level:5},{value:"Turn on File Logging",id:"turn-on-file-logging",level:5},{value:"Provide Detail and Context",id:"provide-detail-and-context",level:5}];function h(e){let n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",h5:"h5",h6:"h6",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,o.R)(),...e.components},{Details:i}=n;return i||function(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h2,{id:"connection-issues",children:"Connection Issues"}),"\n",(0,t.jsx)(n.h3,{id:"ingress-connection",children:"Webscrobbler/Listenbrainz Endpoint doesn't connect"}),"\n",(0,t.jsxs)(n.p,{children:["These sources are ",(0,t.jsx)(n.strong,{children:"ingress-based"})," which means that multi-scrobbler waits for the Webscrobbler extension or app using Listenbrainz Endpoint to contact multi-scrobbler, as opposed to multi-scrobbler contacting them."]}),"\n",(0,t.jsx)(n.p,{children:"multi-scrobbler will log information about any server that connects to it for these services. In the logs it looks something like this:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"[2025-07-10 12:32:21.417 -0400] INFO : [App] [API] [Ingress] [WebScrobbler] [Ingress] [WebScrobbler] Received request from a new remote address: ::ffff:127.0.0.1 (192.168.0.100) (UA: Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0)\n[2025-07-10 12:32:21.418 -0400] INFO : [App] [API] [Ingress] [WebScrobbler] [Ingress] [WebScrobbler] ::ffff:127.0.0.1 (192.168.0.100) (UA: Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0) Received data for API URL slug '(none)' for the first time.\n"})}),"\n",(0,t.jsx)(n.p,{children:"It also logs if a server tries to connect to a URL that it does not recognize:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"2023-02-22T11:16:12-05:00 debug : [App ] Server received POST request from ::ffff:192.168.0.100 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59) to unknown route: /plkex\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"So, if you do not see either of these in your logs then the Source is not able to connect to your multi-scrobbler instance at all."})}),"\n",(0,t.jsx)(n.p,{children:"This is not something multi-scrobbler can fix and means you have an issue in your network."}),"\n",(0,t.jsx)(n.h4,{id:"troubleshooting",children:"Troubleshooting"}),"\n",(0,t.jsx)(n.p,{children:"Check or try all these steps before submitting an issue:"}),"\n",(0,t.jsx)(n.h5,{id:"turn-on-debug-logging",children:"Turn on Debug Logging"}),"\n",(0,t.jsxs)(n.p,{children:["First, turn on ",(0,t.jsx)(n.strong,{children:"debug"})," logging for multi-scrobbler by setting the environmental variable ",(0,t.jsx)(n.code,{children:"DEBUG_MODE=true"}),". Then, check the output for any additional information."]}),"\n",(0,t.jsx)(n.h5,{id:"check-host-name-and-url",children:"Check Host name and URL"}),"\n",(0,t.jsxs)(n.p,{children:["The URLs examples in the ",(0,t.jsx)(n.a,{href:"/configuration#base-url",children:"configuration"})," documentation assume you are running Webscrobbler/LFM Endpoint on the same server as multi-scrobbler. If these are not the same machine then you need to determine the IP address or domain name that multi-scrobbler is reachable at and use that instead of ",(0,t.jsx)(n.code,{children:"localhost"})," when configuring these sources. ",(0,t.jsx)(n.strong,{children:"This is likely the same host name that you would use to access the web interface for multi-scrobbler."})]}),"\n",(0,t.jsxs)(n.p,{children:["EX ",(0,t.jsx)(n.code,{children:"http://localhost:9078/api/webscrobbler"})," -> ",(0,t.jsx)(n.code,{children:"http://192.168.0.140:9078/api/webscrobbler"})]}),"\n",(0,t.jsx)(n.h5,{id:"check-firewall-and-port-forwarding",children:"Check Firewall and Port Forwarding"}),"\n",(0,t.jsxs)(n.p,{children:["If the machine multi-scrobbler is running on has a firewall ensure that port ",(0,t.jsx)(n.strong,{children:"9078"})," is open. Or if it is in another network entirely make sure your router is forwarding this port and it is open to the correct machine."]}),"\n",(0,t.jsx)(n.h5,{id:"check-source-service-logs",children:"Check Source Service Logs"}),"\n",(0,t.jsx)(n.h6,{id:"webscrobbler",children:"Webscrobbler"}),"\n",(0,t.jsxs)(n.p,{children:["See ",(0,t.jsx)(n.a,{href:"https://github.com/web-scrobbler/web-scrobbler/wiki/Debug-the-extension",children:"Debugging the extension"})," to get logs which should have information about failed requests."]}),"\n",(0,t.jsx)(n.h3,{id:"spotifydeezerlastfm-wont-authenticate",children:"Spotify/Deezer/LastFM won't authenticate"}),"\n",(0,t.jsxs)(n.p,{children:["Ensure any ",(0,t.jsx)(n.strong,{children:"client id"})," or ",(0,t.jsx)(n.strong,{children:"secrets"})," are correct in your configuration."]}),"\n",(0,t.jsx)(n.p,{children:"The callback/redirect URL for these services must be:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["the same address you would use to access the multi-scrobbler web interface","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"the web-interface must be accessible from the browser you are completing authentication from."}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["If multi-scrobbler is not running on the same machine your browser is on then the default/example addresses (",(0,t.jsx)(n.code,{children:"http://localhost..."}),") ",(0,t.jsx)(n.strong,{children:"will not work."})," You must determine the address you can reach the web interface at (such as ",(0,t.jsx)(n.code,{children:"http://192.168.0.140:9078"}),") then use that in place of ",(0,t.jsx)(n.code,{children:"localhost"})," in the callback URLs."]}),"\n",(0,t.jsxs)(n.p,{children:["EX ",(0,t.jsx)(n.code,{children:"http://localhost:9078/lastfm/callback"})," -> ",(0,t.jsx)(n.code,{children:"http://192.168.0.220:9078/lastfm/callback"})]}),"\n",(0,t.jsxs)(n.h3,{id:"i-get-errors-with-econnreset-etimedout-enotfound-eai_again",children:["I get errors with ",(0,t.jsx)(n.code,{children:"ECONNRESET"})," ",(0,t.jsx)(n.code,{children:"ETIMEDOUT"})," ",(0,t.jsx)(n.code,{children:"ENOTFOUND"})," ",(0,t.jsx)(n.code,{children:"EAI_AGAIN"})]}),"\n",(0,t.jsx)(n.p,{children:"These are errors indicating a network issue is occuring between you and the Source/Client."}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"ECONNRESET"})," means the upstream service unexpectedly closed the connection"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"ETIMEDOUT"})," means the upstream service did not respond to a request MS"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"ENOTFOUND"})," means that MS could not find a DNS record for the domain of the upstream service"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"EAI_AGAIN"})," means that a DNS server did not respond to MS's request to lookup the record for a domain"]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"ECONNRESET"})," and timeout issues can occur when you have a poor connection or if the upstream service is overloaded or is generally not behaving well. This is a common occurrence for Listenbrainz/Musicbrainz related requests."]}),"\n",(0,t.jsxs)(n.p,{children:["Please check the ",(0,t.jsx)(n.a,{href:"https://status.multi-scrobbler.app",children:"Upstream Services Status Page"})," to independently verify if the service is misbehaving. If the service is up and has good uptime then the issue may be in your network."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"ENOTFOUND"})," and ",(0,t.jsx)(n.code,{children:"EAI_AGAIN"})," and generally caused by DNS caching (stale records) or misbehaving DNS servers. Though this can happen at the upstream service's end, it is ",(0,t.jsx)(n.em,{children:"usually"})," an issue in your network. If you run a Pihole, AdGaurd, or other DNS solution try flushing the cache for the mishaving domain and check that your DNS application is working correctly."]}),"\n",(0,t.jsx)(n.h3,{id:"youtube-music-fails-after-some-time",children:"Youtube Music fails after some time"}),"\n",(0,t.jsxs)(n.p,{children:["If you see errors in multi-scrobbler for YTM that contain ",(0,t.jsx)(n.strong,{children:"401"})," or ",(0,t.jsx)(n.strong,{children:"403"})," like"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"Error: Could not send the specified request to browse. Status code: 401\n"})}),"\n",(0,t.jsx)(n.p,{children:"then YTM has invalidated your authentication."}),"\n",(0,t.jsxs)(n.p,{children:["First, ensure you are NOT using ",(0,t.jsx)(n.a,{href:"/configuration/sources/youtube-music?ytmAuth=ytt#ytm-auth",children:"YoutubeTV authentication."}),' If you completed authentication by entering a "User Code" you are using YoutubeTV which has stopped working. You should reauthenticate using ',(0,t.jsx)(n.strong,{children:"Cookies"})," or ",(0,t.jsx)(n.strong,{children:"Custom OAuth."})]}),"\n",(0,t.jsx)(n.h4,{id:"when-using-oauth-client-authentication",children:"When using OAuth Client Authentication"}),"\n",(0,t.jsxs)(n.p,{children:["Refresh your authentication by using the ",(0,t.jsx)(n.strong,{children:"(Re)authenticate"})," link from MS's web dashboard."]}),"\n",(0,t.jsx)(n.h4,{id:"when-using-cookies-authentication",children:"When using Cookies Authentication"}),"\n",(0,t.jsxs)(n.p,{children:["The library MS uses relies on scraping the YTM site by using cookies from your actual browser to pretend it is a browser. It does its best to keep these up to date but since this is not an official way to access the service YTM may invalidate your access ",(0,t.jsx)(n.em,{children:"to the authenticated session"})," at any time. How this is triggered is unknown and not something multi-scrobbler can control. You can help limit the chance of your session being invalidated by ",(0,t.jsx)(n.a,{href:"https://github.com/LuanRT/YouTube.js/issues/803#issuecomment-2504032666",children:"getting the cookie from an Incognito/Private Session"})," and then immediately closing the browser afterwards."]}),"\n",(0,t.jsxs)(n.p,{children:["To re-authenticate MS ",(0,t.jsx)(n.a,{href:"/configuration/sources/youtube-music?ytmAuth=cookie#ytm-auth",children:"follow the YTM instructions to retrieve a new set of cookies for multi-scrobbler"})," and then restart MS to potentially resolve the problem."]}),"\n",(0,t.jsx)(n.h2,{id:"configuration-issues",children:"Configuration Issues"}),"\n",(0,t.jsx)(n.h3,{id:"config-could-not-be-parsed",children:"Config could not be parsed"}),"\n",(0,t.jsx)(n.p,{children:"If you see something like this in your logs:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"2023-02-19T10:05:42-06:00 warn : [App] App config file exists but could not be parsed!\n2023-02-19T10:05:42-06:00 error : [App] Exited with uncaught error\n2023-02-19T10:05:42-06:00 error : [App] Error: config.json could not be parsed\n"})}),"\n",(0,t.jsxs)(n.p,{children:["It means the JSON in your configuration file is not valid. Copy and paste your configuration into a site like ",(0,t.jsx)(n.a,{href:"https://jsonlint.com/",children:"JSONLint"})," to find out where errors you have and fix them."]}),"\n",(0,t.jsx)(n.h2,{id:"scrobbling-issues",children:"Scrobbling Issues"}),"\n",(0,t.jsx)(n.h3,{id:"lastfm-does-not-scrobble-tracks-with-multiple-artists-correctly",children:"Last.fm does not scrobble tracks with multiple artists correctly"}),"\n",(0,t.jsxs)(n.p,{children:["This is a limitation of the ",(0,t.jsx)(n.a,{href:"https://www.last.fm/api/show/track.scrobble",children:"Last.fm API"})," where the ",(0,t.jsx)(n.strong,{children:"artist"}),' field is only one string and Last.fm does not recognize (play well) with "combined" artists.']}),"\n",(0,t.jsxs)(n.p,{children:["Multi-scrobbler works the same was the official Spotify-Last.fm integration works -- it only scrobbles the ",(0,t.jsx)(n.strong,{children:"first"})," artist on a multi-artist track."]}),"\n",(0,t.jsx)(n.h3,{id:"google-cast-track-information-is-missingincorrect-or-ms-player-has-weird-times",children:"Google Cast track information is missing/incorrect or MS player has weird times"}),"\n",(0,t.jsxs)(n.p,{children:["The Google Cast integration relies on a few common fields in the data it receives from your casting device. Every platform that can cast (Spotify, Pandora, etc...) ",(0,t.jsx)(n.em,{children:"should"})," use these fields the same but there are slight differences between their implementations that may confuse multi-scrobbler. Specific platforms may also return more information in non-common fields that are undocumented."]}),"\n",(0,t.jsxs)(n.p,{children:["To diagnose these issues you ",(0,t.jsx)(n.a,{href:"/configuration/sources/google-cast#cast-troubleshooting",children:(0,t.jsx)(n.strong,{children:"must enable payload logging"})})," for your google cast Source, run MS, and then include logs with this output from that run. Without the raw data logged from your cast device it will be nearly impossible to resolve your issue."]}),"\n",(0,t.jsx)(n.h3,{id:"google-cast-device-does-not-track-media",children:"Google Cast device does not track media"}),"\n",(0,t.jsxs)(n.p,{children:["It is likely the app playing on the cast device is incorrectly reporting the media type as ",(0,t.jsx)(n.strong,{children:"not music"}),"."]}),"\n",(0,t.jsx)(n.p,{children:"MS logs will tell you what type the media is reported as with lines like:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"My Artist - Example Track has 'unknown' media type and allowUnknownMedia=false, will not track\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Refer to ",(0,t.jsx)(n.a,{href:"/configuration/sources/google-cast#allow-unknown-media-type",children:"Allow Unknown Media Type"})," section to fix this"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"My Artist - Example Track has 'movie' media type so will not track\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Refer to ",(0,t.jsx)(n.a,{href:"/configuration/sources/google-cast#forcing-media-tracking",children:"Force Media Tracking"})," section to fix this"]}),"\n",(0,t.jsx)(n.h3,{id:"vlc-is-not-scrobbling-fields-correctly",children:"VLC is not scrobbling fields correctly"}),"\n",(0,t.jsxs)(n.p,{children:["Before reporting an issue turn on metadata logging in the MS VLC configuration, ",(0,t.jsx)(n.a,{href:"/configuration/sources/vlc#vlc-information-reporting",children:"see the VLC documentation."})]}),"\n",(0,t.jsx)(n.h3,{id:"youtube-music-misses-or-duplicates-scrobbles",children:"Youtube Music misses or duplicates scrobbles"}),"\n",(0,t.jsxs)(i,{children:[(0,t.jsx)("summary",{children:"How MS detects YTM history"}),(0,t.jsxs)(n.p,{children:["In order for multi-scrobbler to accurately determine if a song has been scrobbled it needs ",(0,t.jsx)(n.strong,{children:"a source of truth."}),' For YTM this is a "history" list scraped from the YTM website. Unfortunately, the data in this list can be (often) inconsistent which makes it hard for multi-scrobbler to "trust" that it is correct and determine when/if new scrobbles occur. This inconsistency is not something multi-scrobbler can control -- it is a side-effect of having to use an unofficial method to access YTM (scraping).']}),(0,t.jsx)(n.p,{children:'To compensate for this multi-scrobbler resets when it considers this list the "source of truth" based on if the list changes in an inconsistent way between consecutive checks. New scrobbles can only be detected when this list is "OK" as a source of truth for N+1 checks. Therefore, any new tracks that appear when the list is inconsistent will be ignored.'}),(0,t.jsx)(n.p,{children:'Duplicate scrobbles can also occur if the change between two checks is technically consistent. For instance, if you listen to a track twice in some period, separated by other music, YTM will sometimes "remove" the track from the earlier time (further down in your history) and "re-add" it at the top of the history.'})]}),"\n",(0,t.jsx)(n.h4,{id:"reporting-ytm-scrobble-issues",children:"Reporting YTM scrobble issues"}),"\n",(0,t.jsxs)(n.p,{children:["If you experience scrobble detection issues you can help improve MS's YTM heureistic by providing thorough feedback as ",(0,t.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/issues/new?assignees=&labels=bug&projects=&template=01-bug-report.yml&title=bug%3A+",children:"an issue."})," ",(0,t.jsx)(n.strong,{children:"Please do the following to provide the most useful report:"})]}),"\n",(0,t.jsx)(n.h5,{id:"turn-on-change-detection",children:"Turn on Change Detection"}),"\n",(0,t.jsxs)(n.p,{children:["In your YTM configuration (",(0,t.jsx)(n.code,{children:"ytmusic.json"}),") add ",(0,t.jsx)(n.code,{children:"logDiff"})," under ",(0,t.jsx)(n.code,{children:"options"})," like this:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "type": "ytmusic",\n "name": "MyYTM",\n "data": { ... },\n "options": {\n "logDiff": true\n }\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"or set either ENVs:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"YTM_LOG_DIFF=true"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"/configuration#debug-mode",children:(0,t.jsx)(n.code,{children:"DEBUG_MODE=true"})})}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"This will cause MS to log YTM history changes similar to this:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"[Ytmusic - MyYTM] Changes from last seen list:\n1. (tuhe1CpHRxY) KNOWER - I\u2019m The President --- undefined => Moved - Originally at 6\n2. (Mtg8V6Xa2nc) Vulfpeck - Romanian Drinking Song --- Schvitz => Moved - Originally at 1\n3. (rxbCaiyYSXM) Nightmares On Wax - You Wish --- In A Space Outta Sound => Moved - Originally at 2\n4. (tMt_YXr90AM) Gorillaz - O Green World --- undefined => Moved - Originally at 3\n...\n"})}),"\n",(0,t.jsx)(n.p,{children:"Which are essential to troubleshooting this behavior."}),"\n",(0,t.jsx)(n.h5,{id:"turn-on-file-logging",children:"Turn on File Logging"}),"\n",(0,t.jsxs)(n.p,{children:["Optionally, if your logs are busy due to many other sources, you can log ",(0,t.jsx)(n.strong,{children:"only"})," YTM logs to a file. Turn this on with the ",(0,t.jsx)(n.code,{children:"logToFile"})," config option:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "type": "ytmusic",\n "name": "MyYTM",\n "data": { ... },\n "options": {\n "logDiff": true,\n "logToFile": true\n }\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["This will cause MS to write YTM logs to a file in your ",(0,t.jsx)(n.code,{children:"logs"})," folder named ",(0,t.jsx)(n.code,{children:"ytmusic-MyYTM.log"})," (based on your source name)."]}),"\n",(0,t.jsx)(n.h5,{id:"provide-detail-and-context",children:"Provide Detail and Context"}),"\n",(0,t.jsx)(n.p,{children:"Provide a detailed account of how you were using YTM when the issue occurred, including things like:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"the platform listening on (desktop, mobile, 3rd party client, etc...)"}),"\n",(0,t.jsxs)(n.li,{children:["any changes in platform","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"I switched from desktop to listening on my phone..."}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["how you were listening to music","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"I was playing an album start to finish"}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"I listened to two songs in a row, then browsed for a new song in library by artist, then went back to a song in the queue..."}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"Explain the expected behavior (it should have scrobbled songs x, y, then z) and what actually happened (it scrobbled songs x, then y, then x again, then z)"}),"\n",(0,t.jsx)(n.p,{children:"Provide ALL logs from the time when the issue occurred including logs from BEFORE (ideally 2-3 minutes of logs) and AFTER the issue."})]})}function d(e={}){let{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}}}]); \ No newline at end of file diff --git a/assets/js/af9480c3.0e9da9a7.js b/assets/js/af9480c3.0e9da9a7.js deleted file mode 100644 index d39a2afd..00000000 --- a/assets/js/af9480c3.0e9da9a7.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["232"],{6613(e,n,r){r.r(n),r.d(n,{metadata:()=>i,default:()=>h,frontMatter:()=>a,contentTitle:()=>c,toc:()=>d,assets:()=>l});var i=JSON.parse('{"id":"configuration/sources/listenbrainz-source","title":"Listenbrainz","description":"This Source monitors a Listenbrainz account\'s scrobble history and then re-scrobbles discovered tracks to configured clients.","source":"@site/docs/configuration/sources/listenbrainz-source.mdx","sourceDirName":"configuration/sources","slug":"/configuration/sources/listenbrainz-source","permalink":"/multi-scrobbler/configuration/sources/listenbrainz-source","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/sources/listenbrainz-source.mdx","tags":[],"version":"current","frontMatter":{"title":"Listenbrainz","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Listenbrainz (Endpoint)","permalink":"/multi-scrobbler/configuration/sources/listenbrainz-endpoint"},"next":{"title":"Maloja","permalink":"/multi-scrobbler/configuration/sources/maloja"}}'),t=r(4848),s=r(8453);r(8194),r(698),r(2170);var o=r(5248);let a={title:"Listenbrainz",toc_min_heading_level:2,toc_max_heading_level:5},c,l={},d=[{value:"Configuration",id:"configuration",level:2}];function u(e){let n={a:"a",admonition:"admonition",code:"code",h2:"h2",li:"li",p:"p",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components},{Config:r}=n;return r||function(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Config",!0),(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.p,{children:"This Source monitors a Listenbrainz account's scrobble history and then re-scrobbles discovered tracks to configured clients."}),"\n",(0,t.jsx)(n.admonition,{title:"Other Uses",type:"tip",children:(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["To ",(0,t.jsx)(n.strong,{children:"scrobble to"})," a Listenbrainz server, create a ",(0,t.jsx)(n.a,{href:"/configuration/clients/listenbrainz",children:"Listenbrainz (Client)"})]}),"\n",(0,t.jsxs)(n.li,{children:["To accept scrobbles from outside applications as if multi-scrobbler was a Listenbrainz server, create a ",(0,t.jsx)(n.a,{href:"/configuration/sources/listenbrainz-endpoint",children:"Listenbrainz (Endpoint)"})]}),"\n"]})}),"\n",(0,t.jsxs)(n.p,{children:["You will need to run your own Listenbrainz server or have an account ",(0,t.jsx)(n.a,{href:"https://listenbrainz.org/login/",children:"on the official instance"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["On your ",(0,t.jsx)(n.a,{href:"https://listenbrainz.org/profile/",children:"profile page"})," find your ",(0,t.jsx)(n.strong,{children:"User Token"})," to use in the configuration."]}),"\n",(0,t.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsx)(r,{config:"ListenBrainzSourceConfig",fileContent:o.A,name:"listenbrainz",children:(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Environmental Variable"}),(0,t.jsx)(n.th,{children:"Required?"}),(0,t.jsx)(n.th,{children:"Default"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"SOURCE_LZ_TOKEN"})}),(0,t.jsx)(n.td,{children:"Yes"}),(0,t.jsx)(n.td,{}),(0,t.jsx)(n.td,{children:"User token from your LZ profile"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"SOURCE_LZ_USER"})}),(0,t.jsx)(n.td,{children:"Yes"}),(0,t.jsx)(n.td,{}),(0,t.jsx)(n.td,{children:"Your LZ username"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"SOURCE_LZ_URL"})}),(0,t.jsx)(n.td,{children:"No"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.a,{href:"https://api.listenbrainz.org/",children:"https://api.listenbrainz.org/"})}),(0,t.jsx)(n.td,{children:"The base URL for the LZ server"})]})]})]})})]})}function h(e={}){let{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(u,{...e})}):u(e)}},5248(e,n,r){r.d(n,{A:()=>i});let i='[\n {\n "name": "brainzClient",\n "enable": true,\n "configureAs": "client",\n "data": {\n "token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",\n "username": "FoxxMD",\n "url": "https://api.listenbrainz.org"\n }\n },\n {\n "name": "brainzSource",\n "enable": true,\n "configureAs": "source",\n "data": {\n "token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",\n "username": "FoxxMD",\n "url": "https://api.listenbrainz.org"\n }\n }\n]\n'}}]); \ No newline at end of file diff --git a/assets/js/af9480c3.7c27f7e5.js b/assets/js/af9480c3.7c27f7e5.js new file mode 100644 index 00000000..3c749388 --- /dev/null +++ b/assets/js/af9480c3.7c27f7e5.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["232"],{6613(e,n,r){r.r(n),r.d(n,{metadata:()=>i,default:()=>h,frontMatter:()=>a,contentTitle:()=>c,toc:()=>d,assets:()=>l});var i=JSON.parse('{"id":"configuration/sources/listenbrainz-source","title":"Listenbrainz","description":"Listenbrainz (Source)","source":"@site/docs/configuration/sources/listenbrainz-source.mdx","sourceDirName":"configuration/sources","slug":"/configuration/sources/listenbrainz-source","permalink":"/multi-scrobbler/configuration/sources/listenbrainz-source","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/sources/listenbrainz-source.mdx","tags":[],"version":"current","frontMatter":{"title":"Listenbrainz","description":"Listenbrainz (Source)","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Listenbrainz (Endpoint)","permalink":"/multi-scrobbler/configuration/sources/listenbrainz-endpoint"},"next":{"title":"Maloja","permalink":"/multi-scrobbler/configuration/sources/maloja"}}'),t=r(4848),s=r(8453);r(8194),r(698),r(2170);var o=r(5248);let a={title:"Listenbrainz",description:"Listenbrainz (Source)",toc_min_heading_level:2,toc_max_heading_level:5},c,l={},d=[{value:"Configuration",id:"configuration",level:2}];function u(e){let n={a:"a",admonition:"admonition",code:"code",h2:"h2",li:"li",p:"p",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components},{Config:r}=n;return r||function(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Config",!0),(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.p,{children:"This Source monitors a Listenbrainz account's scrobble history and then re-scrobbles discovered tracks to configured clients."}),"\n",(0,t.jsx)(n.admonition,{title:"Other Uses",type:"tip",children:(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["To ",(0,t.jsx)(n.strong,{children:"scrobble to"})," a Listenbrainz server, create a ",(0,t.jsx)(n.a,{href:"/configuration/clients/listenbrainz",children:"Listenbrainz (Client)"})]}),"\n",(0,t.jsxs)(n.li,{children:["To accept scrobbles from outside applications as if multi-scrobbler was a Listenbrainz server, create a ",(0,t.jsx)(n.a,{href:"/configuration/sources/listenbrainz-endpoint",children:"Listenbrainz (Endpoint)"})]}),"\n"]})}),"\n",(0,t.jsxs)(n.p,{children:["You will need to run your own Listenbrainz server or have an account ",(0,t.jsx)(n.a,{href:"https://listenbrainz.org/login/",children:"on the official instance"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["On your ",(0,t.jsx)(n.a,{href:"https://listenbrainz.org/profile/",children:"profile page"})," find your ",(0,t.jsx)(n.strong,{children:"User Token"})," to use in the configuration."]}),"\n",(0,t.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsx)(r,{config:"ListenBrainzSourceConfig",fileContent:o.A,name:"listenbrainz",children:(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Environmental Variable"}),(0,t.jsx)(n.th,{children:"Required?"}),(0,t.jsx)(n.th,{children:"Default"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"SOURCE_LZ_TOKEN"})}),(0,t.jsx)(n.td,{children:"Yes"}),(0,t.jsx)(n.td,{}),(0,t.jsx)(n.td,{children:"User token from your LZ profile"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"SOURCE_LZ_USER"})}),(0,t.jsx)(n.td,{children:"Yes"}),(0,t.jsx)(n.td,{}),(0,t.jsx)(n.td,{children:"Your LZ username"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"SOURCE_LZ_URL"})}),(0,t.jsx)(n.td,{children:"No"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.a,{href:"https://api.listenbrainz.org/",children:"https://api.listenbrainz.org/"})}),(0,t.jsx)(n.td,{children:"The base URL for the LZ server"})]})]})]})})]})}function h(e={}){let{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(u,{...e})}):u(e)}},5248(e,n,r){r.d(n,{A:()=>i});let i='[\n {\n "name": "brainzClient",\n "enable": true,\n "configureAs": "client",\n "data": {\n "token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",\n "username": "FoxxMD",\n "url": "https://api.listenbrainz.org"\n }\n },\n {\n "name": "brainzSource",\n "enable": true,\n "configureAs": "source",\n "data": {\n "token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",\n "username": "FoxxMD",\n "url": "https://api.listenbrainz.org"\n }\n }\n]\n'}}]); \ No newline at end of file diff --git a/assets/js/b46f7c12.51d04af7.js b/assets/js/b46f7c12.5cb14606.js similarity index 80% rename from assets/js/b46f7c12.51d04af7.js rename to assets/js/b46f7c12.5cb14606.js index 08d792ac..7983cf13 100644 --- a/assets/js/b46f7c12.51d04af7.js +++ b/assets/js/b46f7c12.5cb14606.js @@ -1 +1 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["6691"],{2038(e,r,t){t.r(r),t.d(r,{metadata:()=>s,default:()=>u,frontMatter:()=>l,contentTitle:()=>a,toc:()=>o,assets:()=>c});var s=JSON.parse('{"id":"installation/service","title":"As a Service","description":"If you have multi-scrobbler installed locally you can enable it to run as a background service when you login.","source":"@site/docs/installation/service.md","sourceDirName":"installation","slug":"/installation/service","permalink":"/multi-scrobbler/installation/service","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/installation/service.md","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"sidebar_position":2,"title":"As a Service"},"sidebar":"tutorialSidebar","previous":{"title":"Overview","permalink":"/multi-scrobbler/installation/"},"next":{"title":"Updating","permalink":"/multi-scrobbler/category/updating"}}'),i=t(4848),n=t(8453);let l={sidebar_position:2,title:"As a Service"},a="Systemd",c={},o=[{value:"Create A Unit File",id:"create-a-unit-file",level:2},{value:"Start the Service",id:"start-the-service",level:2}];function d(e){let r={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",li:"li",p:"p",pre:"pre",ul:"ul",...(0,n.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(r.p,{children:["If you have multi-scrobbler installed ",(0,i.jsx)(r.a,{href:"/multi-scrobbler/installation/#nodejs",children:"locally"})," you can enable it to run as a background service when you login."]}),"\n",(0,i.jsx)(r.p,{children:"Before running as a service you should run it at least once in the foreground to ensure it can start up correctly!"}),"\n",(0,i.jsx)(r.header,{children:(0,i.jsx)(r.h1,{id:"systemd",children:(0,i.jsx)(r.a,{href:"https://systemd.io/",children:"Systemd"})})}),"\n",(0,i.jsxs)(r.p,{children:["Systemd is system and service manager that is used by ",(0,i.jsx)(r.a,{href:"https://en.wikipedia.org/wiki/Systemd#Adoption",children:"most popular linux distros"})," including ",(0,i.jsx)(r.a,{href:"https://wiki.ubuntu.com/SystemdForUpstartUsers",children:"Ubuntu"}),", ",(0,i.jsx)(r.a,{href:"https://docs.fedoraproject.org/en-US/quick-docs/understanding-and-administering-systemd/",children:"Fedora"}),", Pop!_OS, ",(0,i.jsx)(r.a,{href:"https://wiki.debian.org/systemd",children:"Debian"}),", and ",(0,i.jsx)(r.a,{href:"https://wiki.archlinux.org/title/systemd",children:"Arch"}),"."]}),"\n",(0,i.jsxs)(r.p,{children:["This setup will create a ",(0,i.jsx)(r.a,{href:"https://wiki.archlinux.org/title/systemd/User",children:"user service"})," that runs on login."]}),"\n",(0,i.jsx)(r.h2,{id:"create-a-unit-file",children:"Create A Unit File"}),"\n",(0,i.jsx)(r.p,{children:"Create a new service file for multi-scrobbler under your HOME config:"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-bash",children:"mkdir -p ~/.config/systemd/user\ntouch ~/.config/systemd/user/multi-scrobbler.service\n"})}),"\n",(0,i.jsxs)(r.p,{children:["In a text editor add contents below to the file you created, ",(0,i.jsx)(r.code,{children:"multi-scrobbler.service"}),":"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-ini",children:"[Unit]\nDescription=multi-scrobbler\nAfter=network.target\n\n[Service]\nType=simple\nWorkingDirectory=/path/to/multi-scrobbler/directory\nExecStart=npm run start\nRestart=no\n\n[Install]\nWantedBy=default.target\n"})}),"\n",(0,i.jsx)(r.h2,{id:"start-the-service",children:"Start the Service"}),"\n",(0,i.jsx)(r.p,{children:"Save the file then run:"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-bash",children:"systemctl daemon-reload\nsystemctl --user enable multi-scrobbler.service\nsystemctl --user start multi-scrobbler.service\n"})}),"\n",(0,i.jsx)(r.p,{children:"This will"}),"\n",(0,i.jsxs)(r.ul,{children:["\n",(0,i.jsx)(r.li,{children:"scan for new services and pickup our multi-scrobbler user service"}),"\n",(0,i.jsx)(r.li,{children:"enable the service to run at login automatically"}),"\n",(0,i.jsx)(r.li,{children:"start the service now"}),"\n"]}),"\n",(0,i.jsx)(r.h1,{id:"other-service-methods",children:"Other Service Methods"}),"\n",(0,i.jsx)(r.p,{children:"Open a PR if you would like to document setting up multi-scrobbler for other service managers!"})]})}function u(e={}){let{wrapper:r}={...(0,n.R)(),...e.components};return r?(0,i.jsx)(r,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["6691"],{2038(e,r,t){t.r(r),t.d(r,{metadata:()=>s,default:()=>u,frontMatter:()=>l,contentTitle:()=>a,toc:()=>o,assets:()=>c});var s=JSON.parse('{"id":"installation/service","title":"As a Service","description":"Local Install as a Service","source":"@site/docs/installation/service.md","sourceDirName":"installation","slug":"/installation/service","permalink":"/multi-scrobbler/installation/service","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/installation/service.md","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"sidebar_position":2,"title":"As a Service","description":"Local Install as a Service"},"sidebar":"tutorialSidebar","previous":{"title":"Overview","permalink":"/multi-scrobbler/installation/"},"next":{"title":"Updating","permalink":"/multi-scrobbler/category/updating"}}'),i=t(4848),n=t(8453);let l={sidebar_position:2,title:"As a Service",description:"Local Install as a Service"},a="Systemd",c={},o=[{value:"Create A Unit File",id:"create-a-unit-file",level:2},{value:"Start the Service",id:"start-the-service",level:2}];function d(e){let r={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",li:"li",p:"p",pre:"pre",ul:"ul",...(0,n.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(r.p,{children:["If you have multi-scrobbler installed ",(0,i.jsx)(r.a,{href:"/multi-scrobbler/installation/#nodejs",children:"locally"})," you can enable it to run as a background service when you login."]}),"\n",(0,i.jsx)(r.p,{children:"Before running as a service you should run it at least once in the foreground to ensure it can start up correctly!"}),"\n",(0,i.jsx)(r.header,{children:(0,i.jsx)(r.h1,{id:"systemd",children:(0,i.jsx)(r.a,{href:"https://systemd.io/",children:"Systemd"})})}),"\n",(0,i.jsxs)(r.p,{children:["Systemd is system and service manager that is used by ",(0,i.jsx)(r.a,{href:"https://en.wikipedia.org/wiki/Systemd#Adoption",children:"most popular linux distros"})," including ",(0,i.jsx)(r.a,{href:"https://wiki.ubuntu.com/SystemdForUpstartUsers",children:"Ubuntu"}),", ",(0,i.jsx)(r.a,{href:"https://docs.fedoraproject.org/en-US/quick-docs/understanding-and-administering-systemd/",children:"Fedora"}),", Pop!_OS, ",(0,i.jsx)(r.a,{href:"https://wiki.debian.org/systemd",children:"Debian"}),", and ",(0,i.jsx)(r.a,{href:"https://wiki.archlinux.org/title/systemd",children:"Arch"}),"."]}),"\n",(0,i.jsxs)(r.p,{children:["This setup will create a ",(0,i.jsx)(r.a,{href:"https://wiki.archlinux.org/title/systemd/User",children:"user service"})," that runs on login."]}),"\n",(0,i.jsx)(r.h2,{id:"create-a-unit-file",children:"Create A Unit File"}),"\n",(0,i.jsx)(r.p,{children:"Create a new service file for multi-scrobbler under your HOME config:"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-bash",children:"mkdir -p ~/.config/systemd/user\ntouch ~/.config/systemd/user/multi-scrobbler.service\n"})}),"\n",(0,i.jsxs)(r.p,{children:["In a text editor add contents below to the file you created, ",(0,i.jsx)(r.code,{children:"multi-scrobbler.service"}),":"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-ini",children:"[Unit]\nDescription=multi-scrobbler\nAfter=network.target\n\n[Service]\nType=simple\nWorkingDirectory=/path/to/multi-scrobbler/directory\nExecStart=npm run start\nRestart=no\n\n[Install]\nWantedBy=default.target\n"})}),"\n",(0,i.jsx)(r.h2,{id:"start-the-service",children:"Start the Service"}),"\n",(0,i.jsx)(r.p,{children:"Save the file then run:"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-bash",children:"systemctl daemon-reload\nsystemctl --user enable multi-scrobbler.service\nsystemctl --user start multi-scrobbler.service\n"})}),"\n",(0,i.jsx)(r.p,{children:"This will"}),"\n",(0,i.jsxs)(r.ul,{children:["\n",(0,i.jsx)(r.li,{children:"scan for new services and pickup our multi-scrobbler user service"}),"\n",(0,i.jsx)(r.li,{children:"enable the service to run at login automatically"}),"\n",(0,i.jsx)(r.li,{children:"start the service now"}),"\n"]}),"\n",(0,i.jsx)(r.h1,{id:"other-service-methods",children:"Other Service Methods"}),"\n",(0,i.jsx)(r.p,{children:"Open a PR if you would like to document setting up multi-scrobbler for other service managers!"})]})}function u(e={}){let{wrapper:r}={...(0,n.R)(),...e.components};return r?(0,i.jsx)(r,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}}}]); \ No newline at end of file diff --git a/assets/js/bf69edfb.4546f1f1.js b/assets/js/bf69edfb.4546f1f1.js deleted file mode 100644 index 2141c666..00000000 --- a/assets/js/bf69edfb.4546f1f1.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["7736"],{6684(e,n,i){i.r(n),i.d(n,{metadata:()=>r,default:()=>m,frontMatter:()=>a,contentTitle:()=>c,toc:()=>h,assets:()=>d});var r=JSON.parse('{"id":"installation/installation","title":"Overview","description":"For the difference between ENV and File examples in this document see Configuration Types.","source":"@site/docs/installation/installation.mdx","sourceDirName":"installation","slug":"/installation/","permalink":"/multi-scrobbler/installation/","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/installation/installation.mdx","tags":[],"version":"current","sidebarPosition":1,"frontMatter":{"sidebar_position":1,"title":"Overview"},"sidebar":"tutorialSidebar","previous":{"title":"Quickstart","permalink":"/multi-scrobbler/quickstart"},"next":{"title":"As a Service","permalink":"/multi-scrobbler/installation/service"}}'),s=i(4848),l=i(8453),t=i(8194),o=i(698);i(2170);let a={sidebar_position:1,title:"Overview"},c,d={},h=[{value:"Docker",id:"docker",level:2},{value:"Recommended Settings",id:"recommended-settings",level:3},{value:"Docker Usage Example",id:"docker-usage-example",level:3},{value:"Local Installation",id:"local-installation",level:2},{value:"Nodejs",id:"nodejs",level:3},{value:"Rollup build error",id:"rollup-build-error",level:4},{value:"Usage Examples",id:"usage-examples",level:4},{value:"Flatpak",id:"flatpak",level:3}];function u(e){let n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",h4:"h4",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,l.R)(),...e.components},{Details:i}=n;return i||function(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsxs)(n.p,{children:["For the difference between ",(0,s.jsx)(n.strong,{children:"ENV"})," and ",(0,s.jsx)(n.strong,{children:"File"})," examples in this document see ",(0,s.jsx)(n.a,{href:"/multi-scrobbler/configuration/#configuration-types",children:"Configuration Types"}),"."]})}),"\n",(0,s.jsx)(n.h2,{id:"docker",children:"Docker"}),"\n",(0,s.jsx)(n.p,{children:"Cross-platform images are built for x86 (Intel/AMD) and ARM64 (IE Raspberry Pi)"}),"\n",(0,s.jsx)(n.admonition,{title:"Available Images",type:"info",children:(0,s.jsxs)(t.A,{children:[(0,s.jsxs)(o.A,{value:"docker",label:"Dockerhub",children:[(0,s.jsx)(n.p,{children:(0,s.jsx)(n.a,{href:"https://hub.docker.com/r/foxxmd/multi-scrobbler",children:"Repository Page"})}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"docker.io/foxxmd/multi-scrobbler:latest\n"})})]}),(0,s.jsxs)(o.A,{value:"ghcr",label:"Github Packages",children:[(0,s.jsx)(n.p,{children:(0,s.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/pkgs/container/multi-scrobbler",children:"Repository Page"})}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"ghcr.io/foxxmd/multi-scrobbler:latest\n"})})]})]})}),"\n",(0,s.jsx)(n.h3,{id:"recommended-settings",children:"Recommended Settings"}),"\n",(0,s.jsxs)(t.A,{groupId:"dockerSetting",queryString:!0,children:[(0,s.jsxs)(o.A,{value:"storage",label:"Storage",children:[(0,s.jsxs)(n.p,{children:["You ",(0,s.jsx)(n.strong,{children:"should"})," bind a host directory into the container for storing configurations and credentials. Otherwise, these will be lost when the container is updated."]}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:'services:\n multi-scrobbler:\n # ...\n // highlight-start\n volumes:\n - "./config:/config"\n // highlight-end\n'})})]}),(0,s.jsxs)(o.A,{value:"networking",label:"Networking",children:[(0,s.jsxs)(n.p,{children:["If you are using a ",(0,s.jsx)(n.a,{href:"https://www.appsdeveloperblog.com/docker-networking-bridging-host-and-overlay/",children:"bridge network"})," (default docker setup) you must map a port to the container in order to access the dashboard and use MS with some sources (Webscrobbler, LFM/LZ Endpoints). The default container port is ",(0,s.jsx)(n.code,{children:"9078"}),"."]}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:'services:\n multi-scrobbler:\n # ...\n // highlight-start\n ports:\n - "9078:9078"\n // highlight-end\n'})})]}),(0,s.jsxs)(o.A,{value:"baseUrl",label:"Base URL",children:[(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Optionally"}),", when"]}),(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["using a ",(0,s.jsx)(n.a,{href:"/multi-scrobbler/configuration/",children:"Source or Client"}),' that has a "Redirect URI" that you have not explicitly defined']}),"\n",(0,s.jsxs)(n.li,{children:["and","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"using a bridge network or"}),"\n",(0,s.jsx)(n.li,{children:"installing MS on a different machine than the one used to view the dashboard"}),"\n"]}),"\n"]}),"\n"]}),(0,s.jsxs)(n.p,{children:["set the ",(0,s.jsx)(n.a,{href:"/multi-scrobbler/configuration/#base-url",children:"Base URL"})," as the IP of the host machine. (This is the IP you would use to view the dashboard in a browser)"]}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n # ...\n environment:\n // highlight-start\n - BASE_URL=http://hostMachineIP\n // highlight-end\n"})})]}),(0,s.jsxs)(o.A,{value:"tz",label:"Timezone",children:[(0,s.jsxs)(n.p,{children:["Set the ",(0,s.jsx)(n.a,{href:"https://en.wikipedia.org/wiki/List_of_tz_database_time_zones",children:"timezone"})," for the container using the environmental variable ",(0,s.jsx)(n.code,{children:"TZ"}),"."]}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n # ...\n environment:\n # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones\n // highlight-start\n - TZ=America/New_York\n // highlight-end\n"})})]}),(0,s.jsxs)(o.A,{value:"linuxHost",label:"Linux Host",children:[(0,s.jsxs)(n.p,{children:["If you are running this container with ",(0,s.jsx)(n.strong,{children:"Docker"})," on a ",(0,s.jsx)(n.strong,{children:"Linux Host"})," you ",(0,s.jsx)(n.strong,{children:"should"})," specify ",(0,s.jsx)(n.code,{children:"user:group"})," permissions of the user who owns the ",(0,s.jsx)(n.strong,{children:"configuration directory"})," on the host to avoid ",(0,s.jsx)(n.a,{href:"https://ikriv.com/blog/?p=4698",children:"docker file permission problems."})," These can be specified using the ",(0,s.jsxs)(n.a,{href:"https://docs.linuxserver.io/general/understanding-puid-and-pgid",children:["environmental variables ",(0,s.jsx)(n.strong,{children:"PUID"})," and ",(0,s.jsx)(n.strong,{children:"PGID"}),"."]})]}),(0,s.jsx)(n.p,{children:"To get the UID and GID for the current user run these commands from a terminal:"}),(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"id -u"})," -- prints UID"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"id -g"})," -- prints GID"]}),"\n"]})]}),(0,s.jsxs)(o.A,{value:"ro",label:"Read Only",children:[(0,s.jsxs)(n.p,{children:["MS can be hardended by setting the file system as read only with ",(0,s.jsx)(n.a,{href:"https://docs.docker.com/reference/compose-file/services/#read_only",children:(0,s.jsx)(n.code,{children:"read_only"})}),", with a few exceptions:"]}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n image: foxxmd/multi-scrobbler\n # ...\n // highlight-start\n read_only: true\n tmpfs:\n - /run:exec\n - /tmp:noexec \n // highlight-end \n"})})]}),(0,s.jsxs)(o.A,{value:"caching",label:"Caching",children:[(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Optionally"}),", add a ",(0,s.jsx)(n.a,{href:"https://valkey.io/",children:"Valkey"})," service to your stack for ",(0,s.jsx)(n.a,{href:"/configuration/?cacheType=valkey#secondary-caching-configuration",children:"secondary caching"})," to take advantage of faster performance and reduced memory usage."]}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n image: foxxmd/multi-scrobbler\n # ...\n environment:\n # ...\n // highlight-start\n CACHE_METADATA=valkey\n CACHE_METADATA_CONN=redis://valkey:6379\n // highlight-end\n # ...\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"})})]})]}),"\n",(0,s.jsx)(n.h3,{id:"docker-usage-example",children:"Docker Usage Example"}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsxs)(n.p,{children:["See the ",(0,s.jsx)(n.a,{href:"/quickstart",children:(0,s.jsx)(n.strong,{children:"Quick Start Guide"})})," for another guided docker-compose example"]})}),"\n",(0,s.jsx)(n.p,{children:"The example scenario:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsxs)(n.a,{href:"/configuration/sources/jellyfin",children:["Jellyfin ",(0,s.jsx)(n.strong,{children:"Source"})]})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsxs)(n.a,{href:"/configuration/sources/maloja",children:["Maloja ",(0,s.jsx)(n.strong,{children:"Client"})]})}),"\n",(0,s.jsxs)(n.li,{children:["Serving app on port ",(0,s.jsx)(n.code,{children:"9078"})]}),"\n",(0,s.jsxs)(n.li,{children:["Docker container located on a different IP (",(0,s.jsx)(n.code,{children:"192.168.0.100"}),") so use ",(0,s.jsx)(n.a,{href:"/configuration#base-url",children:"Base URL"})]}),"\n",(0,s.jsxs)(n.li,{children:["Config/data directory on host machine in a directory next to ",(0,s.jsx)(n.code,{children:"docker-compose.yml"})]}),"\n",(0,s.jsxs)(n.li,{children:["Linux uid/gid is ",(0,s.jsx)(n.code,{children:"1000:1000"})]}),"\n",(0,s.jsx)(n.li,{children:"Optional caching using valkey (after uncommenting)"}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["See ",(0,s.jsx)(n.a,{href:"#docker",children:(0,s.jsx)(n.code,{children:"docker-compose.yml"})})," sample above for more options and annotations."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n image: foxxmd/multi-scrobbler\n container_name: multi-scrobbler\n environment:\n - TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones\n - JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56bd6540c\n - JELLYFIN_URL=192.168.0.101:8096\n - JELLYFIN_USER=MyUser\n - BASE_URL=http://192.168.0.100:9078\n - MALOJA_URL=http://domain.tld:42010\n - MALOJA_API_KEY=1234\n - PUID=1000\n - PGID=1000\n # uncomment along with valkey service/volume below for better caching\n #- CACHE_METADATA=valkey\n #- CACHE_METADATA_CONN=redis://valkey:6379\n volumes:\n - ./config:/config\n ports:\n - 9078:9078\n restart: unless-stopped\n \n #valkey:\n # image: valkey/valkey\n # volumes:\n # - valkeydata:/data\n\n#volumes:\n# valkeydata:\n# driver: local\n"})}),"\n",(0,s.jsx)(n.h2,{id:"local-installation",children:"Local Installation"}),"\n",(0,s.jsxs)(n.p,{children:["After installation see ",(0,s.jsx)(n.a,{href:"/multi-scrobbler/installation/service",children:"service.md"})," to configure multi-scrobbler to run automatically in the background."]}),"\n",(0,s.jsx)(n.h3,{id:"nodejs",children:"Nodejs"}),"\n",(0,s.jsx)(n.p,{children:"Clone this repository somewhere and then install from the working directory"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",children:"git clone https://github.com/FoxxMD/multi-scrobbler.git .\ncd multi-scrobbler\nnvm use # optional, to set correct Node version\nnpm install\nnpm run docs:install && npm run build\nnpm run start\n"})}),"\n",(0,s.jsx)(n.h4,{id:"rollup-build-error",children:"Rollup build error"}),"\n",(0,s.jsxs)(n.p,{children:["During building if you encounter an error like: ",(0,s.jsx)(n.code,{children:'Your current platform "XXX" and architecture "XXX" combination is not yet supported by the native Rollup build.'})]}),"\n",(0,s.jsxs)(n.p,{children:["Modify ",(0,s.jsx)(n.code,{children:"overrides"})," in ",(0,s.jsx)(n.code,{children:"package.json"})," to use ",(0,s.jsx)(n.code,{children:"@rollup/wasm-node"})," as a drop-in replacement for rollup:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'"overrides": {\n "spotify-web-api-node": {\n "superagent": "$superagent"\n }\n "vite": {\n "rollup": "npm:@rollup/wasm-node@^4.9.6"\n }\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["See ",(0,s.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/issues/135#issuecomment-1927080260",children:"this issue"})," for more detail."]}),"\n",(0,s.jsx)(n.h4,{id:"usage-examples",children:"Usage Examples"}),"\n",(0,s.jsxs)(t.A,{groupId:"configType",queryString:!0,children:[(0,s.jsx)(o.A,{value:"env",label:"ENV",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",children:'JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56bd6540c JELLYFIN_URL=192.168.0.101:8096 JELLYFIN_USER=MyUser MALOJA_URL="http://domain.tld" node src/index.js\n'})})}),(0,s.jsxs)(o.A,{value:"file",label:"File",children:[(0,s.jsxs)(i,{children:[(0,s.jsx)("summary",{children:(0,s.jsx)(n.code,{children:"./config/config.json"})}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",metastring:'title="./config/config.json"',children:'{\n "sources": [\n {\n "type": "jellyfin",\n "clients": ["myConfig"],\n "name": "myJellyfinSource",\n "data": {\n "apiKey": "a89cba1569901a0671d5a9875fed4be1",\n "url": "http://192.168.0.101:8096",\n "user": "MyUser"\n }\n }\n ],\n "clients": [\n {\n "type": "maloja",\n "name": "myConfig",\n "data": {\n "url": "http://localhost:42010",\n "apiKey": "myMalojaKey"\n }\n }\n ],\n}\n'})})]}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",children:"npm run start\n"})})]})]}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsxs)(n.p,{children:["The web UI and API is served on port ",(0,s.jsx)(n.code,{children:"9078"}),". This can be modified using the ",(0,s.jsx)(n.code,{children:"PORT"})," environmental variable."]})}),"\n",(0,s.jsx)(n.h3,{id:"flatpak",children:"Flatpak"}),"\n",(0,s.jsx)(n.admonition,{title:"Unsupported",type:"warning",children:(0,s.jsxs)(n.p,{children:["Flatpak/Flathub installs are no longer supported. You can still build MS as a ",(0,s.jsx)(n.a,{href:"../development/flatpak",children:"Flatpak app from source."})," See more information about ",(0,s.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/issues/287",children:"Flatpak EOL."})]})})]})}function m(e={}){let{wrapper:n}={...(0,l.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(u,{...e})}):u(e)}}}]); \ No newline at end of file diff --git a/assets/js/bf69edfb.5fec8bfc.js b/assets/js/bf69edfb.5fec8bfc.js new file mode 100644 index 00000000..0a36e0b4 --- /dev/null +++ b/assets/js/bf69edfb.5fec8bfc.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["7736"],{6684(e,n,i){i.r(n),i.d(n,{metadata:()=>r,default:()=>m,frontMatter:()=>a,contentTitle:()=>c,toc:()=>h,assets:()=>d});var r=JSON.parse('{"id":"installation/installation","title":"Overview","description":"Installation","source":"@site/docs/installation/installation.mdx","sourceDirName":"installation","slug":"/installation/","permalink":"/multi-scrobbler/installation/","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/installation/installation.mdx","tags":[],"version":"current","sidebarPosition":1,"frontMatter":{"sidebar_position":1,"title":"Overview","description":"Installation"},"sidebar":"tutorialSidebar","previous":{"title":"Quickstart","permalink":"/multi-scrobbler/quickstart"},"next":{"title":"As a Service","permalink":"/multi-scrobbler/installation/service"}}'),s=i(4848),l=i(8453),t=i(8194),o=i(698);i(2170);let a={sidebar_position:1,title:"Overview",description:"Installation"},c,d={},h=[{value:"Docker",id:"docker",level:2},{value:"Recommended Settings",id:"recommended-settings",level:3},{value:"Docker Usage Example",id:"docker-usage-example",level:3},{value:"Local Installation",id:"local-installation",level:2},{value:"Nodejs",id:"nodejs",level:3},{value:"Rollup build error",id:"rollup-build-error",level:4},{value:"Usage Examples",id:"usage-examples",level:4},{value:"Flatpak",id:"flatpak",level:3}];function u(e){let n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",h4:"h4",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,l.R)(),...e.components},{Details:i}=n;return i||function(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsxs)(n.p,{children:["For the difference between ",(0,s.jsx)(n.strong,{children:"ENV"})," and ",(0,s.jsx)(n.strong,{children:"File"})," examples in this document see ",(0,s.jsx)(n.a,{href:"/multi-scrobbler/configuration/#configuration-types",children:"Configuration Types"}),"."]})}),"\n",(0,s.jsx)(n.h2,{id:"docker",children:"Docker"}),"\n",(0,s.jsx)(n.p,{children:"Cross-platform images are built for x86 (Intel/AMD) and ARM64 (IE Raspberry Pi)"}),"\n",(0,s.jsx)(n.admonition,{title:"Available Images",type:"info",children:(0,s.jsxs)(t.A,{children:[(0,s.jsxs)(o.A,{value:"docker",label:"Dockerhub",children:[(0,s.jsx)(n.p,{children:(0,s.jsx)(n.a,{href:"https://hub.docker.com/r/foxxmd/multi-scrobbler",children:"Repository Page"})}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"docker.io/foxxmd/multi-scrobbler:latest\n"})})]}),(0,s.jsxs)(o.A,{value:"ghcr",label:"Github Packages",children:[(0,s.jsx)(n.p,{children:(0,s.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/pkgs/container/multi-scrobbler",children:"Repository Page"})}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"ghcr.io/foxxmd/multi-scrobbler:latest\n"})})]})]})}),"\n",(0,s.jsx)(n.h3,{id:"recommended-settings",children:"Recommended Settings"}),"\n",(0,s.jsxs)(t.A,{groupId:"dockerSetting",queryString:!0,children:[(0,s.jsxs)(o.A,{value:"storage",label:"Storage",children:[(0,s.jsxs)(n.p,{children:["You ",(0,s.jsx)(n.strong,{children:"should"})," bind a host directory into the container for storing configurations and credentials. Otherwise, these will be lost when the container is updated."]}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:'services:\n multi-scrobbler:\n # ...\n // highlight-start\n volumes:\n - "./config:/config"\n // highlight-end\n'})})]}),(0,s.jsxs)(o.A,{value:"networking",label:"Networking",children:[(0,s.jsxs)(n.p,{children:["If you are using a ",(0,s.jsx)(n.a,{href:"https://www.appsdeveloperblog.com/docker-networking-bridging-host-and-overlay/",children:"bridge network"})," (default docker setup) you must map a port to the container in order to access the dashboard and use MS with some sources (Webscrobbler, LFM/LZ Endpoints). The default container port is ",(0,s.jsx)(n.code,{children:"9078"}),"."]}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:'services:\n multi-scrobbler:\n # ...\n // highlight-start\n ports:\n - "9078:9078"\n // highlight-end\n'})})]}),(0,s.jsxs)(o.A,{value:"baseUrl",label:"Base URL",children:[(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Optionally"}),", when"]}),(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["using a ",(0,s.jsx)(n.a,{href:"/multi-scrobbler/configuration/",children:"Source or Client"}),' that has a "Redirect URI" that you have not explicitly defined']}),"\n",(0,s.jsxs)(n.li,{children:["and","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"using a bridge network or"}),"\n",(0,s.jsx)(n.li,{children:"installing MS on a different machine than the one used to view the dashboard"}),"\n"]}),"\n"]}),"\n"]}),(0,s.jsxs)(n.p,{children:["set the ",(0,s.jsx)(n.a,{href:"/multi-scrobbler/configuration/#base-url",children:"Base URL"})," as the IP of the host machine. (This is the IP you would use to view the dashboard in a browser)"]}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n # ...\n environment:\n // highlight-start\n - BASE_URL=http://hostMachineIP\n // highlight-end\n"})})]}),(0,s.jsxs)(o.A,{value:"tz",label:"Timezone",children:[(0,s.jsxs)(n.p,{children:["Set the ",(0,s.jsx)(n.a,{href:"https://en.wikipedia.org/wiki/List_of_tz_database_time_zones",children:"timezone"})," for the container using the environmental variable ",(0,s.jsx)(n.code,{children:"TZ"}),"."]}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n # ...\n environment:\n # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones\n // highlight-start\n - TZ=America/New_York\n // highlight-end\n"})})]}),(0,s.jsxs)(o.A,{value:"linuxHost",label:"Linux Host",children:[(0,s.jsxs)(n.p,{children:["If you are running this container with ",(0,s.jsx)(n.strong,{children:"Docker"})," on a ",(0,s.jsx)(n.strong,{children:"Linux Host"})," you ",(0,s.jsx)(n.strong,{children:"should"})," specify ",(0,s.jsx)(n.code,{children:"user:group"})," permissions of the user who owns the ",(0,s.jsx)(n.strong,{children:"configuration directory"})," on the host to avoid ",(0,s.jsx)(n.a,{href:"https://ikriv.com/blog/?p=4698",children:"docker file permission problems."})," These can be specified using the ",(0,s.jsxs)(n.a,{href:"https://docs.linuxserver.io/general/understanding-puid-and-pgid",children:["environmental variables ",(0,s.jsx)(n.strong,{children:"PUID"})," and ",(0,s.jsx)(n.strong,{children:"PGID"}),"."]})]}),(0,s.jsx)(n.p,{children:"To get the UID and GID for the current user run these commands from a terminal:"}),(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"id -u"})," -- prints UID"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"id -g"})," -- prints GID"]}),"\n"]})]}),(0,s.jsxs)(o.A,{value:"ro",label:"Read Only",children:[(0,s.jsxs)(n.p,{children:["MS can be hardended by setting the file system as read only with ",(0,s.jsx)(n.a,{href:"https://docs.docker.com/reference/compose-file/services/#read_only",children:(0,s.jsx)(n.code,{children:"read_only"})}),", with a few exceptions:"]}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n image: foxxmd/multi-scrobbler\n # ...\n // highlight-start\n read_only: true\n tmpfs:\n - /run:exec\n - /tmp:noexec \n // highlight-end \n"})})]}),(0,s.jsxs)(o.A,{value:"caching",label:"Caching",children:[(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Optionally"}),", add a ",(0,s.jsx)(n.a,{href:"https://valkey.io/",children:"Valkey"})," service to your stack for ",(0,s.jsx)(n.a,{href:"/configuration/?cacheType=valkey#secondary-caching-configuration",children:"secondary caching"})," to take advantage of faster performance and reduced memory usage."]}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n image: foxxmd/multi-scrobbler\n # ...\n environment:\n # ...\n // highlight-start\n CACHE_METADATA=valkey\n CACHE_METADATA_CONN=redis://valkey:6379\n // highlight-end\n # ...\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"})})]})]}),"\n",(0,s.jsx)(n.h3,{id:"docker-usage-example",children:"Docker Usage Example"}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsxs)(n.p,{children:["See the ",(0,s.jsx)(n.a,{href:"/quickstart",children:(0,s.jsx)(n.strong,{children:"Quick Start Guide"})})," for another guided docker-compose example"]})}),"\n",(0,s.jsx)(n.p,{children:"The example scenario:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsxs)(n.a,{href:"/configuration/sources/jellyfin",children:["Jellyfin ",(0,s.jsx)(n.strong,{children:"Source"})]})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsxs)(n.a,{href:"/configuration/sources/maloja",children:["Maloja ",(0,s.jsx)(n.strong,{children:"Client"})]})}),"\n",(0,s.jsxs)(n.li,{children:["Serving app on port ",(0,s.jsx)(n.code,{children:"9078"})]}),"\n",(0,s.jsxs)(n.li,{children:["Docker container located on a different IP (",(0,s.jsx)(n.code,{children:"192.168.0.100"}),") so use ",(0,s.jsx)(n.a,{href:"/configuration#base-url",children:"Base URL"})]}),"\n",(0,s.jsxs)(n.li,{children:["Config/data directory on host machine in a directory next to ",(0,s.jsx)(n.code,{children:"docker-compose.yml"})]}),"\n",(0,s.jsxs)(n.li,{children:["Linux uid/gid is ",(0,s.jsx)(n.code,{children:"1000:1000"})]}),"\n",(0,s.jsx)(n.li,{children:"Optional caching using valkey (after uncommenting)"}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["See ",(0,s.jsx)(n.a,{href:"#docker",children:(0,s.jsx)(n.code,{children:"docker-compose.yml"})})," sample above for more options and annotations."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",metastring:'title="docker-compose.yml"',children:"services:\n multi-scrobbler:\n image: foxxmd/multi-scrobbler\n container_name: multi-scrobbler\n environment:\n - TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones\n - JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56bd6540c\n - JELLYFIN_URL=192.168.0.101:8096\n - JELLYFIN_USER=MyUser\n - BASE_URL=http://192.168.0.100:9078\n - MALOJA_URL=http://domain.tld:42010\n - MALOJA_API_KEY=1234\n - PUID=1000\n - PGID=1000\n # uncomment along with valkey service/volume below for better caching\n #- CACHE_METADATA=valkey\n #- CACHE_METADATA_CONN=redis://valkey:6379\n volumes:\n - ./config:/config\n ports:\n - 9078:9078\n restart: unless-stopped\n \n #valkey:\n # image: valkey/valkey\n # volumes:\n # - valkeydata:/data\n\n#volumes:\n# valkeydata:\n# driver: local\n"})}),"\n",(0,s.jsx)(n.h2,{id:"local-installation",children:"Local Installation"}),"\n",(0,s.jsxs)(n.p,{children:["After installation see ",(0,s.jsx)(n.a,{href:"/multi-scrobbler/installation/service",children:"service.md"})," to configure multi-scrobbler to run automatically in the background."]}),"\n",(0,s.jsx)(n.h3,{id:"nodejs",children:"Nodejs"}),"\n",(0,s.jsx)(n.p,{children:"Clone this repository somewhere and then install from the working directory"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",children:"git clone https://github.com/FoxxMD/multi-scrobbler.git .\ncd multi-scrobbler\nnvm use # optional, to set correct Node version\nnpm install\nnpm run docs:install && npm run build\nnpm run start\n"})}),"\n",(0,s.jsx)(n.h4,{id:"rollup-build-error",children:"Rollup build error"}),"\n",(0,s.jsxs)(n.p,{children:["During building if you encounter an error like: ",(0,s.jsx)(n.code,{children:'Your current platform "XXX" and architecture "XXX" combination is not yet supported by the native Rollup build.'})]}),"\n",(0,s.jsxs)(n.p,{children:["Modify ",(0,s.jsx)(n.code,{children:"overrides"})," in ",(0,s.jsx)(n.code,{children:"package.json"})," to use ",(0,s.jsx)(n.code,{children:"@rollup/wasm-node"})," as a drop-in replacement for rollup:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'"overrides": {\n "spotify-web-api-node": {\n "superagent": "$superagent"\n }\n "vite": {\n "rollup": "npm:@rollup/wasm-node@^4.9.6"\n }\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["See ",(0,s.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/issues/135#issuecomment-1927080260",children:"this issue"})," for more detail."]}),"\n",(0,s.jsx)(n.h4,{id:"usage-examples",children:"Usage Examples"}),"\n",(0,s.jsxs)(t.A,{groupId:"configType",queryString:!0,children:[(0,s.jsx)(o.A,{value:"env",label:"ENV",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",children:'JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56bd6540c JELLYFIN_URL=192.168.0.101:8096 JELLYFIN_USER=MyUser MALOJA_URL="http://domain.tld" node src/index.js\n'})})}),(0,s.jsxs)(o.A,{value:"file",label:"File",children:[(0,s.jsxs)(i,{children:[(0,s.jsx)("summary",{children:(0,s.jsx)(n.code,{children:"./config/config.json"})}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",metastring:'title="./config/config.json"',children:'{\n "sources": [\n {\n "type": "jellyfin",\n "clients": ["myConfig"],\n "name": "myJellyfinSource",\n "data": {\n "apiKey": "a89cba1569901a0671d5a9875fed4be1",\n "url": "http://192.168.0.101:8096",\n "user": "MyUser"\n }\n }\n ],\n "clients": [\n {\n "type": "maloja",\n "name": "myConfig",\n "data": {\n "url": "http://localhost:42010",\n "apiKey": "myMalojaKey"\n }\n }\n ],\n}\n'})})]}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",children:"npm run start\n"})})]})]}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsxs)(n.p,{children:["The web UI and API is served on port ",(0,s.jsx)(n.code,{children:"9078"}),". This can be modified using the ",(0,s.jsx)(n.code,{children:"PORT"})," environmental variable."]})}),"\n",(0,s.jsx)(n.h3,{id:"flatpak",children:"Flatpak"}),"\n",(0,s.jsx)(n.admonition,{title:"Unsupported",type:"warning",children:(0,s.jsxs)(n.p,{children:["Flatpak/Flathub installs are no longer supported. You can still build MS as a ",(0,s.jsx)(n.a,{href:"../development/flatpak",children:"Flatpak app from source."})," See more information about ",(0,s.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/issues/287",children:"Flatpak EOL."})]})})]})}function m(e={}){let{wrapper:n}={...(0,l.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(u,{...e})}):u(e)}}}]); \ No newline at end of file diff --git a/assets/js/c2e935db.098624a9.js b/assets/js/c2e935db.098624a9.js new file mode 100644 index 00000000..35f2d6e5 --- /dev/null +++ b/assets/js/c2e935db.098624a9.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["1785"],{8537(e,n,t){t.r(n),t.d(n,{metadata:()=>s,default:()=>h,frontMatter:()=>r,contentTitle:()=>c,toc:()=>l,assets:()=>a});var s=JSON.parse('{"id":"development/dev-common","title":"Common Development","description":"Common Development Knowledge","source":"@site/docs/development/dev-common.md","sourceDirName":"development","slug":"/development/dev-common","permalink":"/multi-scrobbler/development/dev-common","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/development/dev-common.md","tags":[],"version":"current","sidebarPosition":1,"frontMatter":{"toc_min_heading_level":2,"toc_max_heading_level":5,"sidebar_position":1,"title":"Common Development","description":"Common Development Knowledge"},"sidebar":"tutorialSidebar","previous":{"title":"Development","permalink":"/multi-scrobbler/category/development"},"next":{"title":"Source Development/Tutorial","permalink":"/multi-scrobbler/development/dev-source"}}'),i=t(4848),o=t(8453);let r={toc_min_heading_level:2,toc_max_heading_level:5,sidebar_position:1,title:"Common Development",description:"Common Development Knowledge"},c="Development",a={},l=[{value:"Architecture",id:"architecture",level:2},{value:"Project Setup",id:"project-setup",level:2},{value:"VSCode",id:"vscode",level:3},{value:"Common Development",id:"common-development",level:2},{value:"Config",id:"config",level:3},{value:"Concrete Class",id:"concrete-class",level:3},{value:"Stages",id:"stages",level:4},{value:"Stage: Build Data",id:"stage-build-data",level:5},{value:"Stage: Check Connection",id:"stage-check-connection",level:5},{value:"Stage: Test Auth",id:"stage-test-auth",level:5},{value:"Play Object",id:"play-object",level:3},{value:"Creating Clients and Sources",id:"creating-clients-and-sources",level:2},{value:"Profiling",id:"profiling",level:2}];function d(e){let n={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",h4:"h4",h5:"h5",header:"header",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,o.R)(),...e.components},{Details:t}=n;return t||function(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"development",children:"Development"})}),"\n",(0,i.jsx)(n.h2,{id:"architecture",children:"Architecture"}),"\n",(0,i.jsxs)(n.p,{children:["Multi-scrobbler is written entirely in ",(0,i.jsx)(n.a,{href:"https://www.typescriptlang.org/",children:"Typescript"}),". It consists of a backend and frontend. The backend handles all Source/Client logic, mounts web server endpoints that listen for Auth callbacks and Source ingress using ",(0,i.jsx)(n.a,{href:"https://expressjs.com/",children:"expressjs"}),", and serves the frontend. The frontend is a standalone ",(0,i.jsx)(n.a,{href:"https://vitejs.dev/",children:"Vitejs"})," app that communicates via API to the backend in order to render the dashboard."]}),"\n",(0,i.jsx)(n.h2,{id:"project-setup",children:"Project Setup"}),"\n",(0,i.jsxs)(n.p,{children:["Development requires ",(0,i.jsx)(n.a,{href:"https://nodejs.org/en",children:"Node v18.19.1"})," or higher is installed on your system."]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["When running locally (not with a devcontainer) you can use ",(0,i.jsx)(n.a,{href:"https://github.com/nvm-sh/nvm",children:"nvm"})," to manage the installed node version."]})}),"\n",(0,i.jsx)(n.p,{children:"Clone this repository somewhere and then install from the working directory"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"git clone https://github.com/FoxxMD/multi-scrobbler.git .\ncd multi-scrobbler\nnvm use # optional, sets correct node version when running without devcontainer\nnpm install\nnpm run start\n"})}),"\n",(0,i.jsx)(n.h3,{id:"vscode",children:"VSCode"}),"\n",(0,i.jsxs)(n.p,{children:["This repository contains ",(0,i.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/.devcontainer",children:"workspace settings"})," for development with VSCode. These include:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Run/Debug ",(0,i.jsx)(n.a,{href:"https://code.visualstudio.com/Docs/editor/debugging#_launch-configurations",children:"Launch configurations"})," for the application and tests"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://code.visualstudio.com/docs/devcontainers/containers",children:"Devcontainer"})," for development with all dependencies already installed"]}),"\n",(0,i.jsx)(n.li,{children:"Useful extensions for linting and running tests"}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:['To use the Devcontainer simple open the repository in VSCode and "Use Devcontainer" when the notification is presented. ',(0,i.jsx)(n.code,{children:"npm install"})," will be run when a new container is created."]}),"\n",(0,i.jsx)(n.h2,{id:"common-development",children:"Common Development"}),"\n",(0,i.jsx)(n.admonition,{type:"info",children:(0,i.jsxs)(n.p,{children:["In this document, when referring to aspects of Sources and Clients that are shared between both, the Source/Client will be referred to as a ",(0,i.jsx)(n.strong,{children:"Component."})]})}),"\n",(0,i.jsx)(n.p,{children:"A Component is composed of two parts:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Typescript interfaces describing structure of configuration for that Component"}),"\n",(0,i.jsx)(n.li,{children:'A concrete class inheriting from a common "startup" abstract class that enforces how the Component is built and operates'}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"In both parts Source/Clients share some common properties/behavior before diverging in how they operate."}),"\n",(0,i.jsx)(n.h3,{id:"config",children:"Config"}),"\n",(0,i.jsxs)(n.p,{children:["The configuration for a Component should always have this minimum shape, enforced respectively by the interfaces ",(0,i.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/infrastructure/config/source/index.ts#L105",children:"CommonSourceConfig"})," and ",(0,i.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/ce1c70a4e1e87fb5bea7cca960eaafbd15881a1f/src/backend/common/infrastructure/config/client/index.ts#L68",children:"CommonClientConfig"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"interface MyConfig {\n name: string\n data?: object\n options?: object\n}\n"})}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"data"})," contains data that is required for a Component to operate such as credentials, callback urls, api keys, endpoints, etc..."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"options"})," are ",(0,i.jsx)(n.strong,{children:"optional"})," settings that can be used to fine-tune the usage of the Component but are not required or do not majorly affect behavior. EX additional logging toggles"]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"concrete-class",children:"Concrete Class"}),"\n",(0,i.jsxs)(n.p,{children:["Components inherit from an abstract base class, ",(0,i.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/AbstractComponent.ts",children:(0,i.jsx)(n.code,{children:"AbstractComponent"})}),', that defines different "stages" of how a Component is built and initialized when MS first starts as well as when restarting the Component in the event it stops due to an error/network failure/etc...']}),"\n",(0,i.jsx)(n.h4,{id:"stages",children:"Stages"}),"\n",(0,i.jsx)(n.p,{children:"Stages below are invoked in the order listed. All stages are asynchronous to allow fetching network requests or reading files."}),"\n",(0,i.jsx)(n.p,{children:"The stage function (described in each stage below) should return a value or throw:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["return ",(0,i.jsx)(n.code,{children:"null"})," if the stage is not required"]}),"\n",(0,i.jsxs)(n.li,{children:["return ",(0,i.jsx)(n.code,{children:"true"})," if the stage succeeded"]}),"\n",(0,i.jsxs)(n.li,{children:["return a ",(0,i.jsx)(n.code,{children:"string"})," if the stage succeeded and you wish to append a result to the log output for this stage"]}),"\n",(0,i.jsxs)(n.li,{children:["throw an ",(0,i.jsx)(n.code,{children:"Exception"})," if the stage failed for any reason and the Component should not continue to run/start up"]}),"\n"]}),"\n",(0,i.jsx)(n.h5,{id:"stage-build-data",children:"Stage: Build Data"}),"\n",(0,i.jsx)(n.p,{children:"This stage should be used to validate user configuration, parse any additional data from async sources (file, network), and finalize the shape of any configuration/data needed for the Component to operate."}),"\n",(0,i.jsx)(n.admonition,{type:"info",children:(0,i.jsxs)(n.p,{children:["Implement ",(0,i.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/AbstractComponent.ts#L71",children:(0,i.jsx)(n.code,{children:"doBuildInitData"})})," in your child class to invoke this stage."]})}),"\n",(0,i.jsxs)(t,{children:[(0,i.jsx)("summary",{children:"Examples"}),(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Parse a full URL like ",(0,i.jsx)(n.code,{children:"http://SOME_IP:7000/subfolder/api"})," from user config containing a base url like ",(0,i.jsx)(n.code,{children:"data.baseUrl: 'SOME_IP'"})," and then store this in the class config"]}),"\n",(0,i.jsxs)(n.li,{children:["Validate that config ",(0,i.jsx)(n.code,{children:"data"})," contains required properties ",(0,i.jsx)(n.code,{children:"user"})," ",(0,i.jsx)(n.code,{children:"password"})," ",(0,i.jsx)(n.code,{children:"salt"})]}),"\n",(0,i.jsxs)(n.li,{children:["Read stored credentials from ",(0,i.jsx)(n.code,{children:"${this.configDir}/currentCreds-MySource-${name}.json"}),";"]}),"\n"]})]}),"\n",(0,i.jsx)(n.h5,{id:"stage-check-connection",children:"Stage: Check Connection"}),"\n",(0,i.jsx)(n.p,{children:"This stage is used to validate that MS can communicate with the service the Component is interacting with. This stage is invoked on MS startup as well as any time the Component tries to restart after a failure."}),"\n",(0,i.jsxs)(n.p,{children:["If the Component depends on ",(0,i.jsx)(n.strong,{children:"ingress"})," (like Jellyfin/Plex webhook) this stage is not necessary."]}),"\n",(0,i.jsx)(n.admonition,{type:"info",children:(0,i.jsxs)(n.p,{children:["Implement ",(0,i.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/AbstractComponent.ts#L103",children:(0,i.jsx)(n.code,{children:"doCheckConnection"})})," in your child class to invoke this stage."]})}),"\n",(0,i.jsxs)(t,{children:[(0,i.jsx)("summary",{children:"Examples"}),(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Make a ",(0,i.jsx)(n.a,{href:"https://nodejs.org/docs/latest-v18.x/api/http.html#httprequesturl-options-callback",children:(0,i.jsx)(n.code,{children:"request"})})," to the service's server to ensure it is accessible"]}),"\n",(0,i.jsx)(n.li,{children:"Open a websocket connection and check for a ping-pong"}),"\n"]})]}),"\n",(0,i.jsx)(n.h5,{id:"stage-test-auth",children:"Stage: Test Auth"}),"\n",(0,i.jsxs)(n.p,{children:["MS determines if Auth is required for a Component based on two class properties. You should set these properties during ",(0,i.jsx)(n.code,{children:"constructor"})," initialization for your Component class:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"requiresAuth"})," - (default ",(0,i.jsx)(n.code,{children:"false"}),") Set to ",(0,i.jsx)(n.code,{children:"true"})," if MS should check/test Auth for this Component"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"requiresAuthInteraction"})," - (default ",(0,i.jsx)(n.code,{children:"false"}),") Set to ",(0,i.jsx)(n.code,{children:"true"})," if user interaction is required to complete auth IE user needs to visit a callback URL"]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"If the Component requires authentication in order to communicate with a service then any required data should be built in this stage and a request made to the service to ensure the authentication data is valid."}),"\n",(0,i.jsx)(n.p,{children:"This stage should return:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"true"})," if auth succeeded"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"false"})," if auth failed without unexpected errors","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"IE the authentication data is not valid and requires user interaction to resolve the failure"}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.li,{children:"throw an exception if network failure or unexpected error occurred"}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["You ",(0,i.jsx)(n.em,{children:"should"})," attempt to re-authenticate, if possible. Only throw an exception or return ",(0,i.jsx)(n.code,{children:"false"})," if there is no way to recover from an authentication failure."]}),"\n",(0,i.jsx)(n.admonition,{type:"info",children:(0,i.jsxs)(n.p,{children:["Implement ",(0,i.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/AbstractComponent.ts#L111",children:(0,i.jsx)(n.code,{children:"doAuthentication"})})," in your child class to invoke this stage."]})}),"\n",(0,i.jsxs)(t,{children:[(0,i.jsx)("summary",{children:"Examples"}),(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Generate a Bearer Token for Basic Auth from user/password given in config and store in class properties"}),"\n",(0,i.jsx)(n.li,{children:"Make a request to a known endpoint with Authorization token from read credentials file to see if succeeds or returns 403"}),"\n",(0,i.jsx)(n.li,{children:"Catch a 403 and attempt to reauthenticate at an auth endpoint with user/password given in config"}),"\n"]})]}),"\n",(0,i.jsx)(n.h3,{id:"play-object",children:"Play Object"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.strong,{children:"PlayObject"})," is the standard data structure MS uses to store listen (track) information and data required for scrobbling. It consists of:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Track Data -- a standard format for storing track, artists, album, track duration, the date the track was played at, etc..."}),"\n",(0,i.jsxs)(n.li,{children:["Listen Metadata -- Optional but useful data related to the specific play or specifics about the Source/Client context for this play such as","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Platform specific ID, web URL to track, device/user ID that played this track, etc..."}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Both Sources and Clients use the ",(0,i.jsx)(n.strong,{children:"PlayObject"})," interface. When a Component receives track info from its corresponding service it must transform this data into a PlayObject before it can be interacted with."]}),"\n",(0,i.jsxs)(n.p,{children:["For more refer to the TS documentation for ",(0,i.jsx)(n.code,{children:"PlayObject"})," or ",(0,i.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/core/Atomic.ts#L141",children:(0,i.jsx)(n.code,{children:"AmbPlayObject"})})," in your project"]}),"\n",(0,i.jsx)(n.h2,{id:"creating-clients-and-sources",children:"Creating Clients and Sources"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.a,{href:"/multi-scrobbler/development/dev-source",children:"Source Development and Tutorial"})}),"\n",(0,i.jsx)(n.li,{children:"Client Development and Tutorial (TBD)"}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"profiling",children:"Profiling"}),"\n",(0,i.jsx)(n.p,{children:"Run tsx with inspect args"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"NODE_ENV=production node node_modules/.bin/tsx --inspect --heap-prof src/backend/index.ts\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Use ",(0,i.jsx)(n.code,{children:"chrome://inspect"})," from a chromium-based browser and attach to the running process, usually ",(0,i.jsx)(n.code,{children:"localhost:9229"})]}),"\n",(0,i.jsx)(n.p,{children:"From the opened DevTools window use Performance or Memory to profile the running process."})]})}function h(e={}){let{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}}}]); \ No newline at end of file diff --git a/assets/js/c2e935db.f80b2104.js b/assets/js/c2e935db.f80b2104.js deleted file mode 100644 index 1403d04a..00000000 --- a/assets/js/c2e935db.f80b2104.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["1785"],{8537(e,n,t){t.r(n),t.d(n,{metadata:()=>s,default:()=>h,frontMatter:()=>o,contentTitle:()=>c,toc:()=>l,assets:()=>a});var s=JSON.parse('{"id":"development/dev-common","title":"Common Development","description":"Start here for MS development","source":"@site/docs/development/dev-common.md","sourceDirName":"development","slug":"/development/dev-common","permalink":"/multi-scrobbler/development/dev-common","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/development/dev-common.md","tags":[],"version":"current","sidebarPosition":1,"frontMatter":{"toc_min_heading_level":2,"toc_max_heading_level":5,"sidebar_position":1,"title":"Common Development","description":"Start here for MS development"},"sidebar":"tutorialSidebar","previous":{"title":"Development","permalink":"/multi-scrobbler/category/development"},"next":{"title":"Source Development/Tutorial","permalink":"/multi-scrobbler/development/dev-source"}}'),i=t(4848),r=t(8453);let o={toc_min_heading_level:2,toc_max_heading_level:5,sidebar_position:1,title:"Common Development",description:"Start here for MS development"},c="Development",a={},l=[{value:"Architecture",id:"architecture",level:2},{value:"Project Setup",id:"project-setup",level:2},{value:"VSCode",id:"vscode",level:3},{value:"Common Development",id:"common-development",level:2},{value:"Config",id:"config",level:3},{value:"Concrete Class",id:"concrete-class",level:3},{value:"Stages",id:"stages",level:4},{value:"Stage: Build Data",id:"stage-build-data",level:5},{value:"Stage: Check Connection",id:"stage-check-connection",level:5},{value:"Stage: Test Auth",id:"stage-test-auth",level:5},{value:"Play Object",id:"play-object",level:3},{value:"Creating Clients and Sources",id:"creating-clients-and-sources",level:2},{value:"Profiling",id:"profiling",level:2}];function d(e){let n={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",h4:"h4",h5:"h5",header:"header",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components},{Details:t}=n;return t||function(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"development",children:"Development"})}),"\n",(0,i.jsx)(n.h2,{id:"architecture",children:"Architecture"}),"\n",(0,i.jsxs)(n.p,{children:["Multi-scrobbler is written entirely in ",(0,i.jsx)(n.a,{href:"https://www.typescriptlang.org/",children:"Typescript"}),". It consists of a backend and frontend. The backend handles all Source/Client logic, mounts web server endpoints that listen for Auth callbacks and Source ingress using ",(0,i.jsx)(n.a,{href:"https://expressjs.com/",children:"expressjs"}),", and serves the frontend. The frontend is a standalone ",(0,i.jsx)(n.a,{href:"https://vitejs.dev/",children:"Vitejs"})," app that communicates via API to the backend in order to render the dashboard."]}),"\n",(0,i.jsx)(n.h2,{id:"project-setup",children:"Project Setup"}),"\n",(0,i.jsxs)(n.p,{children:["Development requires ",(0,i.jsx)(n.a,{href:"https://nodejs.org/en",children:"Node v18.19.1"})," or higher is installed on your system."]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["When running locally (not with a devcontainer) you can use ",(0,i.jsx)(n.a,{href:"https://github.com/nvm-sh/nvm",children:"nvm"})," to manage the installed node version."]})}),"\n",(0,i.jsx)(n.p,{children:"Clone this repository somewhere and then install from the working directory"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"git clone https://github.com/FoxxMD/multi-scrobbler.git .\ncd multi-scrobbler\nnvm use # optional, sets correct node version when running without devcontainer\nnpm install\nnpm run start\n"})}),"\n",(0,i.jsx)(n.h3,{id:"vscode",children:"VSCode"}),"\n",(0,i.jsxs)(n.p,{children:["This repository contains ",(0,i.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/.devcontainer",children:"workspace settings"})," for development with VSCode. These include:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Run/Debug ",(0,i.jsx)(n.a,{href:"https://code.visualstudio.com/Docs/editor/debugging#_launch-configurations",children:"Launch configurations"})," for the application and tests"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://code.visualstudio.com/docs/devcontainers/containers",children:"Devcontainer"})," for development with all dependencies already installed"]}),"\n",(0,i.jsx)(n.li,{children:"Useful extensions for linting and running tests"}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:['To use the Devcontainer simple open the repository in VSCode and "Use Devcontainer" when the notification is presented. ',(0,i.jsx)(n.code,{children:"npm install"})," will be run when a new container is created."]}),"\n",(0,i.jsx)(n.h2,{id:"common-development",children:"Common Development"}),"\n",(0,i.jsx)(n.admonition,{type:"info",children:(0,i.jsxs)(n.p,{children:["In this document, when referring to aspects of Sources and Clients that are shared between both, the Source/Client will be referred to as a ",(0,i.jsx)(n.strong,{children:"Component."})]})}),"\n",(0,i.jsx)(n.p,{children:"A Component is composed of two parts:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Typescript interfaces describing structure of configuration for that Component"}),"\n",(0,i.jsx)(n.li,{children:'A concrete class inheriting from a common "startup" abstract class that enforces how the Component is built and operates'}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"In both parts Source/Clients share some common properties/behavior before diverging in how they operate."}),"\n",(0,i.jsx)(n.h3,{id:"config",children:"Config"}),"\n",(0,i.jsxs)(n.p,{children:["The configuration for a Component should always have this minimum shape, enforced respectively by the interfaces ",(0,i.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/infrastructure/config/source/index.ts#L105",children:"CommonSourceConfig"})," and ",(0,i.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/ce1c70a4e1e87fb5bea7cca960eaafbd15881a1f/src/backend/common/infrastructure/config/client/index.ts#L68",children:"CommonClientConfig"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"interface MyConfig {\n name: string\n data?: object\n options?: object\n}\n"})}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"data"})," contains data that is required for a Component to operate such as credentials, callback urls, api keys, endpoints, etc..."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"options"})," are ",(0,i.jsx)(n.strong,{children:"optional"})," settings that can be used to fine-tune the usage of the Component but are not required or do not majorly affect behavior. EX additional logging toggles"]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"concrete-class",children:"Concrete Class"}),"\n",(0,i.jsxs)(n.p,{children:["Components inherit from an abstract base class, ",(0,i.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/AbstractComponent.ts",children:(0,i.jsx)(n.code,{children:"AbstractComponent"})}),', that defines different "stages" of how a Component is built and initialized when MS first starts as well as when restarting the Component in the event it stops due to an error/network failure/etc...']}),"\n",(0,i.jsx)(n.h4,{id:"stages",children:"Stages"}),"\n",(0,i.jsx)(n.p,{children:"Stages below are invoked in the order listed. All stages are asynchronous to allow fetching network requests or reading files."}),"\n",(0,i.jsx)(n.p,{children:"The stage function (described in each stage below) should return a value or throw:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["return ",(0,i.jsx)(n.code,{children:"null"})," if the stage is not required"]}),"\n",(0,i.jsxs)(n.li,{children:["return ",(0,i.jsx)(n.code,{children:"true"})," if the stage succeeded"]}),"\n",(0,i.jsxs)(n.li,{children:["return a ",(0,i.jsx)(n.code,{children:"string"})," if the stage succeeded and you wish to append a result to the log output for this stage"]}),"\n",(0,i.jsxs)(n.li,{children:["throw an ",(0,i.jsx)(n.code,{children:"Exception"})," if the stage failed for any reason and the Component should not continue to run/start up"]}),"\n"]}),"\n",(0,i.jsx)(n.h5,{id:"stage-build-data",children:"Stage: Build Data"}),"\n",(0,i.jsx)(n.p,{children:"This stage should be used to validate user configuration, parse any additional data from async sources (file, network), and finalize the shape of any configuration/data needed for the Component to operate."}),"\n",(0,i.jsx)(n.admonition,{type:"info",children:(0,i.jsxs)(n.p,{children:["Implement ",(0,i.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/AbstractComponent.ts#L71",children:(0,i.jsx)(n.code,{children:"doBuildInitData"})})," in your child class to invoke this stage."]})}),"\n",(0,i.jsxs)(t,{children:[(0,i.jsx)("summary",{children:"Examples"}),(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Parse a full URL like ",(0,i.jsx)(n.code,{children:"http://SOME_IP:7000/subfolder/api"})," from user config containing a base url like ",(0,i.jsx)(n.code,{children:"data.baseUrl: 'SOME_IP'"})," and then store this in the class config"]}),"\n",(0,i.jsxs)(n.li,{children:["Validate that config ",(0,i.jsx)(n.code,{children:"data"})," contains required properties ",(0,i.jsx)(n.code,{children:"user"})," ",(0,i.jsx)(n.code,{children:"password"})," ",(0,i.jsx)(n.code,{children:"salt"})]}),"\n",(0,i.jsxs)(n.li,{children:["Read stored credentials from ",(0,i.jsx)(n.code,{children:"${this.configDir}/currentCreds-MySource-${name}.json"}),";"]}),"\n"]})]}),"\n",(0,i.jsx)(n.h5,{id:"stage-check-connection",children:"Stage: Check Connection"}),"\n",(0,i.jsx)(n.p,{children:"This stage is used to validate that MS can communicate with the service the Component is interacting with. This stage is invoked on MS startup as well as any time the Component tries to restart after a failure."}),"\n",(0,i.jsxs)(n.p,{children:["If the Component depends on ",(0,i.jsx)(n.strong,{children:"ingress"})," (like Jellyfin/Plex webhook) this stage is not necessary."]}),"\n",(0,i.jsx)(n.admonition,{type:"info",children:(0,i.jsxs)(n.p,{children:["Implement ",(0,i.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/AbstractComponent.ts#L103",children:(0,i.jsx)(n.code,{children:"doCheckConnection"})})," in your child class to invoke this stage."]})}),"\n",(0,i.jsxs)(t,{children:[(0,i.jsx)("summary",{children:"Examples"}),(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Make a ",(0,i.jsx)(n.a,{href:"https://nodejs.org/docs/latest-v18.x/api/http.html#httprequesturl-options-callback",children:(0,i.jsx)(n.code,{children:"request"})})," to the service's server to ensure it is accessible"]}),"\n",(0,i.jsx)(n.li,{children:"Open a websocket connection and check for a ping-pong"}),"\n"]})]}),"\n",(0,i.jsx)(n.h5,{id:"stage-test-auth",children:"Stage: Test Auth"}),"\n",(0,i.jsxs)(n.p,{children:["MS determines if Auth is required for a Component based on two class properties. You should set these properties during ",(0,i.jsx)(n.code,{children:"constructor"})," initialization for your Component class:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"requiresAuth"})," - (default ",(0,i.jsx)(n.code,{children:"false"}),") Set to ",(0,i.jsx)(n.code,{children:"true"})," if MS should check/test Auth for this Component"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"requiresAuthInteraction"})," - (default ",(0,i.jsx)(n.code,{children:"false"}),") Set to ",(0,i.jsx)(n.code,{children:"true"})," if user interaction is required to complete auth IE user needs to visit a callback URL"]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"If the Component requires authentication in order to communicate with a service then any required data should be built in this stage and a request made to the service to ensure the authentication data is valid."}),"\n",(0,i.jsx)(n.p,{children:"This stage should return:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"true"})," if auth succeeded"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"false"})," if auth failed without unexpected errors","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"IE the authentication data is not valid and requires user interaction to resolve the failure"}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.li,{children:"throw an exception if network failure or unexpected error occurred"}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["You ",(0,i.jsx)(n.em,{children:"should"})," attempt to re-authenticate, if possible. Only throw an exception or return ",(0,i.jsx)(n.code,{children:"false"})," if there is no way to recover from an authentication failure."]}),"\n",(0,i.jsx)(n.admonition,{type:"info",children:(0,i.jsxs)(n.p,{children:["Implement ",(0,i.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/AbstractComponent.ts#L111",children:(0,i.jsx)(n.code,{children:"doAuthentication"})})," in your child class to invoke this stage."]})}),"\n",(0,i.jsxs)(t,{children:[(0,i.jsx)("summary",{children:"Examples"}),(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Generate a Bearer Token for Basic Auth from user/password given in config and store in class properties"}),"\n",(0,i.jsx)(n.li,{children:"Make a request to a known endpoint with Authorization token from read credentials file to see if succeeds or returns 403"}),"\n",(0,i.jsx)(n.li,{children:"Catch a 403 and attempt to reauthenticate at an auth endpoint with user/password given in config"}),"\n"]})]}),"\n",(0,i.jsx)(n.h3,{id:"play-object",children:"Play Object"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.strong,{children:"PlayObject"})," is the standard data structure MS uses to store listen (track) information and data required for scrobbling. It consists of:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Track Data -- a standard format for storing track, artists, album, track duration, the date the track was played at, etc..."}),"\n",(0,i.jsxs)(n.li,{children:["Listen Metadata -- Optional but useful data related to the specific play or specifics about the Source/Client context for this play such as","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Platform specific ID, web URL to track, device/user ID that played this track, etc..."}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Both Sources and Clients use the ",(0,i.jsx)(n.strong,{children:"PlayObject"})," interface. When a Component receives track info from its corresponding service it must transform this data into a PlayObject before it can be interacted with."]}),"\n",(0,i.jsxs)(n.p,{children:["For more refer to the TS documentation for ",(0,i.jsx)(n.code,{children:"PlayObject"})," or ",(0,i.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/core/Atomic.ts#L141",children:(0,i.jsx)(n.code,{children:"AmbPlayObject"})})," in your project"]}),"\n",(0,i.jsx)(n.h2,{id:"creating-clients-and-sources",children:"Creating Clients and Sources"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.a,{href:"/multi-scrobbler/development/dev-source",children:"Source Development and Tutorial"})}),"\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.a,{href:"/multi-scrobbler/development/dev-client",children:"Client Development and Tutorial"})}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"profiling",children:"Profiling"}),"\n",(0,i.jsx)(n.p,{children:"Run tsx with inspect args"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"NODE_ENV=production node node_modules/.bin/tsx --inspect --heap-prof src/backend/index.ts\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Use ",(0,i.jsx)(n.code,{children:"chrome://inspect"})," from a chromium-based browser and attach to the running process, usually ",(0,i.jsx)(n.code,{children:"localhost:9229"})]}),"\n",(0,i.jsx)(n.p,{children:"From the opened DevTools window use Performance or Memory to profile the running process."})]})}function h(e={}){let{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}}}]); \ No newline at end of file diff --git a/assets/js/d3a18f08.3b64d6e7.js b/assets/js/d3a18f08.3b64d6e7.js new file mode 100644 index 00000000..356ea60e --- /dev/null +++ b/assets/js/d3a18f08.3b64d6e7.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["8037"],{8106(e,r,i){i.r(r),i.d(r,{metadata:()=>t,default:()=>h,frontMatter:()=>a,contentTitle:()=>d,toc:()=>u,assets:()=>c});var t=JSON.parse('{"id":"updating/updating","title":"Overview","description":"Updating","source":"@site/docs/updating/updating.mdx","sourceDirName":"updating","slug":"/updating/","permalink":"/multi-scrobbler/updating/","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/updating/updating.mdx","tags":[],"version":"current","sidebarPosition":1,"frontMatter":{"sidebar_position":1,"title":"Overview","description":"Updating"},"sidebar":"tutorialSidebar","previous":{"title":"Updating","permalink":"/multi-scrobbler/category/updating"},"next":{"title":"Overview","permalink":"/multi-scrobbler/configuration/"}}'),n=i(4848),s=i(8453),l=i(8194),o=i(698);i(2170);let a={sidebar_position:1,title:"Overview",description:"Updating"},d,c={},u=[{value:"Updating",id:"updating",level:2},{value:"Docker",id:"docker",level:3},{value:"Nodejs",id:"nodejs",level:3}];function p(e){let r={a:"a",code:"code",em:"em",h2:"h2",h3:"h3",p:"p",pre:"pre",...(0,s.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(r.h2,{id:"updating",children:"Updating"}),"\n",(0,n.jsx)(r.p,{children:"Currently, multi-scrobbler does not have any databases or dependencies that require additional interaction/consideration when updating."}),"\n",(0,n.jsxs)(r.p,{children:["The majority of major/minor version releases introduce only ",(0,n.jsx)(r.em,{children:"new"})," functionality but occasionally may also include breaking changes in behavior or configuration. These changes, and how to update configs accordingly, are detailed in ",(0,n.jsx)(r.a,{href:"https://github.com/FoxxMD/multi-scrobbler/releases",children:"Github Release Notes"}),". It is recommended to check this page before upgrading."]}),"\n",(0,n.jsxs)(r.p,{children:["Alternatively, multi-scrobbler version can be pinned using ",(0,n.jsx)(r.a,{href:"https://github.com/FoxxMD/multi-scrobbler/tags",children:"git tags"})," or docker image tags that match release versions IE ",(0,n.jsx)(r.code,{children:"foxxmd/multi-scrobbler:0.9.7"})]}),"\n",(0,n.jsx)(r.h3,{id:"docker",children:"Docker"}),"\n",(0,n.jsxs)(l.A,{groupId:"runType",queryString:!0,children:[(0,n.jsxs)(o.A,{value:"docker",label:"Docker",children:[(0,n.jsx)(r.pre,{children:(0,n.jsx)(r.code,{className:"language-shell",children:"docker image pull foxxmd/multi-scrobbler:latest\ndocker container stop multi-scrobbler\ndocker container rm multi-scrobbler\ndocker run --name multi-scrobbler ...\n"})}),(0,n.jsxs)(r.p,{children:["The last command is the ",(0,n.jsx)(r.a,{href:"/multi-scrobbler/installation/?runType=docker#docker-usage-example",children:"same as the command used to initialy run multi-scrobbler."})]})]}),(0,n.jsx)(o.A,{value:"docker-compose",label:"Docker Compose",children:(0,n.jsx)(r.pre,{children:(0,n.jsx)(r.code,{className:"language-shell",children:"docker compose pull\ndocker compose up -d\n"})})})]}),"\n",(0,n.jsx)(r.h3,{id:"nodejs",children:"Nodejs"}),"\n",(0,n.jsxs)(r.p,{children:["Assuming the ",(0,n.jsxs)(r.a,{href:"/multi-scrobbler/installation/#nodejs",children:["repository was used for initial installation with ",(0,n.jsx)(r.code,{children:"git clone"})]}),", update by pulling the latest commit:"]}),"\n",(0,n.jsx)(r.pre,{children:(0,n.jsx)(r.code,{children:"git pull\n"})}),"\n",(0,n.jsx)(r.p,{children:"and restart multi-scrobbler."})]})}function h(e={}){let{wrapper:r}={...(0,s.R)(),...e.components};return r?(0,n.jsx)(r,{...e,children:(0,n.jsx)(p,{...e})}):p(e)}}}]); \ No newline at end of file diff --git a/assets/js/d3a18f08.5790858b.js b/assets/js/d3a18f08.5790858b.js deleted file mode 100644 index 69683f50..00000000 --- a/assets/js/d3a18f08.5790858b.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["8037"],{8106(e,r,i){i.r(r),i.d(r,{metadata:()=>t,default:()=>h,frontMatter:()=>a,contentTitle:()=>c,toc:()=>u,assets:()=>d});var t=JSON.parse('{"id":"updating/updating","title":"Overview","description":"Updating","source":"@site/docs/updating/updating.mdx","sourceDirName":"updating","slug":"/updating/","permalink":"/multi-scrobbler/updating/","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/updating/updating.mdx","tags":[],"version":"current","sidebarPosition":1,"frontMatter":{"sidebar_position":1,"title":"Overview"},"sidebar":"tutorialSidebar","previous":{"title":"Updating","permalink":"/multi-scrobbler/category/updating"},"next":{"title":"Overview","permalink":"/multi-scrobbler/configuration/"}}'),n=i(4848),s=i(8453),l=i(8194),o=i(698);i(2170);let a={sidebar_position:1,title:"Overview"},c,d={},u=[{value:"Updating",id:"updating",level:2},{value:"Docker",id:"docker",level:3},{value:"Nodejs",id:"nodejs",level:3}];function p(e){let r={a:"a",code:"code",em:"em",h2:"h2",h3:"h3",p:"p",pre:"pre",...(0,s.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(r.h2,{id:"updating",children:"Updating"}),"\n",(0,n.jsx)(r.p,{children:"Currently, multi-scrobbler does not have any databases or dependencies that require additional interaction/consideration when updating."}),"\n",(0,n.jsxs)(r.p,{children:["The majority of major/minor version releases introduce only ",(0,n.jsx)(r.em,{children:"new"})," functionality but occasionally may also include breaking changes in behavior or configuration. These changes, and how to update configs accordingly, are detailed in ",(0,n.jsx)(r.a,{href:"https://github.com/FoxxMD/multi-scrobbler/releases",children:"Github Release Notes"}),". It is recommended to check this page before upgrading."]}),"\n",(0,n.jsxs)(r.p,{children:["Alternatively, multi-scrobbler version can be pinned using ",(0,n.jsx)(r.a,{href:"https://github.com/FoxxMD/multi-scrobbler/tags",children:"git tags"})," or docker image tags that match release versions IE ",(0,n.jsx)(r.code,{children:"foxxmd/multi-scrobbler:0.9.7"})]}),"\n",(0,n.jsx)(r.h3,{id:"docker",children:"Docker"}),"\n",(0,n.jsxs)(l.A,{groupId:"runType",queryString:!0,children:[(0,n.jsxs)(o.A,{value:"docker",label:"Docker",children:[(0,n.jsx)(r.pre,{children:(0,n.jsx)(r.code,{className:"language-shell",children:"docker image pull foxxmd/multi-scrobbler:latest\ndocker container stop multi-scrobbler\ndocker container rm multi-scrobbler\ndocker run --name multi-scrobbler ...\n"})}),(0,n.jsxs)(r.p,{children:["The last command is the ",(0,n.jsx)(r.a,{href:"/multi-scrobbler/installation/?runType=docker#docker-usage-example",children:"same as the command used to initialy run multi-scrobbler."})]})]}),(0,n.jsx)(o.A,{value:"docker-compose",label:"Docker Compose",children:(0,n.jsx)(r.pre,{children:(0,n.jsx)(r.code,{className:"language-shell",children:"docker compose pull\ndocker compose up -d\n"})})})]}),"\n",(0,n.jsx)(r.h3,{id:"nodejs",children:"Nodejs"}),"\n",(0,n.jsxs)(r.p,{children:["Assuming the ",(0,n.jsxs)(r.a,{href:"/multi-scrobbler/installation/#nodejs",children:["repository was used for initial installation with ",(0,n.jsx)(r.code,{children:"git clone"})]}),", update by pulling the latest commit:"]}),"\n",(0,n.jsx)(r.pre,{children:(0,n.jsx)(r.code,{children:"git pull\n"})}),"\n",(0,n.jsx)(r.p,{children:"and restart multi-scrobbler."})]})}function h(e={}){let{wrapper:r}={...(0,s.R)(),...e.components};return r?(0,n.jsx)(r,{...e,children:(0,n.jsx)(p,{...e})}):p(e)}}}]); \ No newline at end of file diff --git a/assets/js/dfa6d5b1.4faf8228.js b/assets/js/dfa6d5b1.4faf8228.js deleted file mode 100644 index 28196e68..00000000 --- a/assets/js/dfa6d5b1.4faf8228.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["539"],{3743(e,n,s){s.r(n),s.d(n,{metadata:()=>i,default:()=>h,frontMatter:()=>o,contentTitle:()=>a,toc:()=>c,assets:()=>l});var i=JSON.parse('{"id":"configuration/transforms/native","title":"Native Stage","description":"The Native Stage uses built-in heuristics to try to extract Artists from Play artist/track data.","source":"@site/docs/configuration/transforms/native.mdx","sourceDirName":"configuration/transforms","slug":"/configuration/transforms/native","permalink":"/multi-scrobbler/configuration/transforms/native","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/transforms/native.mdx","tags":[],"version":"current","frontMatter":{"title":"Native Stage","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Musicbrainz Stage","permalink":"/multi-scrobbler/configuration/transforms/musicbrainz"},"next":{"title":"User Stage","permalink":"/multi-scrobbler/configuration/transforms/user"}}'),t=s(4848),r=s(8453);let o={title:"Native Stage",toc_min_heading_level:2,toc_max_heading_level:5},a,l={},c=[{value:"Configuration",id:"configuration",level:2},{value:"Rules",id:"rules",level:3},{value:"Default Stage",id:"default-stage",level:3},{value:"ENV Configuration",id:"env-configuration",level:2},{value:"Examples",id:"examples",level:2},{value:"Parse only artists string using a custom delimiter",id:"parse-only-artists-string-using-a-custom-delimiter",level:3},{value:"Don't parse specific artists",id:"dont-parse-specific-artists",level:3},{value:"Extract Artists from Title but do not modify Title",id:"extract-artists-from-title-but-do-not-modify-title",level:3}];function d(e){let n={a:"a",code:"code",em:"em",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components},{Details:s}=n;return s||function(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.strong,{children:"Native"})," ",(0,t.jsx)(n.a,{href:"/configuration/transforms#stage",children:"Stage"})," uses ",(0,t.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/tests/plays/playParsing.test.ts",children:"built-in heuristics"})," to try to extract Artists from Play artist/track data."]}),"\n",(0,t.jsx)(n.p,{children:"This Stage is most useful for Sources that report limited data such as:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," - Reports Artists as a single string"]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"A non-exhaustive list of heuristics:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["Splits artists in artist string using common delimiters EX ",(0,t.jsx)(n.code,{children:"Foo Artist, Bar Guy, Baz Band - My Cool Song"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["Does not split artists with ",(0,t.jsx)(n.code,{children:"&"})," in name, if other delimiters are present"]}),"\n",(0,t.jsx)(n.li,{children:"Does not split artist name when only one delimiter is present"}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["Splits artists on common joiner phrases (ft. feat. vs. etc...)","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["Extracts artists from Play title using joiner phrases EX ",(0,t.jsx)(n.code,{children:"My Cool Song (feat. SomeGuy)"})]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"A practical example of what the Native Stage will do to Play data:"}),"\n",(0,t.jsxs)(n.p,{children:["This Scrobble from ",(0,t.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility (feat. Wheatus)",\n "artists": ["Bowling For Soup & Punk Rock Factory"]\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"Will be transformed into this"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility",\n "artists": ["Bowling For Soup", "Punk Rock Factory", "Wheatus"]\n}\n'})}),"\n",(0,t.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsxs)(n.p,{children:["Available properties for ",(0,t.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"}),":"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"delimitersExtra"})," - A list of string characters that should be considered delimiters for artists ",(0,t.jsx)(n.strong,{children:"in addition to"})," multi-scrobbler's default list (",(0,t.jsx)(n.code,{children:", / \\ "}),")"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"delimiters"})," - A list of string characters that should be considered delimiters for artists.","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Replaces"})," all delimiters (MS not use any defaults, only what you give it)"]}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"artistsIgnore"})," - a list of strings and/or regular expressions. Any monolothic artist string that matches from the list ",(0,t.jsx)(n.em,{children:"will not be modified."})]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"artistsParseFrom"})," a list of the properties that should be used to try to extract artists. Can be ",(0,t.jsx)(n.code,{children:"artists"})," ",(0,t.jsx)(n.code,{children:"title"})," or both. Defaults to both when not provided in options ( ",(0,t.jsx)(n.code,{children:'["artists", "title"]'})," )","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["When ",(0,t.jsx)(n.code,{children:"artists"})," is present it tries to extract additional artists from artist strings"]}),"\n",(0,t.jsxs)(n.li,{children:["When ",(0,t.jsx)(n.code,{children:"title"})," is present it tries to extract artists from ft. feat. vs. etc... found in the track title"]}),"\n",(0,t.jsxs)(n.li,{children:["Importantly, if ",(0,t.jsx)(n.code,{children:"artists"})," is ",(0,t.jsx)(n.em,{children:"not"})," present in ",(0,t.jsx)(n.code,{children:"artistsParseFrom"})," then ",(0,t.jsx)(n.em,{children:"no artists"})," are used at all (only those from ",(0,t.jsx)(n.code,{children:"title"}),", if present)"]}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"artistsParseMonolithicOnly"})," - boolean value, defaults to ",(0,t.jsx)(n.code,{children:"true"}),". When ",(0,t.jsx)(n.code,{children:"true"})," native tranformer will only attempt to extract artists if the scrobble data has ",(0,t.jsx)(n.em,{children:"only one string for artist"})," (pre-transform)","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["This means that, for Sources like Spotify/Jellyfin/etc. that provide proper lists of artists in their data ",(0,t.jsx)(n.em,{children:"and the list has more than one string"}),", it will not try to extract artists from their strings"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(s,{children:[(0,t.jsxs)("summary",{children:["Example of the behavior for ",(0,t.jsx)(n.code,{children:"artistsParseFrom"})]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"The Foos, The Bars - My Cool Track (ft. Frank)\n"})}),(0,t.jsx)(n.p,{children:"Config => extracted artists"}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:'"artistsParseFrom": ["artists", "title"] => The Foos, The Bars, Frank\n"artistsParseFrom": ["artists"] => The Foos, The Bars\n"artistsParseFrom": ["title"] => Frank\n"artistsParseFrom": [] =>\n'})})]}),"\n",(0,t.jsx)(n.h3,{id:"rules",children:"Rules"}),"\n",(0,t.jsxs)(n.p,{children:["Each ",(0,t.jsx)(n.a,{href:"/configuration/transforms#stage-rules",children:"Rule"})," should be either a boolean, specifying if the transformed data should be used for this field, or a ",(0,t.jsxs)(n.a,{href:"/configuration/transforms#conditional-modification",children:[(0,t.jsx)(n.code,{children:"when"})," condition."]}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json5",children:'{\n "type": "native",\n // ...\n "title": false, // will not apply any changes to Play title\n "artists": {\n "when": {/* ... */}, // will only apply changes to Play artists if "when" is satisfied\n /* ... */\n },\n "album": true // will always apply changes to Play album\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["If a rule is not present then multi-scrobbler defaults it to ",(0,t.jsx)(n.code,{children:"true"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"default-stage",children:"Default Stage"}),"\n",(0,t.jsxs)(n.p,{children:["For your convenience, if you do not define any Native ",(0,t.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configurations"})," then multi-scrobbler automatically builds one using the defaults shown in ",(0,t.jsx)(n.a,{href:"#configuration",children:"configuration"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["This can be used by omitting the ",(0,t.jsx)(n.code,{children:"name"})," in your ",(0,t.jsx)(n.a,{href:"/configuration/transforms#stage",children:"modification Stage"}),"."]}),"\n",(0,t.jsxs)(s,{children:[(0,t.jsx)("summary",{children:"Example"}),(0,t.jsxs)(n.p,{children:["In a ",(0,t.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," ",(0,t.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "native",\n // do not include name, the default native stage will be used\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,t.jsx)(n.h2,{id:"env-configuration",children:"ENV Configuration"}),"\n",(0,t.jsxs)(n.p,{children:["The default configuration shown above can also be applied using ",(0,t.jsx)(n.a,{href:"/configuration?configType=env#configuration-types",children:"ENV Config"})," for individual Sources/Clients."]}),"\n",(0,t.jsxs)(n.p,{children:["Use ENV ",(0,t.jsx)(n.code,{children:"*_TRANSFORMS=native"})," on each Source/Client you wish to apply this stage to. This applies the stage in the ",(0,t.jsxs)(n.a,{href:"/configuration/transforms#lifecycle-hooks",children:[(0,t.jsx)(n.code,{children:"preTransform"})," Hook"]})," with all ",(0,t.jsx)(n.a,{href:"#rules",children:"Rules"})," enabled."]}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"*"})," stands for the prefix used for each Source/Client's ENV keys. Refer to the individual Source/Client Configuration sections to find this. Example"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["All ",(0,t.jsx)(n.a,{href:"/configuration/sources/subsonic/",children:"Subsonic Source"})," ENVs look like ",(0,t.jsx)(n.code,{children:"SUBSONIC_USER=myuser"})," etc..."]}),"\n",(0,t.jsxs)(n.li,{children:["Use ",(0,t.jsx)(n.code,{children:"SUBSONIC_TRANSFORMS=native"})]}),"\n"]}),"\n",(0,t.jsxs)(s,{children:[(0,t.jsx)("summary",{children:"Example Full Docker Deploy with ENV Configuration"}),(0,t.jsxs)(n.p,{children:["Using ",(0,t.jsx)(n.a,{href:"/quickstart#create-docker-compose-file",children:"Jellyfin + Maloja"})," example from Quickstart:"]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:'services:\n multi-scrobbler:\n image: foxxmd/multi-scrobbler\n container_name: multi-scrobbler\n environment:\n - JELLYFIN_URL=192.168.0.110:8096\n - JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56b\n - JELLYFIN_USER=MyUser\n - JELLYFIN_TRANSFORMS=native # applies native Stage to preTransform of Jellyfin source\n\n - MALOJA_URL=http://192.168.0.100:42010 # maloja receives enhanced scrobble from Jellyfin\n - MALOJA_API_KEY=myApiKey\n\n\n volumes:\n - "./config:/config"\n ports:\n - "9078:9078"\n restart: unless-stopped\n'})})]}),"\n",(0,t.jsx)(n.h2,{id:"examples",children:"Examples"}),"\n",(0,t.jsx)(n.h3,{id:"parse-only-artists-string-using-a-custom-delimiter",children:"Parse only artists string using a custom delimiter"}),"\n",(0,t.jsxs)(s,{children:[(0,t.jsx)("summary",{children:"Example"}),(0,t.jsxs)(n.p,{children:["Your ",(0,t.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "native",\n "name": "MyNativeTransformer",\n "defaults": {\n // default delimiters when this Stage is used in a hook\n "delimiters": [\n "\u2022"\n ],\n // default delimiters when this Stage is used in a hook\n "artistsParseFrom": ["artists"]\n }\n }\n ]\n}\n'})}),(0,t.jsxs)(n.p,{children:["In a ",(0,t.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," ",(0,t.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "native",\n "name": "MyNativeTransformer"\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,t.jsx)(n.h3,{id:"dont-parse-specific-artists",children:"Don't parse specific artists"}),"\n",(0,t.jsxs)(s,{children:[(0,t.jsx)("summary",{children:"Example"}),(0,t.jsxs)(n.p,{children:["Your ",(0,t.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "native",\n "name": "MyNativeTransformer",\n "defaults": {\n "artistsIgnore": [\n "Crosby, Stills, Nash & Young",\n "Polo & Pan"\n ]\n }\n }\n ]\n}\n'})}),(0,t.jsxs)(n.p,{children:["In a ",(0,t.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," ",(0,t.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "native",\n "name": "MyNativeTransformer"\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,t.jsx)(n.h3,{id:"extract-artists-from-title-but-do-not-modify-title",children:"Extract Artists from Title but do not modify Title"}),"\n",(0,t.jsxs)(n.p,{children:["In the scenario where you want to extract Artists from Track titles with joiners like ",(0,t.jsx)(n.code,{children:"My Cool Track (feat. Foobar)"})," but want to keep the title as-is:"]}),"\n",(0,t.jsxs)(s,{children:[(0,t.jsx)("summary",{children:"Example"}),(0,t.jsxs)(n.p,{children:["No additional ",(0,t.jsx)(n.a,{href:"#configuration",children:"stage configuration"})," is required so we can use the ",(0,t.jsx)(n.a,{href:"#default-stage",children:"default Stage"}),"."]}),(0,t.jsxs)(n.p,{children:["In a ",(0,t.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," ",(0,t.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "native",\n "title": false // do not modify title\n }\n ]\n }\n }\n }\n]\n'})})]})]})}function h(e={}){let{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}}}]); \ No newline at end of file diff --git a/assets/js/dfa6d5b1.738dc2f2.js b/assets/js/dfa6d5b1.738dc2f2.js new file mode 100644 index 00000000..da67a6fe --- /dev/null +++ b/assets/js/dfa6d5b1.738dc2f2.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["539"],{3743(e,n,s){s.r(n),s.d(n,{metadata:()=>i,default:()=>h,frontMatter:()=>o,contentTitle:()=>a,toc:()=>c,assets:()=>l});var i=JSON.parse('{"id":"configuration/transforms/native","title":"Native Stage","description":"Enhancing Scrobbles with Built-in Parsing","source":"@site/docs/configuration/transforms/native.mdx","sourceDirName":"configuration/transforms","slug":"/configuration/transforms/native","permalink":"/multi-scrobbler/configuration/transforms/native","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/transforms/native.mdx","tags":[],"version":"current","frontMatter":{"title":"Native Stage","description":"Enhancing Scrobbles with Built-in Parsing","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Musicbrainz Stage","permalink":"/multi-scrobbler/configuration/transforms/musicbrainz"},"next":{"title":"User Stage","permalink":"/multi-scrobbler/configuration/transforms/user"}}'),t=s(4848),r=s(8453);let o={title:"Native Stage",description:"Enhancing Scrobbles with Built-in Parsing",toc_min_heading_level:2,toc_max_heading_level:5},a,l={},c=[{value:"Configuration",id:"configuration",level:2},{value:"Rules",id:"rules",level:3},{value:"Default Stage",id:"default-stage",level:3},{value:"ENV Configuration",id:"env-configuration",level:2},{value:"Examples",id:"examples",level:2},{value:"Parse only artists string using a custom delimiter",id:"parse-only-artists-string-using-a-custom-delimiter",level:3},{value:"Don't parse specific artists",id:"dont-parse-specific-artists",level:3},{value:"Extract Artists from Title but do not modify Title",id:"extract-artists-from-title-but-do-not-modify-title",level:3}];function d(e){let n={a:"a",code:"code",em:"em",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components},{Details:s}=n;return s||function(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.strong,{children:"Native"})," ",(0,t.jsx)(n.a,{href:"/configuration/transforms#stage",children:"Stage"})," uses ",(0,t.jsx)(n.a,{href:"https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/tests/plays/playParsing.test.ts",children:"built-in heuristics"})," to try to extract Artists from Play artist/track data."]}),"\n",(0,t.jsx)(n.p,{children:"This Stage is most useful for Sources that report limited data such as:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," - Reports Artists as a single string"]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"A non-exhaustive list of heuristics:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["Splits artists in artist string using common delimiters EX ",(0,t.jsx)(n.code,{children:"Foo Artist, Bar Guy, Baz Band - My Cool Song"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["Does not split artists with ",(0,t.jsx)(n.code,{children:"&"})," in name, if other delimiters are present"]}),"\n",(0,t.jsx)(n.li,{children:"Does not split artist name when only one delimiter is present"}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["Splits artists on common joiner phrases (ft. feat. vs. etc...)","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["Extracts artists from Play title using joiner phrases EX ",(0,t.jsx)(n.code,{children:"My Cool Song (feat. SomeGuy)"})]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"A practical example of what the Native Stage will do to Play data:"}),"\n",(0,t.jsxs)(n.p,{children:["This Scrobble from ",(0,t.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility (feat. Wheatus)",\n "artists": ["Bowling For Soup & Punk Rock Factory"]\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"Will be transformed into this"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility",\n "artists": ["Bowling For Soup", "Punk Rock Factory", "Wheatus"]\n}\n'})}),"\n",(0,t.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsxs)(n.p,{children:["Available properties for ",(0,t.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"}),":"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"delimitersExtra"})," - A list of string characters that should be considered delimiters for artists ",(0,t.jsx)(n.strong,{children:"in addition to"})," multi-scrobbler's default list (",(0,t.jsx)(n.code,{children:", / \\ "}),")"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"delimiters"})," - A list of string characters that should be considered delimiters for artists.","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Replaces"})," all delimiters (MS not use any defaults, only what you give it)"]}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"artistsIgnore"})," - a list of strings and/or regular expressions. Any monolothic artist string that matches from the list ",(0,t.jsx)(n.em,{children:"will not be modified."})]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"artistsParseFrom"})," a list of the properties that should be used to try to extract artists. Can be ",(0,t.jsx)(n.code,{children:"artists"})," ",(0,t.jsx)(n.code,{children:"title"})," or both. Defaults to both when not provided in options ( ",(0,t.jsx)(n.code,{children:'["artists", "title"]'})," )","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["When ",(0,t.jsx)(n.code,{children:"artists"})," is present it tries to extract additional artists from artist strings"]}),"\n",(0,t.jsxs)(n.li,{children:["When ",(0,t.jsx)(n.code,{children:"title"})," is present it tries to extract artists from ft. feat. vs. etc... found in the track title"]}),"\n",(0,t.jsxs)(n.li,{children:["Importantly, if ",(0,t.jsx)(n.code,{children:"artists"})," is ",(0,t.jsx)(n.em,{children:"not"})," present in ",(0,t.jsx)(n.code,{children:"artistsParseFrom"})," then ",(0,t.jsx)(n.em,{children:"no artists"})," are used at all (only those from ",(0,t.jsx)(n.code,{children:"title"}),", if present)"]}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"artistsParseMonolithicOnly"})," - boolean value, defaults to ",(0,t.jsx)(n.code,{children:"true"}),". When ",(0,t.jsx)(n.code,{children:"true"})," native tranformer will only attempt to extract artists if the scrobble data has ",(0,t.jsx)(n.em,{children:"only one string for artist"})," (pre-transform)","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["This means that, for Sources like Spotify/Jellyfin/etc. that provide proper lists of artists in their data ",(0,t.jsx)(n.em,{children:"and the list has more than one string"}),", it will not try to extract artists from their strings"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(s,{children:[(0,t.jsxs)("summary",{children:["Example of the behavior for ",(0,t.jsx)(n.code,{children:"artistsParseFrom"})]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"The Foos, The Bars - My Cool Track (ft. Frank)\n"})}),(0,t.jsx)(n.p,{children:"Config => extracted artists"}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:'"artistsParseFrom": ["artists", "title"] => The Foos, The Bars, Frank\n"artistsParseFrom": ["artists"] => The Foos, The Bars\n"artistsParseFrom": ["title"] => Frank\n"artistsParseFrom": [] =>\n'})})]}),"\n",(0,t.jsx)(n.h3,{id:"rules",children:"Rules"}),"\n",(0,t.jsxs)(n.p,{children:["Each ",(0,t.jsx)(n.a,{href:"/configuration/transforms#stage-rules",children:"Rule"})," should be either a boolean, specifying if the transformed data should be used for this field, or a ",(0,t.jsxs)(n.a,{href:"/configuration/transforms#conditional-modification",children:[(0,t.jsx)(n.code,{children:"when"})," condition."]}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json5",children:'{\n "type": "native",\n // ...\n "title": false, // will not apply any changes to Play title\n "artists": {\n "when": {/* ... */}, // will only apply changes to Play artists if "when" is satisfied\n /* ... */\n },\n "album": true // will always apply changes to Play album\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["If a rule is not present then multi-scrobbler defaults it to ",(0,t.jsx)(n.code,{children:"true"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"default-stage",children:"Default Stage"}),"\n",(0,t.jsxs)(n.p,{children:["For your convenience, if you do not define any Native ",(0,t.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configurations"})," then multi-scrobbler automatically builds one using the defaults shown in ",(0,t.jsx)(n.a,{href:"#configuration",children:"configuration"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["This can be used by omitting the ",(0,t.jsx)(n.code,{children:"name"})," in your ",(0,t.jsx)(n.a,{href:"/configuration/transforms#stage",children:"modification Stage"}),"."]}),"\n",(0,t.jsxs)(s,{children:[(0,t.jsx)("summary",{children:"Example"}),(0,t.jsxs)(n.p,{children:["In a ",(0,t.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," ",(0,t.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "native",\n // do not include name, the default native stage will be used\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,t.jsx)(n.h2,{id:"env-configuration",children:"ENV Configuration"}),"\n",(0,t.jsxs)(n.p,{children:["The default configuration shown above can also be applied using ",(0,t.jsx)(n.a,{href:"/configuration?configType=env#configuration-types",children:"ENV Config"})," for individual Sources/Clients."]}),"\n",(0,t.jsxs)(n.p,{children:["Use ENV ",(0,t.jsx)(n.code,{children:"*_TRANSFORMS=native"})," on each Source/Client you wish to apply this stage to. This applies the stage in the ",(0,t.jsxs)(n.a,{href:"/configuration/transforms#lifecycle-hooks",children:[(0,t.jsx)(n.code,{children:"preTransform"})," Hook"]})," with all ",(0,t.jsx)(n.a,{href:"#rules",children:"Rules"})," enabled."]}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"*"})," stands for the prefix used for each Source/Client's ENV keys. Refer to the individual Source/Client Configuration sections to find this. Example"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["All ",(0,t.jsx)(n.a,{href:"/configuration/sources/subsonic/",children:"Subsonic Source"})," ENVs look like ",(0,t.jsx)(n.code,{children:"SUBSONIC_USER=myuser"})," etc..."]}),"\n",(0,t.jsxs)(n.li,{children:["Use ",(0,t.jsx)(n.code,{children:"SUBSONIC_TRANSFORMS=native"})]}),"\n"]}),"\n",(0,t.jsxs)(s,{children:[(0,t.jsx)("summary",{children:"Example Full Docker Deploy with ENV Configuration"}),(0,t.jsxs)(n.p,{children:["Using ",(0,t.jsx)(n.a,{href:"/quickstart#create-docker-compose-file",children:"Jellyfin + Maloja"})," example from Quickstart:"]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:'services:\n multi-scrobbler:\n image: foxxmd/multi-scrobbler\n container_name: multi-scrobbler\n environment:\n - JELLYFIN_URL=192.168.0.110:8096\n - JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56b\n - JELLYFIN_USER=MyUser\n - JELLYFIN_TRANSFORMS=native # applies native Stage to preTransform of Jellyfin source\n\n - MALOJA_URL=http://192.168.0.100:42010 # maloja receives enhanced scrobble from Jellyfin\n - MALOJA_API_KEY=myApiKey\n\n\n volumes:\n - "./config:/config"\n ports:\n - "9078:9078"\n restart: unless-stopped\n'})})]}),"\n",(0,t.jsx)(n.h2,{id:"examples",children:"Examples"}),"\n",(0,t.jsx)(n.h3,{id:"parse-only-artists-string-using-a-custom-delimiter",children:"Parse only artists string using a custom delimiter"}),"\n",(0,t.jsxs)(s,{children:[(0,t.jsx)("summary",{children:"Example"}),(0,t.jsxs)(n.p,{children:["Your ",(0,t.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "native",\n "name": "MyNativeTransformer",\n "defaults": {\n // default delimiters when this Stage is used in a hook\n "delimiters": [\n "\u2022"\n ],\n // default delimiters when this Stage is used in a hook\n "artistsParseFrom": ["artists"]\n }\n }\n ]\n}\n'})}),(0,t.jsxs)(n.p,{children:["In a ",(0,t.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," ",(0,t.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "native",\n "name": "MyNativeTransformer"\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,t.jsx)(n.h3,{id:"dont-parse-specific-artists",children:"Don't parse specific artists"}),"\n",(0,t.jsxs)(s,{children:[(0,t.jsx)("summary",{children:"Example"}),(0,t.jsxs)(n.p,{children:["Your ",(0,t.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "native",\n "name": "MyNativeTransformer",\n "defaults": {\n "artistsIgnore": [\n "Crosby, Stills, Nash & Young",\n "Polo & Pan"\n ]\n }\n }\n ]\n}\n'})}),(0,t.jsxs)(n.p,{children:["In a ",(0,t.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," ",(0,t.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "native",\n "name": "MyNativeTransformer"\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,t.jsx)(n.h3,{id:"extract-artists-from-title-but-do-not-modify-title",children:"Extract Artists from Title but do not modify Title"}),"\n",(0,t.jsxs)(n.p,{children:["In the scenario where you want to extract Artists from Track titles with joiners like ",(0,t.jsx)(n.code,{children:"My Cool Track (feat. Foobar)"})," but want to keep the title as-is:"]}),"\n",(0,t.jsxs)(s,{children:[(0,t.jsx)("summary",{children:"Example"}),(0,t.jsxs)(n.p,{children:["No additional ",(0,t.jsx)(n.a,{href:"#configuration",children:"stage configuration"})," is required so we can use the ",(0,t.jsx)(n.a,{href:"#default-stage",children:"default Stage"}),"."]}),(0,t.jsxs)(n.p,{children:["In a ",(0,t.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," ",(0,t.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "native",\n "title": false // do not modify title\n }\n ]\n }\n }\n }\n]\n'})})]})]})}function h(e={}){let{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}}}]); \ No newline at end of file diff --git a/assets/js/e6ac3f64.65cdf413.js b/assets/js/e6ac3f64.65cdf413.js new file mode 100644 index 00000000..0ce8b645 --- /dev/null +++ b/assets/js/e6ac3f64.65cdf413.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["1184"],{265(e,n,i){i.r(n),i.d(n,{metadata:()=>s,default:()=>m,frontMatter:()=>l,contentTitle:()=>c,toc:()=>h,assets:()=>d});var s=JSON.parse('{"id":"configuration/transforms/musicbrainz","title":"Musicbrainz Stage","description":"Enhancing Scrobbles with Musicbrainz","source":"@site/docs/configuration/transforms/musicbrainz.mdx","sourceDirName":"configuration/transforms","slug":"/configuration/transforms/musicbrainz","permalink":"/multi-scrobbler/configuration/transforms/musicbrainz","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/transforms/musicbrainz.mdx","tags":[],"version":"current","frontMatter":{"title":"Musicbrainz Stage","description":"Enhancing Scrobbles with Musicbrainz","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Enhancing Scrobbles","permalink":"/multi-scrobbler/configuration/transforms/"},"next":{"title":"Native Stage","permalink":"/multi-scrobbler/configuration/transforms/native"}}'),r=i(4848),a=i(8453),t=i(8194),o=i(698);let l={title:"Musicbrainz Stage",description:"Enhancing Scrobbles with Musicbrainz",toc_min_heading_level:2,toc_max_heading_level:5},c,d={},h=[{value:"Configuration",id:"configuration",level:2},{value:"API Setup",id:"api-setup",level:3},{value:"Stage Configuration",id:"stage-configuration",level:3},{value:"Rules and Hooks",id:"rules-and-hooks",level:3},{value:"ENV Configuration",id:"env-configuration",level:2},{value:"Matching with Musicbrainz",id:"matching-with-musicbrainz",level:2},{value:"Searching",id:"searching",level:3},{value:"Should MS Search?",id:"should-ms-search",level:4},{value:"Search Methods",id:"search-methods",level:4},{value:"Refining",id:"refining",level:3},{value:"Score",id:"score",level:3},{value:"Filtering",id:"filtering",level:3},{value:"Release Attributes",id:"release-attributes",level:5},{value:"Empty Releases",id:"empty-releases",level:4},{value:"Sorting",id:"sorting",level:3},{value:"Release Attributes",id:"sorting-releases",level:4},{value:"Field Scoring",id:"field-scoring",level:4},{value:"Best Practices",id:"best-practices",level:2},{value:"Caching",id:"caching",level:3},{value:"Sensible Default",id:"sensible-default",level:3},{value:"Search Considerations",id:"search-considerations",level:3},{value:"Filter Considerations",id:"filter-considerations",level:3},{value:"Using Partial Match",id:"using-partial-match",level:3},{value:"Logging",id:"logging",level:2},{value:"Examples",id:"examples",level:2},{value:"Minimal",id:"minimal",level:3},{value:"Sensible Default",id:"sensible-default-1",level:3},{value:"Add Metadata Only (No Scrobble Modification)",id:"add-metadata-only-no-scrobble-modification",level:3},{value:"Use Native Stage if no Musicbrainz Matches",id:"use-native-stage-if-no-musicbrainz-matches",level:3},{value:"Different Search Methods Based on Source (Last.fm and Spotify)",id:"override-stage-configuration-example",level:3},{value:"Use Metadata for Discord Album Art",id:"use-metadata-for-discord-album-art",level:3}];function u(e){let n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",h5:"h5",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.R)(),...e.components},{Details:i,DetailsAdmo:s}=n;return i||f("Details",!0),s||f("DetailsAdmo",!0),(0,r.jsxs)(r.Fragment,{children:[(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.strong,{children:"Musicbrainz"})," ",(0,r.jsx)(n.a,{href:"/configuration/transforms#stage",children:"Stage"})," matches your Play data with the ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/",children:"MusicBrainz"})," database. If the match score is high enough then Multi-Scrobbler uses the match to correct and fill-in missing information in your Play data."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"This Stage is useful for standardizing your Scrobble's Play data, regardless of the Source it is coming from."})}),"\n",(0,r.jsxs)(n.p,{children:["Even if you have have strong opinions about the way your existing music is already tagged/organized you should consider adding the Musicbrainz Stage to your Sources, if only to benefit from adding ",(0,r.jsx)(n.a,{href:"#using-partial-match",children:"MBIDs to scrobbled metadata."})]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Why Should I Prefer MusicBrainz?"}),(0,r.jsx)("h3",{children:"More Than Mainstream Data"}),(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/statistics",children:"MusicBrainz database contains"})]}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"37 million+ tracks"}),"\n",(0,r.jsx)(n.li,{children:"5 million+ albums"}),"\n",(0,r.jsx)(n.li,{children:"2.7 million+ artists"}),"\n"]}),(0,r.jsx)(n.p,{children:"in over 200 languages, contributed by 10,000+ editors each year with many more users voting on edits."}),(0,r.jsx)(n.p,{children:"The database covers a much wider range of music than most commercial services since it isn't restricted by licensing or incentivized to prioritize mainstream music due to business interests. You are much more likely to find data for obscure, indie, and international artists."}),(0,r.jsx)("h3",{children:"As The Artist Intended"}),(0,r.jsx)(n.p,{children:'While some large services (Spotify) may be more consistent in data structure, the MusicBrainz equivalent data is likely to be more true to the way the Artist originally intended since individuals can devote effort to individual artists. A fan of an artist is much more likely to "get it right" for their favorite band than a corporate business trying to do the least amount of work for the largest result.'})]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["If you are using ",(0,r.jsx)(n.a,{href:"/configuration?config-type=env#configuration-types",children:"ENV Config"})," for multi-scrobbler and just want a quick and easy setup, skip to ",(0,r.jsx)(n.a,{href:"#env-configuration",children:(0,r.jsx)(n.strong,{children:"ENV Configuration"})}),"."]})}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["Set up ",(0,r.jsx)(n.a,{href:"/configuration?cachedThings=metadata#caching",children:"Valkey Caching"})," to cache Musicbrainz API calls for faster processing."]})}),"\n",(0,r.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsx)(n.h3,{id:"api-setup",children:"API Setup"}),"\n",(0,r.jsxs)(n.p,{children:["To avoid rate limiting, MusicBrainz requires API users to identify themself using a ",(0,r.jsx)(n.a,{href:"https://wiki.musicbrainz.org/MusicBrainz_API/Rate_Limiting#How_can_I_be_a_good_citizen_and_be_smart_about_using_the_Web_Service?",children:"User-Agent that contains an email or URL."})," You must set this in the ",(0,r.jsx)(n.code,{children:"data"})," for your ",(0,r.jsx)(n.a,{href:"/configuration/transforms/#configuring-stages",children:"stage configuration"})," in your ",(0,r.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ]\n },\n }\n ]\n}\n'})}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Additional Musicbrainz Servers"}),(0,r.jsx)(n.p,{children:"Other, or additional, Musicbrainz Servers/Mirrors can be added to the API configuration. If more than one server is defined then multi-scrobbler will round-robin load balance API calls."}),(0,r.jsxs)(n.p,{children:["Use ",(0,r.jsx)(n.code,{children:"url"})," to define the base URL of the Musicbrainz server to use. If ",(0,r.jsx)(n.code,{children:"url"})," is not defined multi-scrobbler assumes it is the primary Musicbrainz server, ",(0,r.jsx)(n.code,{children:"https://musicbrainz.org"}),"."]}),(0,r.jsx)(n.p,{children:"Example of multiple servers:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n // uses default Musicbrainz server https://musicbrainz.org\n },\n // additional server\n {\n "contact": "contact@mydomain.com",\n "url": "https://my.mb.mirror.domain.com"\n }\n ]\n },\n }\n ]\n}\n'})})]}),"\n",(0,r.jsx)(n.h3,{id:"stage-configuration",children:"Stage Configuration"}),"\n",(0,r.jsxs)(n.p,{children:["All of the properties found in ",(0,r.jsx)(n.a,{href:"#matching-with-musicbrainz",children:(0,r.jsx)(n.strong,{children:"Matching with Musicbrainz"})})," section are configured in ",(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," as ",(0,r.jsx)(n.code,{children:"defaults"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"Example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ],\n "defaults": {\n "releaseStatusPriority": ["official"],\n "releaseAllowEmpty": true\n }\n },\n }\n ]\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"rules-and-hooks",children:"Rules and Hooks"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms/#stage",children:"Add your Stage"})," to a Source or Client by specifying it in a ",(0,r.jsx)(n.a,{href:"/configuration/transforms/#hook",children:"Hook"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB"\n }\n ]\n }\n }\n }\n]\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Each ",(0,r.jsx)(n.a,{href:"/configuration/transforms#stage-rules",children:(0,r.jsx)(n.strong,{children:"Stage Rule"})})," should be either a boolean, specifying if the transformed data should be used for this field, or a ",(0,r.jsxs)(n.a,{href:"/configuration/transforms#conditional-modification",children:[(0,r.jsx)(n.code,{children:"when"})," condition"]}),":"]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB"\n // ...\n "title": false, // will not apply any changes to Play title\n "artists": {\n "when": {/* ... */}, // will only apply changes to Play artists if "when" is satisfied\n /* ... */\n },\n "album": true // will always apply changes to Play album\n "meta": true // adds MusicBrainz MBIDs to scrobble data\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,r.jsxs)(n.admonition,{title:"Per Component Override",type:"tip",children:[(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"defaults"})," you set in ",(0,r.jsx)(n.a,{href:"#stage-configuration",children:"Stage Configuration"})," ",(0,r.jsx)(n.a,{href:"/configuration/transforms/#overriding-configuration",children:"can be overriden/added to"})," (per property) in each Hook."]}),(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "releaseStatusPriority": ["psuedo-release"], // override from defaults\n "searchOrder": ["freetext"] // add new property, combined with defaults\n }\n ]\n }\n }\n }\n]\n'})})]})]}),"\n",(0,r.jsx)(n.h2,{id:"env-configuration",children:"ENV Configuration"}),"\n",(0,r.jsxs)(n.p,{children:["The general configuration shown above can also be configured from a selection of ",(0,r.jsx)(n.em,{children:"presets"})," using ",(0,r.jsx)(n.a,{href:"/configuration?configType=env#configuration-types",children:"ENV Config"})," for individual Sources/Clients."]}),"\n",(0,r.jsxs)(n.p,{children:["You must ",(0,r.jsx)(n.strong,{children:"always"})," include the ",(0,r.jsx)(n.code,{children:"MB_CONTACT"})," ENV to ",(0,r.jsx)(n.a,{href:"#api-setup",children:"identify your application"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ini",children:"MB_CONTACT=contact@mydomain.com\n"})}),"\n",(0,r.jsxs)(n.p,{children:["To configure ",(0,r.jsx)(n.a,{href:"#stage-configuration",children:"stage defaults"})," use ",(0,r.jsx)(n.code,{children:"MB_PRESETS"})," with a comma-delimited list of presets you wish to apply. More than one preset can be applied, in which case they combine. You must choose at least one of the presets below:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"default"})," - Adds the default ",(0,r.jsx)(n.a,{href:"#search-methods",children:(0,r.jsx)(n.code,{children:"searchOrder"})})," ",(0,r.jsx)(n.code,{children:"isrc"}),"+",(0,r.jsx)(n.code,{children:"basic"})," (same as using no presets that affect ",(0,r.jsx)(n.code,{children:"searchOrder"}),")"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"id"})," - Adds ",(0,r.jsx)(n.a,{href:"#search-methods",children:(0,r.jsx)(n.code,{children:"searchOrder"})})," methods that priorites searching by ID: ",(0,r.jsx)(n.code,{children:"isrc"})," + ",(0,r.jsx)(n.code,{children:"mbidrecording"})," + ",(0,r.jsx)(n.code,{children:"basicOrIds"})," + ",(0,r.jsx)(n.code,{children:"basic"})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"native"})," - Adds ",(0,r.jsx)(n.strong,{children:"Extracted Artists"})," (",(0,r.jsx)(n.code,{children:"artist"}),") in ",(0,r.jsx)(n.code,{children:"native"})," mode to ",(0,r.jsx)(n.a,{href:"#search-methods",children:(0,r.jsx)(n.code,{children:"searchOrder"})})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"aggressive"})," - Adds ",(0,r.jsx)(n.strong,{children:"Free Text Search"})," (",(0,r.jsx)(n.code,{children:"freetext"}),") to ",(0,r.jsx)(n.a,{href:"#search-methods",children:(0,r.jsx)(n.code,{children:"searchOrder"})})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"fields"})," - Applies ",(0,r.jsx)(n.a,{href:"#field-scoring",children:"Field Scoring"})," to match sorting so match text is baised towards similarity to original scrobble"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"sensible"})," - Applies the ",(0,r.jsx)(n.a,{href:"#sensible-default",children:"Sensible Default"})," configuration for ",(0,r.jsx)(n.a,{href:"#sorting-releases",children:"sorting releases"})]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Finally, use ENV ",(0,r.jsx)(n.code,{children:"*_TRANSFORMS=musicbrainz"})," on each Source/Client you wish to apply this stage to. This applies the stage in the ",(0,r.jsxs)(n.a,{href:"/configuration/transforms#lifecycle-hooks",children:[(0,r.jsx)(n.code,{children:"preTransform"})," Hook"]})," with all ",(0,r.jsx)(n.a,{href:"#rules-and-hooks",children:"Rules"})," enabled."]}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"*"})," stands for the prefix used for each Source/Client's ENV keys. Refer to the individual Source/Client Configuration sections to find this. Example:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["All ",(0,r.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic Source"})," ENVs look like ",(0,r.jsx)(n.code,{children:"SUBSONIC_USER=myuser"})," etc..."]}),"\n",(0,r.jsxs)(n.li,{children:["Use ",(0,r.jsx)(n.code,{children:"SUBSONIC_TRANSFORMS=musicbrainz"})]}),"\n"]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example Full Docker Deploy with ENV Configuration"}),(0,r.jsxs)(n.p,{children:["Using ",(0,r.jsx)(n.a,{href:"/quickstart#create-docker-compose-file",children:"Jellyfin + Maloja"})," example from Quickstart:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:'services:\n multi-scrobbler:\n image: foxxmd/multi-scrobbler\n container_name: multi-scrobbler\n environment:\n // highlight-start\n - MB_CONTACT=contact@mydomain.com\n # searches with sensible release sorting with the searchOrder\n # isrc => basic => artist (native mode)\n - MB_PRESETS=default,sensible,native\n // highlight-end\n - JELLYFIN_URL=192.168.0.110:8096\n - JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56b\n - JELLYFIN_USER=MyUser\n // highlight-start\n # applies musicbrainz Stage to preTransform of Jellyfin source\n - JELLYFIN_TRANSFORMS=musicbrainz\n // highlight-end\n\n # maloja receives enhanced scrobble from Jellyfin\n - MALOJA_URL=http://192.168.0.100:42010\n - MALOJA_API_KEY=myApiKey\n\n\n volumes:\n - "./config:/config"\n ports:\n - "9078:9078"\n restart: unless-stopped\n'})})]}),"\n",(0,r.jsx)(n.h2,{id:"matching-with-musicbrainz",children:"Matching with Musicbrainz"}),"\n",(0,r.jsxs)(n.admonition,{type:"note",children:[(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"All properties found in this section are optional."})}),(0,r.jsxs)(n.p,{children:["If you just want a sensible default for configuraion refer to the ",(0,r.jsx)(n.a,{href:"#best-practices",children:"Best Practices"})," section."]})]}),"\n",(0,r.jsx)(n.p,{children:"Matching your Scrobble's Play data with a result from Musicbrainz is comprised of two steps:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"#searching",children:(0,r.jsx)(n.strong,{children:"Searching"})})," Musicbrainz using parts of your Scrobble as queries"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"#refining",children:(0,r.jsx)(n.strong,{children:"Refining"})})," matched results to select the desired/allowed match"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"Both steps have separate configuration."}),"\n",(0,r.jsx)(n.h3,{id:"searching",children:"Searching"}),"\n",(0,r.jsx)(n.h4,{id:"should-ms-search",children:"Should MS Search?"}),"\n",(0,r.jsx)(n.p,{children:"Before MS begins a search it checks if your Scrobble data already contains:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"https://musicbrainz.org/doc/MusicBrainz_Identifier",children:"MBIDs"})," for ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/doc/Artist",children:"artists"}),", ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/doc/Release",children:"album"}),", and specific ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/doc/Recording",children:"track/recording"})]}),"\n",(0,r.jsx)(n.li,{children:"and duration"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["If it already has all required data types then the entire Musicbrainz Stage ",(0,r.jsxs)(n.a,{href:"/configuration/transforms/#flow-control",children:["is ",(0,r.jsx)(n.strong,{children:"skipped"}),"."]})," If any are missing then a search is performed."]}),"\n",(0,r.jsx)(n.p,{children:"Define which data types are required using:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"searchWhenMissing"})," (defaults to all) - A list containing any of: ",(0,r.jsx)(n.code,{children:"artists"})," ",(0,r.jsx)(n.code,{children:"title"})," ",(0,r.jsx)(n.code,{children:"album"})," ",(0,r.jsx)(n.code,{children:"duration"})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"forceSearch"})," (default ",(0,r.jsx)(n.code,{children:"false"}),") - Force searching even if all required data is present"]}),"\n"]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // only search if MBIDs for artists or album is missing\n "searchWithMissing": ["artists", "album"]\n\n // uncomment to make the stage always search, even if above data is present\n //forceSearch: true\n }\n'})})]}),"\n",(0,r.jsxs)(n.p,{children:["If required data is missing then the next step is making ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/doc/MusicBrainz_API/Search",children:"search queries to the Musicbrainz database"})," to try to get potential candidates."]}),"\n",(0,r.jsx)(n.h4,{id:"search-methods",children:"Search Methods"}),"\n",(0,r.jsxs)(n.p,{children:["Multi-scrobbler can search for Musicbrainz matches using ",(0,r.jsx)(n.strong,{children:"multiple"})," methods. You control:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"the search methods that are used"}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"the order"})," in which search methods are executed"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"All search methods will be run in the order they were defined until one method returns at least one match."}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"The methods you choose, and the order they run in, can drastically change how many API calls are needed to get a match, and what types of matches are returned."})," See ",(0,r.jsx)(n.a,{href:"#search-considerations",children:(0,r.jsx)(n.strong,{children:"Search Considerations"})})," for guidance on how to decide on methods based on Source."]}),"\n",(0,r.jsxs)(n.p,{children:["To set search methods and their order use the ",(0,r.jsx)(n.code,{children:"searchOrder"})," option in your ",(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // only searches using these methods, in the order they are listed here\n "searchOrder": ["isrc", "mbidrecording", "basic","freetext"]\n }\n'})}),"\n",(0,r.jsx)(n.admonition,{title:"Default Search Methods",type:"tip",children:(0,r.jsxs)(n.p,{children:["If ",(0,r.jsx)(n.code,{children:"searchOrder"})," is undefined then Multi-scrobbler will default to using ",(0,r.jsx)(n.code,{children:"isrc"})," then ",(0,r.jsx)(n.code,{children:"basic"})," method."]})}),"\n",(0,r.jsx)("h4",{children:"Available Methods"}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsxs)("summary",{children:["ISRC (",(0,r.jsx)(n.code,{children:"isrc"}),")"]}),(0,r.jsxs)(n.p,{children:["If your Scrobble data contains an ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/doc/ISRC",children:"ISRC"})," (usually from Spotify) then Multi-scrobbler can search using this ID, only."]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // will only search using isrc\n "searchOrder": ["isrc"]\n }\n'})})]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsxs)("summary",{children:["Track MBID (",(0,r.jsx)(n.code,{children:"mbidrecording"}),")"]}),(0,r.jsxs)(n.p,{children:["If your Scrobble data contains an ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/doc/Recording",children:"Track (Recording) MBID"})," then Multi-scrobbler can search using this MBID, only."]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // will only search using recording mbid\n "searchOrder": ["mbidrecording"]\n }\n'})})]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsxs)("summary",{children:["Album, Artist, and Title Fields (",(0,r.jsx)(n.code,{children:"basic"}),")"]}),(0,r.jsx)(n.p,{children:"Will search Musicbrainz using any/all available text fields from your scrobble: Album, Artists(s), and Title. Does not leverage any existing MBIDs."}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // will only search using album, artist(s), and title\n "searchOrder": ["basic"]\n }\n'})})]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsxs)("summary",{children:["Album/Artist/Title OR MBIDs (",(0,r.jsx)(n.code,{children:"basicOrIds"}),")"]}),(0,r.jsxs)(n.p,{children:["This is the same as the above ",(0,r.jsx)(n.strong,{children:"Album, Artist, and Title Fields"})," search except that if the scrobble data contains any corresponding MBIDs it will use those instead of the plain text field."]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // will only search using album, artist(s), and title\n "searchOrder": ["basicOrIds"]\n }\n'})}),(0,r.jsx)(n.p,{children:"Example:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility (feat. Wheatus)",\n "artists": ["Bowling For Soup"]\n "albumMbid": "c9f91cdc-984e-4303-9a51-4ac0dfa2348f",\n "album": "My Cool Album"\n}\n'})}),(0,r.jsxs)(n.p,{children:["Multi-scrobbler will search Musicbrainz using ",(0,r.jsx)(n.code,{children:"title"}),", ",(0,r.jsx)(n.code,{children:"artists"}),", and ",(0,r.jsx)(n.code,{children:"albumMbid"})," but NOT ",(0,r.jsx)(n.code,{children:"album"}),"."]})]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsxs)("summary",{children:["Album And Title Only (",(0,r.jsx)(n.code,{children:"album"}),")"]}),(0,r.jsxs)(n.p,{children:["If your Scrobble data contains a title, artist(s), and an album (all three fields) then Multi-scrobbler can search using only title and album by using ",(0,r.jsx)(n.code,{children:"album"})," for ",(0,r.jsx)(n.code,{children:"searchOrder"}),":"]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // will only search using only title + album name\n "searchOrder": ["album"]\n }\n'})})]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsxs)("summary",{children:["Extracted Artists (",(0,r.jsx)(n.code,{children:"artist"}),")"]}),(0,r.jsxs)(n.p,{children:["If your scrobble data contains only ",(0,r.jsx)(n.strong,{children:"one"})," artist string then Multi-scrobbler can attempt to extract multiple artists from your artist and track string."]}),(0,r.jsxs)(n.p,{children:["Use ",(0,r.jsx)(n.code,{children:"artist"})," with ",(0,r.jsx)(n.code,{children:"searchOrder"})," and optionally specify the extraction mode with ",(0,r.jsx)(n.code,{children:"searchArtistMethod"}),":"]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // will only search using title/album + extracted artists\n "searchOrder": ["artist"],\n //searchArtistMethod: \'native\' // optional\n }\n'})}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"searchArtistMethod"})," can be set to ",(0,r.jsx)(n.strong,{children:"Native"})," or ",(0,r.jsx)(n.strong,{children:"Naive"})," mode:"]}),(0,r.jsxs)(t.A,{groupId:"searchArtistMethod",queryString:!0,children:[(0,r.jsxs)(o.A,{value:"native",label:"Native (Recommended)",children:[(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Native"})," (",(0,r.jsx)(n.code,{children:"native"}),") mode uses an aggressive configuration of the ",(0,r.jsx)(n.a,{href:"/configuration/transforms/native",children:"Native Stage"}),' to extract artists using common delimiters and common "joined" artist patterns from the artist and title string of your Scrobble data.']}),(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsx)(n.p,{children:"If you already have a Native Stage configured you should use that instead, running it before the Musicbrainz stage."})}),(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Native Mode Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility (feat. Wheatus)",\n "artists": ["Bowling For Soup & Punk Rock Factory, My Cool Band"]\n}\n'})}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Extracts ",(0,r.jsx)(n.code,{children:"Bowling For Soup"})," ",(0,r.jsx)(n.code,{children:"Punk Rock Factory"})," ",(0,r.jsx)(n.code,{children:"My Cool Band"})," from artist string"]}),"\n",(0,r.jsxs)(n.li,{children:["Extracts ",(0,r.jsx)(n.code,{children:"Wheatus"})," from title string","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Removes ",(0,r.jsx)(n.code,{children:"(feat. Wheatus)"})," from title string because it found an artist there"]}),"\n"]}),"\n"]}),"\n"]}),(0,r.jsx)(n.p,{children:"Resulting data used for Musicbrainz search:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility",\n "artists": ["Bowling For Soup", "Punk Rock Factory", "My Cool Band", "Wheatus"]\n}\n'})})]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // will only search using title/album + native extracted artists\n "searchOrder": ["artist"],\n "searchArtistMethod": "native"\n }\n'})})]}),(0,r.jsxs)(o.A,{value:"naive",label:"Naive",children:[(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Naive"})," (",(0,r.jsx)(n.code,{children:"naive"}),") mode looks for the first found common delimiter in the artist string. If it finds one then it uses the preceding value as the only artist in the Musicbrainz search. It does not try to extract additional artists from the artist string, or extract anything from the title string."]}),(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Naive Mode Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility (feat. Wheatus)",\n "artists": ["Bowling For Soup, Punk Rock Factory & Wheatus"]\n}\n'})}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Finds ",(0,r.jsx)(n.code,{children:"&"}),' as first common delimiter, extracts "Bowling For Soup"']}),"\n"]}),(0,r.jsx)(n.p,{children:"Resulting data used for Musicbrainz search:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility (feat. Wheatus)",\n "artists": ["Bowling For Soup"]\n}\n'})})]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // will only search using title/album + naive extracted artists\n "searchOrder": ["artist"],\n "searchArtistMethod": "naive"\n }\n'})})]})]})]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsxs)("summary",{children:["Free Text (",(0,r.jsx)(n.code,{children:"freetext"}),")"]}),(0,r.jsxs)(n.p,{children:["This will search Musicbrainz for ",(0,r.jsx)(n.strong,{children:"all"})," text of your Scrobble data artist/title/album, without constraint."]}),(0,r.jsxs)(n.admonition,{type:"warning",children:[(0,r.jsxs)(n.p,{children:["Free text search is ",(0,r.jsx)(n.strong,{children:"unconstrained"})," which means that Musicbrainz will match text in ",(0,r.jsx)(n.strong,{children:"any"})," part of a Recording (artist, release, or recording field), regardless of where it is found. This may lead to results that are unexpected and undesired."]}),(0,r.jsxs)(n.p,{children:["If you know your Source's Play data is well organized you ",(0,r.jsx)(n.strong,{children:"should not"})," enable this. Free Text search should only be used if:"]}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"your music is not well organized or"}),"\n",(0,r.jsxs)(n.li,{children:["there may be many alternative titles/artists for the music you listen to (such as with ",(0,r.jsx)(n.a,{href:"https://wiki.musicbrainz.org/Release#Status",children:"psuedo-releases"}),")"]}),"\n",(0,r.jsxs)(n.li,{children:["you can tolerate that matches may not be accurate for releases.","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Generally, if you keep ",(0,r.jsx)(n.a,{href:"#score",children:"score"})," high then matched releases should ",(0,r.jsx)(n.em,{children:"at least"}),' be the right artist or what you would expect within a reason, as a match. The release may not be the one you actually listened to but it would be "close enough".']}),"\n"]}),"\n"]}),"\n"]})]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // ...\n // will only search using free text search of all artist/album/title\n "searchOrder": ["freetext"],\n }\n'})})]}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["If all defined search methods do not return any results then the stage is marked as ",(0,r.jsxs)(n.a,{href:"/configuration/transforms/#flow-control",children:[(0,r.jsx)(n.strong,{children:"failed"})," (",(0,r.jsx)(n.code,{children:"onFailure"}),") for ",(0,r.jsx)(n.strong,{children:"Flow Control"})]}),"."]})}),"\n",(0,r.jsx)(n.h3,{id:"refining",children:"Refining"}),"\n",(0,r.jsx)(n.h3,{id:"score",children:"Score"}),"\n",(0,r.jsxs)(n.p,{children:["Each match returned by MusicBrainz contains a numeric score representing how close it was to the search parameters. Set ",(0,r.jsx)(n.code,{children:"score"})," in configuration to set a minimum score that must be met by matches. Default is ",(0,r.jsx)(n.code,{children:"90"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'{\n // ...\n "score": 90 // matches must score 90 or higher to be considered\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"filtering",children:"Filtering"}),"\n",(0,r.jsxs)(n.p,{children:["There are several attributes associated with the ",(0,r.jsx)(n.a,{href:"https://wiki.musicbrainz.org/Release",children:"Release"})," a ",(0,r.jsx)(n.a,{href:"https://wiki.musicbrainz.org/Recording",children:"Recording"})," (individual Track/song) belongs to that you may be interested in controlling. MS can use these attributes to filter what the final Recording selected to match against your Play data is."]}),"\n",(0,r.jsx)(n.p,{children:"An easy way to think about this:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Do you prefer Albums, Singles, or EPs?"}),"\n",(0,r.jsx)(n.li,{children:"Do you always want to use an official album release, or are bootleg or cancelled albums okay to use?"}),"\n",(0,r.jsx)(n.li,{children:"Should a track that belongs to a Compilation album be allowed to match?"}),"\n",(0,r.jsx)(n.li,{children:"What country do you prefer an album release to be from?"}),"\n"]}),"\n",(0,r.jsx)(n.h5,{id:"release-attributes",children:"Release Attributes"}),"\n",(0,r.jsxs)(n.p,{children:["These attributes (",(0,r.jsx)(n.code,{children:"attribute_name"})," in config) are:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.a,{href:"https://wiki.musicbrainz.org/Release#Status",children:["Release ",(0,r.jsx)(n.strong,{children:"Status"})]})," (",(0,r.jsx)(n.code,{children:"releaseStatus"}),") - Is this release/album/ep official, promotional, bootleg, etc..."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.a,{href:"https://wiki.musicbrainz.org/Release_Group/Type#Primary_types",children:["Release Group ",(0,r.jsx)(n.strong,{children:"Primary Type"})]})," (",(0,r.jsx)(n.code,{children:"releaseGroupPrimaryType"}),") - Is this release an Album, Single, EP, etc..."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.a,{href:"https://wiki.musicbrainz.org/Release_Group/Type#Secondary_types",children:["Release Group ",(0,r.jsx)(n.strong,{children:"Secondary Type"})]})," (",(0,r.jsx)(n.code,{children:"releaseGroupSecondaryType"}),") - Is this release a compilation, soundtrack, remix, etc..."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.a,{href:"https://beta.musicbrainz.org/doc/Release/Country",children:["Release ",(0,r.jsx)(n.strong,{children:"Country"})]})," (",(0,r.jsx)(n.code,{children:"releaseCountry"}),") - What ISO2 country was this released in? US, GB, MX, etc...","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["MusicBrainz uses the special code ",(0,r.jsx)(n.code,{children:"XW"}),' to represent a "Worldwide" release AKA release not made specifically for a country']}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Each of the above attributes can be used to ",(0,r.jsx)(n.strong,{children:"filter"})," matches using ",(0,r.jsx)(n.code,{children:"allow"})," (explicitly include) or ",(0,r.jsx)(n.code,{children:"deny"})," (exclude) terms. The correct property name for each is like so:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:'[attribute_name]Allow = [...]\n[attribute_name]Deny\n\nEX to explicitly allow only release status "official"\n\n"releaseStatusAllow": ["official"]\n\nEX to exclude compilations\n\n"releaseGroupSecondaryTypeDeny": ["compilation"]\n'})}),"\n",(0,r.jsx)(n.h4,{id:"empty-releases",children:"Empty Releases"}),"\n",(0,r.jsx)(n.p,{children:"Sometimes a Recording may not have any associated Releases. This may be because there is not enough information about the Recording yet, or it was never included on an actual Release."}),"\n",(0,r.jsxs)(n.p,{children:["You may want to filter the majority of your matches by releases but allow matching a Recording that has no releases to begin with. To allow this set ",(0,r.jsx)(n.code,{children:"releaseAllowEmpty"})," to ",(0,r.jsx)(n.code,{children:"true"})," in configuration:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // ...\n "releaseAllowEmpty": true // don\'t remove a match during filtering just because it has no releases\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"sorting",children:"Sorting"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.em,{children:"After"})," matches have been ",(0,r.jsx)(n.a,{href:"#filtering",children:"filtered"}),", the remaining matches ",(0,r.jsx)(n.em,{children:"and their releases"}),"* can be sorted. Matches/Releases with an attribute that does not match are ",(0,r.jsx)(n.strong,{children:"not"})," removed, but they are sorted lower than matches that do match."]}),"\n",(0,r.jsxs)(n.p,{children:["Sorting can be a good alternative to filtering: with filters there is a possibility your filters may eliminate all matches; if you want to ensure that ",(0,r.jsx)(n.strong,{children:"some"})," match will be used then sorting can ensure that the ",(0,r.jsx)(n.strong,{children:"best choice"})," out of those given will always be used, without accidentally ending up with ",(0,r.jsx)(n.strong,{children:"no choice"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"Sorting can be used instead of filters, or in conjunction with filters, it's your choice."}),"\n",(0,r.jsx)(n.h4,{id:"sorting-releases",children:"Release Attributes"}),"\n",(0,r.jsxs)(n.p,{children:["Each ",(0,r.jsx)(n.a,{href:"#release-attributes",children:"Release Attribute"})," has one additional property that can be used to ",(0,r.jsx)(n.strong,{children:"rank"})," releases based on the order of the values you give it. This is the ",(0,r.jsx)(n.code,{children:"priority"})," property."]}),"\n",(0,r.jsx)(n.p,{children:"In configuration:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:'[attribute_name]Priority = [...]\n\nEX to prefer "official" releases over everything else\n\n"releaseStatusPriority": ["official"]\n\nEX to prefer albums, then singles, over everything else\n\n"releaseGroupPrimaryTypePriority": ["album", "single"]\n'})}),"\n",(0,r.jsx)(n.h4,{id:"field-scoring",children:"Field Scoring"}),"\n",(0,r.jsx)(n.p,{children:'Matches can additionally be sorted/ranked based on how similar their fields are to the original Scrobble. Use these properties for sorting when you want the final Musicbrainz match to be as closely matched to the original Scrobble, as opposed to what may be the most "correct" match based on the Musicbrainz score or suggested Release.'}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example Scenario"}),(0,r.jsxs)(n.p,{children:["You scrobble ",(0,r.jsx)(n.a,{href:"https://open.spotify.com/track/21trRQA61afzljSK9V0SHX",children:"this track from spotify"}),", which has an English title, Album name, and Artist name."]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{ \n "track": "Price",\n "artists": ["ATLUS Sound Team"],\n "album": "PERSONA5 ORIGINAL SOUNDTRACK"\n}\n'})}),(0,r.jsxs)(n.p,{children:["However, the ",(0,r.jsx)(n.a,{href:"https://beta.musicbrainz.org/search?query=isrc%3AJPK651601515&type=recording&limit=25&method=advanced",children:"Recording found by Musicbrainz"})," has multiple Releases (albums). The first suggested release is a ",(0,r.jsx)(n.em,{children:"related"})," Album with similar name (",(0,r.jsx)(n.code,{children:"Persona 25th Anniversary Deluxe Vinyl Box Set"}),") but it's not the same as your album. Additionally, the artists for this album are in Japanese, rather than English."]}),(0,r.jsxs)(n.p,{children:["You have used ",(0,r.jsx)(n.code,{children:"releaseStatusPriority"})," to prioritize ",(0,r.jsx)(n.code,{children:"psuedo-release"})," which does work but there are still multiple releases chosen, and some are non-English (",(0,r.jsx)(n.code,{children:"\u300E\u30DA\u30EB\u30BD\u30CA5\u300F\u30AA\u30EA\u30B8\u30CA\u30EB\u30FB\u30B5\u30A6\u30F3\u30C9\u30C8\u30E9\u30C3\u30AF"}),")."]}),(0,r.jsxs)(n.p,{children:["Using ",(0,r.jsx)(n.code,{children:'"albumWeight": 0.4'})," and ",(0,r.jsx)(n.code,{children:'"artistWeight": 0.3'})," ensures that the releases that have an album name, and artist names, more similar to your original scrobble are ranked higher. Resulting in ",(0,r.jsxs)(n.a,{href:"https://beta.musicbrainz.org/release/82de33b1-1cd6-4236-b116-561d0ecc8acf",children:["the correct Release (",(0,r.jsx)(n.code,{children:"PERSONA5 ORIGINAL SOUNDTRACK"}),")"]})," being chosen as the final match."]})]}),"\n",(0,r.jsxs)(n.p,{children:["Add one or more of these weight properties to your ",(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," to enable Field Scoring:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // ...\n // enables album text similiarity scoring\n "albumWeight": 0.33,\n // enables title text similiarity scoring\n "titleWeight": 0.33,\n // enables artist text similiarity scoring\n "artistWeight": 0.33\n }\n'})}),"\n",(0,r.jsxs)(n.p,{children:["While not enforced, it's a good idea to keep these weights under ",(0,r.jsx)(n.code,{children:"1"}),". And if using more than one weight, they should be add up to ",(0,r.jsx)(n.code,{children:"1"}),"."]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"How Scoring is Combined"}),(0,r.jsxs)(n.p,{children:["The score similarity from ",(0,r.jsx)(n.code,{children:"albumWeight"})," will be added to the score accumulated by ",(0,r.jsx)(n.a,{href:"#sorting-releases",children:"Release Attributes"})," to affect the final ranking of each Release for a Match/Recording."]}),(0,r.jsx)(n.p,{children:"In the final sorting of Recordings, title score + artist score is combined with the top ranked release album score. The top ranked score is then used to choose the matched Recording."})]}),"\n",(0,r.jsx)(n.h2,{id:"best-practices",children:"Best Practices"}),"\n",(0,r.jsx)(n.h3,{id:"caching",children:"Caching"}),"\n",(0,r.jsxs)(n.p,{children:["You ",(0,r.jsx)(n.strong,{children:"should"})," setup ",(0,r.jsx)(n.a,{href:"/configuration/transforms#caching",children:"metadata caching"})," to reduce API calls, improve transform performance, and reduce memory usage when using this stage."]}),"\n",(0,r.jsx)(n.h3,{id:"sensible-default",children:"Sensible Default"}),"\n",(0,r.jsxs)(n.p,{children:["Generally, the Musicbrainz Stage can be used without any of the ",(0,r.jsx)(n.a,{href:"#configuration",children:"optional configuration"})," and you should still see good results from matches. The top ",(0,r.jsx)(n.a,{href:"#score",children:"scored"})," match is, anecdotally, good enough for correcting and filling in surface-level play data like Title and Artist names."]}),"\n",(0,r.jsx)(n.p,{children:"For a more opinionated match that will mirror what you would expect from data from large music services:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'{\n // use official release over anything else\n "releaseStatusPriority": ["official"],\n // prefer album, then single, then ep\n "releaseGroupPrimaryTypePriority": ["album", "single", "ep"],\n // prefer worldwide release\n "releaseCountryPriority": ["XW"]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Consider adding ",(0,r.jsxs)(n.a,{href:"#search-methods",children:["Artist Extraction (",(0,r.jsx)(n.code,{children:"artist"}),")"]})," in ",(0,r.jsx)(n.strong,{children:"Native Mode"})," if any of your Sources do not support multiple artists or your music collection is not tagged well."]}),"\n",(0,r.jsx)(i,{children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'{\n // use official release over anything else\n "releaseStatusPriority": ["official"],\n // prefer album, then single, then ep\n "releaseGroupPrimaryTypePriority": ["album", "single", "ep"],\n // prefer worldwide release\n "releaseCountryPriority": ["XW"],\n // search using extracted artist as last search attempt\n "searchOrder": ["isrc", "basic", "artist"],\n "searchArtistMethod": "native"\n}\n'})})}),"\n",(0,r.jsx)(n.h3,{id:"search-considerations",children:"Search Considerations"}),"\n",(0,r.jsxs)(n.p,{children:["How complete, and accurate, your scrobble data is from a certain Source should dictate what ",(0,r.jsx)(n.a,{href:"#search-methods",children:"Search Methods"})," you should use for matching."]}),"\n",(0,r.jsxs)(n.p,{children:["Musicbrainz agressively normalizes ",(0,r.jsx)(n.strong,{children:"artist, album, and title"})," fields in its database which means that if your Scrobble data contains major innaccuracies or phrases fields in a non-normal way, it's possible Musicbrainz won't find any matches. This is a more common occurrence when trying to match Scrobble data from Sources that don't support multiple artist data, like ",(0,r.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," and ",(0,r.jsx)(n.a,{href:"/configuration/sources/lastfm-source",children:"Last.fm"}),"."]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Examples of Bad Data"}),(0,r.jsx)(n.p,{children:"Last.fm Scrobble returns an Artist in the title and combines two artists into one string:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility (feat. Wheatus)",\n "artists": ["Bowling For Soup & Punk Rock Factory"]\n}\n'})}),(0,r.jsxs)(n.p,{children:["The corresponding ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/release/85bf284b-8b39-414e-8ad9-61f593072588",children:"Musicbrainz Recording"})," has all artists separated and no artist in the title:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility",\n "artists": ["Bowling For Soup", "Punk Rock Factory", "Wheatus"]\n}\n'})})]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"In general, you should prioritize methods that search by unique identifiers"})," (",(0,r.jsx)(n.code,{children:"isrc"})," ",(0,r.jsx)(n.code,{children:"mbidRecording"})," ",(0,r.jsx)(n.code,{children:"basicOrIds"}),") if you know your Sources may contain ",(0,r.jsx)(n.em,{children:"accurate"})," data for these. Such as: Spotify, Jellyfin, Plex, and Listenbrainz (Source)."]}),"\n",(0,r.jsx)(n.p,{children:"Use field-based methods if your Sources do not return IDs consistently (or at all). Add more field-based methods the more inaccurate your scrobble data may be, but be aware that more aggressive/broad searches may return less accurate results."}),"\n",(0,r.jsxs)(s,{type:"tip",summary:"Matching Scrobbles from Last.fm",children:[(0,r.jsx)(n.p,{children:"Last.fm is inconsistent when it comes to matching your scrobbles. It will accept (almost) any fields for track/title/album and aggressively attempt to match/correct these, regardless of your intentions."}),(0,r.jsxs)(n.p,{children:["Unless the client scrobbling to Last.fm is also sending an ",(0,r.jsx)(n.code,{children:"mbid"}),", it's likely your scrobble will not be ",(0,r.jsx)(n.strong,{children:"consistently"})," matched correctly and so the ",(0,r.jsx)(n.code,{children:"mbid"})," supplied by last.fm in its scrobble data will be wrong."]}),(0,r.jsxs)(n.p,{children:["Therefore, you should ",(0,r.jsx)(n.em,{children:"avoid"})," using ",(0,r.jsx)(n.code,{children:"mbidRecording"})," and ",(0,r.jsx)(n.code,{children:"basicOrIds"})," as the primary search methods (or understand the risks of using them). Instead, prefer to use field-based methods ",(0,r.jsx)(n.em,{children:"before"})," falling back to id-based methods."]})]}),"\n",(0,r.jsx)(n.h3,{id:"filter-considerations",children:"Filter Considerations"}),"\n",(0,r.jsx)(n.p,{children:"When using your own filters consider:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Prefer ",(0,r.jsx)(n.code,{children:"deny"})," over ",(0,r.jsx)(n.code,{children:"allow"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Releases come in all kinds of formats. Since ",(0,r.jsx)(n.code,{children:"allow"})," is explicit you may filter out your desired match without realizing it (correct data except for release type). Or the Musicbrainz data for a higher scored match may be appropriate but you did not include it, exhaustively."]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["Prefer ",(0,r.jsx)(n.a,{href:"#sorting",children:"Sorting"})," over ",(0,r.jsx)(n.a,{href:"#filtering",children:"Filtering"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Sorting does not eliminate any matches. It is, generally, better to get ",(0,r.jsx)(n.strong,{children:"some"})," match than it is to have your Play data completely uncorrected because filtering eliminated all matches"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"using-partial-match",children:"Using Partial Match"}),"\n",(0,r.jsxs)(n.p,{children:["Use ",(0,r.jsx)(n.a,{href:"#rules-and-hooks",children:"Rules"})," to apply MusicBrainz match data selectively."]}),"\n",(0,r.jsxs)(n.p,{children:["If you know that your music collection is well organized and you do not want to change the artists/title/album etc... sent to a ",(0,r.jsx)(n.a,{href:"/configuration/clients",children:"Client"}),", you can still benefit from matches by only applying MBIDs using ",(0,r.jsx)(n.code,{children:"meta"})," so that any Client that supports Musicbrainz data (",(0,r.jsx)(n.a,{href:"/configuration/clients/koito",children:"Koito"}),", ",(0,r.jsx)(n.a,{href:"/configuration/clients/tealfm",children:"Tealfm"}),", ",(0,r.jsx)(n.a,{href:"/configuration/clients/listenbrainz",children:"Listenbrainz"}),", ",(0,r.jsx)(n.a,{href:"/configuration/clients/rocksky",children:"Rocksky"}),") can still get that data."]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "title": false,\n "artists": false,\n "album": false,\n "albumArtists": false,\n "meta": true\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,r.jsx)(n.h2,{id:"logging",children:"Logging"}),"\n",(0,r.jsx)(n.p,{children:"If"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Musicbrainz is not returning matches"}),"\n",(0,r.jsx)(n.li,{children:"Multi-scrobbler is using the wrong match"}),"\n",(0,r.jsx)(n.li,{children:"or the resulting enhanced Scrobble is not what you expected"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Enable logging by turning on ",(0,r.jsx)(n.a,{href:"/configuration#debug-mode",children:(0,r.jsx)(n.strong,{children:"Debug Mode"})})," to help diagnose any issues with the Musicbrainz API and Scrobble enhancement. ",(0,r.jsx)(n.strong,{children:"Before creating an issue"})," please enable logging and include any logs with your issue as this is needed to debug."]}),"\n",(0,r.jsxs)(n.p,{children:["If you have multiple Modification Stages and need to see the diff for your Play between each Stage, enable ",(0,r.jsx)(n.code,{children:'"log": "all"'})," in the individual ",(0,r.jsx)(n.a,{href:"#rules-and-hooks",children:"Modification Stage"}),"."]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(n.p,{children:["In a ",(0,r.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," ",(0,r.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n // highlight-start\n "log": "all",\n // highlight-end\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,r.jsx)(n.h2,{id:"examples",children:"Examples"}),"\n",(0,r.jsx)(n.h3,{id:"minimal",children:"Minimal"}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(n.p,{children:["Your ",(0,r.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ]\n },\n }\n ]\n}\n'})}),(0,r.jsxs)(n.p,{children:["In a ",(0,r.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," ",(0,r.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB"\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,r.jsx)(n.h3,{id:"sensible-default-1",children:"Sensible Default"}),"\n",(0,r.jsxs)(n.p,{children:["Using the config shown in ",(0,r.jsx)(n.a,{href:"#sensible-default",children:"Sensible Default"})," with File/AIO config, instead of ",(0,r.jsx)(n.a,{href:"#env-configuration",children:"ENV Config"}),"."]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(n.p,{children:["Your ",(0,r.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ]\n },\n "defaults": {\n "releaseStatusPriority": ["official"],\n "releaseGroupPrimaryTypePriority": ["album", "single", "ep"],\n "releaseCountryPriority": ["XW"],\n "searchArtistMethod": "native"\n "searchOrder": ["isrc", "basic", "artist"]\n }\n }\n ]\n}\n'})}),(0,r.jsxs)(n.p,{children:["In a ",(0,r.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," ",(0,r.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB"\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,r.jsx)(n.h3,{id:"add-metadata-only-no-scrobble-modification",children:"Add Metadata Only (No Scrobble Modification)"}),"\n",(0,r.jsxs)(n.p,{children:["If you do not want your scrobble data to be modified (artists/album/title) but still want to benefit from associating the scrobble with a Musicbrainz match (for Scrobble ",(0,r.jsx)(n.a,{href:"/configuration/clients",children:"Clients"})," like ",(0,r.jsx)(n.a,{href:"/configuration/clients/tealfm",children:"Tealfm"}),", ",(0,r.jsx)(n.a,{href:"/configuration/clients/rocksky",children:"Rocksky"}),", and ",(0,r.jsx)(n.a,{href:"/configuration/clients/koito",children:"Koito"}),"), then use ",(0,r.jsx)(n.a,{href:"#using-partial-match",children:"Partial Match"})," rules to specify only ",(0,r.jsx)(n.code,{children:"meta"})," to be updated."]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(n.p,{children:["Your ",(0,r.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ]\n },\n "defaults": {\n // maybe something like Sensible Default?\n }\n }\n ]\n}\n'})}),(0,r.jsxs)(n.p,{children:["In a ",(0,r.jsx)(n.a,{href:"/configuration/sources/jellyfin",children:"Jellyfin"})," ",(0,r.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="jellyfin.json"',children:'[\n { \n "name": "MyJellyfin",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "title": false,\n "artists": false,\n "album": false,\n "albumArtists": false,\n "meta": true // only update scrobble metadata with MBIDs\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,r.jsx)(n.h3,{id:"use-native-stage-if-no-musicbrainz-matches",children:"Use Native Stage if no Musicbrainz Matches"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Musicbrainz Stage configured with","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"#sensible-default",children:"Sensible Defaults"})}),"\n",(0,r.jsxs)(n.li,{children:["Fallback search with ",(0,r.jsx)(n.a,{href:"#search-methods",children:"Artist Extraction"})]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"/configuration/transforms/native/#default-stage",children:"Default Native Stage"})}),"\n",(0,r.jsxs)(n.li,{children:["Uses ",(0,r.jsx)(n.a,{href:"/configuration/transforms#flow-control",children:"Flow Control"})," to run Native only if there are no Musicbrainz matches"]}),"\n"]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(n.p,{children:["Your ",(0,r.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ]\n },\n "defaults": {\n "releaseStatusPriority": ["official"],\n "releaseGroupPrimaryTypePriority": ["album", "single", "ep"],\n "releaseCountryPriority": ["XW"],\n "searchArtistMethod": "native",\n "searchOrder": ["isrc","basic", "artist"]\n }\n }\n ]\n}\n'})}),(0,r.jsxs)(n.p,{children:["In a ",(0,r.jsx)(n.a,{href:"/configuration/sources/jellyfin",children:"Jellyfin"})," ",(0,r.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="jellyfin.json"',children:'[\n { \n "name": "MyJellyfin",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n // if musicbrainz is successful then do NOT run native,\n // only run native if musicbrainz fails to find a match (onFailure)\n "type": "musicbrainz",\n "name": "MyMB"\n "onSuccess": "stop",\n "onFailure": "continue"\n },\n {\n "type": "native"\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,r.jsx)(n.h3,{id:"override-stage-configuration-example",children:"Different Search Methods Based on Source (Last.fm and Spotify)"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"#search-considerations",children:"Last.fm has unreliable MBIDs"})," and Spotify provides an ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/doc/ISRC",children:"ISRC"})," and consistent formatting."]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["For Spotify, we want to rely on ID ",(0,r.jsx)(n.a,{href:"#search-methods",children:"searches"})," and matches that are ",(0,r.jsx)(n.a,{href:"#field-scoring",children:"close to original scrobble text"})]}),"\n",(0,r.jsx)(n.li,{children:"For Last.fm, we want to use only text searches and take whatever correction is given to us"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Use ",(0,r.jsx)(n.a,{href:"#rules-and-hooks",children:(0,r.jsx)(n.strong,{children:"Per Component Overrides"})})," with the default (or ",(0,r.jsx)(n.a,{href:"#sensible-default",children:"sensible"}),") ",(0,r.jsx)(n.a,{href:"#stage-configuration",children:"Stage Configuration"})," to specify different search behavior for each Source."]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsx)(n.p,{children:"Stage Configuration"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ],\n "defaults": {} // maybe use sensible?\n },\n }\n ]\n}\n'})}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="lastfm.json"',children:'[\n { \n "name": "MyLFM",\n "configureAs": "source"\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "searchArtistMethod": "native"\n // does not use any ID searches\n // fallback to more aggressive/broad searches\n "searchOrder": ["basic", "artist", "freetext"]\n }\n ]\n }\n }\n }\n]\n'})}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="spotify.json"',children:'[\n { \n "name": "MySpotify",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n // use ISRC as first search\n "searchOrder": ["isrc", "basic"]\n // bias matches towards similarity to original data\n "albumWeight": 0.33,\n "titleWeight": 0.33,\n "artistWeight": 0.33\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,r.jsx)(n.h3,{id:"use-metadata-for-discord-album-art",children:"Use Metadata for Discord Album Art"}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.a,{href:"/configuration/clients/discord",children:"Discord"})," Client can fetch album art images from ",(0,r.jsx)(n.a,{href:"http://coverartarchive.org/",children:"Cover Art Archive"})," if the Scrobble data contains a Release MBID. Use the Musicbrainz Stage on the ",(0,r.jsx)(n.code,{children:"preCompare"})," hook for your Discord client so that missing Musicbrainz IDs are populated for this functionality."]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(t.A,{groupId:"configType",queryString:!0,children:[(0,r.jsxs)(o.A,{value:"env",label:"ENV",children:[(0,r.jsxs)(n.p,{children:["Using the ",(0,r.jsx)(n.a,{href:"#env-configuration",children:"ENV Configuration"})," from above, add these fields to your docker compose ",(0,r.jsx)(n.code,{children:"environment:"})]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"- MB_CONTACT=contact@mydomain.com\n- MB_PRESETS=id,native,sensible\n- DISCORD_TRANSFORMS=musicbrainz\n"})})]}),(0,r.jsxs)(o.A,{value:"file",label:"File",children:[(0,r.jsxs)(n.p,{children:["Your ",(0,r.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ]\n },\n "defaults": {\n // sensible defaults\n "releaseStatusPriority": ["official"],\n "releaseGroupPrimaryTypePriority": ["album", "single", "ep"],\n "releaseCountryPriority": ["XW"],\n "searchArtistMethod": "native",\n "searchOrder": ["isrc","basic", "artist"]\n }\n }\n ]\n}\n'})}),(0,r.jsxs)(n.p,{children:["In your ",(0,r.jsx)(n.a,{href:"/configuration/clients/discord",children:"Discord"})," ",(0,r.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="discord.json"',children:'[\n { \n "name": "MS",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n }\n ]\n }\n }\n }\n]\n'})})]}),(0,r.jsxs)(o.A,{value:"aio",label:"AIO",children:[(0,r.jsxs)(n.p,{children:["Your ",(0,r.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ]\n },\n "defaults": {\n // sensible defaults\n "releaseStatusPriority": ["official"],\n "releaseGroupPrimaryTypePriority": ["album", "single", "ep"],\n "releaseCountryPriority": ["XW"],\n "searchArtistMethod": "native",\n "searchOrder": ["isrc","basic", "artist"]\n }\n }\n ],\n "sources": [\n { \n "name": "MS",\n "type": "discord",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n }\n ]\n }\n }\n }\n ]\n}\n'})})]})]})]})]})}function m(e={}){let{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(u,{...e})}):u(e)}function f(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}}}]); \ No newline at end of file diff --git a/assets/js/e6ac3f64.734826d9.js b/assets/js/e6ac3f64.734826d9.js deleted file mode 100644 index 6928041f..00000000 --- a/assets/js/e6ac3f64.734826d9.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["1184"],{265(e,n,i){i.r(n),i.d(n,{metadata:()=>s,default:()=>m,frontMatter:()=>l,contentTitle:()=>c,toc:()=>h,assets:()=>d});var s=JSON.parse('{"id":"configuration/transforms/musicbrainz","title":"Musicbrainz Stage","description":"The Musicbrainz Stage matches your Play data with the MusicBrainz database. If the match score is high enough then Multi-Scrobbler uses the match to correct and fill-in missing information in your Play data.","source":"@site/docs/configuration/transforms/musicbrainz.mdx","sourceDirName":"configuration/transforms","slug":"/configuration/transforms/musicbrainz","permalink":"/multi-scrobbler/configuration/transforms/musicbrainz","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/transforms/musicbrainz.mdx","tags":[],"version":"current","frontMatter":{"title":"Musicbrainz Stage","toc_min_heading_level":2,"toc_max_heading_level":5},"sidebar":"tutorialSidebar","previous":{"title":"Enhancing Scrobbles","permalink":"/multi-scrobbler/configuration/transforms/"},"next":{"title":"Native Stage","permalink":"/multi-scrobbler/configuration/transforms/native"}}'),r=i(4848),a=i(8453),t=i(8194),o=i(698);let l={title:"Musicbrainz Stage",toc_min_heading_level:2,toc_max_heading_level:5},c,d={},h=[{value:"Configuration",id:"configuration",level:2},{value:"API Setup",id:"api-setup",level:3},{value:"Stage Configuration",id:"stage-configuration",level:3},{value:"Rules and Hooks",id:"rules-and-hooks",level:3},{value:"ENV Configuration",id:"env-configuration",level:2},{value:"Matching with Musicbrainz",id:"matching-with-musicbrainz",level:2},{value:"Searching",id:"searching",level:3},{value:"Should MS Search?",id:"should-ms-search",level:4},{value:"Search Methods",id:"search-methods",level:4},{value:"Refining",id:"refining",level:3},{value:"Score",id:"score",level:3},{value:"Filtering",id:"filtering",level:3},{value:"Release Attributes",id:"release-attributes",level:5},{value:"Empty Releases",id:"empty-releases",level:4},{value:"Sorting",id:"sorting",level:3},{value:"Release Attributes",id:"sorting-releases",level:4},{value:"Field Scoring",id:"field-scoring",level:4},{value:"Best Practices",id:"best-practices",level:2},{value:"Caching",id:"caching",level:3},{value:"Sensible Default",id:"sensible-default",level:3},{value:"Search Considerations",id:"search-considerations",level:3},{value:"Filter Considerations",id:"filter-considerations",level:3},{value:"Using Partial Match",id:"using-partial-match",level:3},{value:"Logging",id:"logging",level:2},{value:"Examples",id:"examples",level:2},{value:"Minimal",id:"minimal",level:3},{value:"Sensible Default",id:"sensible-default-1",level:3},{value:"Add Metadata Only (No Scrobble Modification)",id:"add-metadata-only-no-scrobble-modification",level:3},{value:"Use Native Stage if no Musicbrainz Matches",id:"use-native-stage-if-no-musicbrainz-matches",level:3},{value:"Different Search Methods Based on Source (Last.fm and Spotify)",id:"override-stage-configuration-example",level:3},{value:"Use Metadata for Discord Album Art",id:"use-metadata-for-discord-album-art",level:3}];function u(e){let n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",h5:"h5",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.R)(),...e.components},{Details:i,DetailsAdmo:s}=n;return i||f("Details",!0),s||f("DetailsAdmo",!0),(0,r.jsxs)(r.Fragment,{children:[(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.strong,{children:"Musicbrainz"})," ",(0,r.jsx)(n.a,{href:"/configuration/transforms#stage",children:"Stage"})," matches your Play data with the ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/",children:"MusicBrainz"})," database. If the match score is high enough then Multi-Scrobbler uses the match to correct and fill-in missing information in your Play data."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"This Stage is useful for standardizing your Scrobble's Play data, regardless of the Source it is coming from."})}),"\n",(0,r.jsxs)(n.p,{children:["Even if you have have strong opinions about the way your existing music is already tagged/organized you should consider adding the Musicbrainz Stage to your Sources, if only to benefit from adding ",(0,r.jsx)(n.a,{href:"#using-partial-match",children:"MBIDs to scrobbled metadata."})]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Why Should I Prefer MusicBrainz?"}),(0,r.jsx)("h3",{children:"More Than Mainstream Data"}),(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/statistics",children:"MusicBrainz database contains"})]}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"37 million+ tracks"}),"\n",(0,r.jsx)(n.li,{children:"5 million+ albums"}),"\n",(0,r.jsx)(n.li,{children:"2.7 million+ artists"}),"\n"]}),(0,r.jsx)(n.p,{children:"in over 200 languages, contributed by 10,000+ editors each year with many more users voting on edits."}),(0,r.jsx)(n.p,{children:"The database covers a much wider range of music than most commercial services since it isn't restricted by licensing or incentivized to prioritize mainstream music due to business interests. You are much more likely to find data for obscure, indie, and international artists."}),(0,r.jsx)("h3",{children:"As The Artist Intended"}),(0,r.jsx)(n.p,{children:'While some large services (Spotify) may be more consistent in data structure, the MusicBrainz equivalent data is likely to be more true to the way the Artist originally intended since individuals can devote effort to individual artists. A fan of an artist is much more likely to "get it right" for their favorite band than a corporate business trying to do the least amount of work for the largest result.'})]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["If you are using ",(0,r.jsx)(n.a,{href:"/configuration?config-type=env#configuration-types",children:"ENV Config"})," for multi-scrobbler and just want a quick and easy setup, skip to ",(0,r.jsx)(n.a,{href:"#env-configuration",children:(0,r.jsx)(n.strong,{children:"ENV Configuration"})}),"."]})}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["Set up ",(0,r.jsx)(n.a,{href:"/configuration?cachedThings=metadata#caching",children:"Valkey Caching"})," to cache Musicbrainz API calls for faster processing."]})}),"\n",(0,r.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsx)(n.h3,{id:"api-setup",children:"API Setup"}),"\n",(0,r.jsxs)(n.p,{children:["To avoid rate limiting, MusicBrainz requires API users to identify themself using a ",(0,r.jsx)(n.a,{href:"https://wiki.musicbrainz.org/MusicBrainz_API/Rate_Limiting#How_can_I_be_a_good_citizen_and_be_smart_about_using_the_Web_Service?",children:"User-Agent that contains an email or URL."})," You must set this in the ",(0,r.jsx)(n.code,{children:"data"})," for your ",(0,r.jsx)(n.a,{href:"/configuration/transforms/#configuring-stages",children:"stage configuration"})," in your ",(0,r.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ]\n },\n }\n ]\n}\n'})}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Additional Musicbrainz Servers"}),(0,r.jsx)(n.p,{children:"Other, or additional, Musicbrainz Servers/Mirrors can be added to the API configuration. If more than one server is defined then multi-scrobbler will round-robin load balance API calls."}),(0,r.jsxs)(n.p,{children:["Use ",(0,r.jsx)(n.code,{children:"url"})," to define the base URL of the Musicbrainz server to use. If ",(0,r.jsx)(n.code,{children:"url"})," is not defined multi-scrobbler assumes it is the primary Musicbrainz server, ",(0,r.jsx)(n.code,{children:"https://musicbrainz.org"}),"."]}),(0,r.jsx)(n.p,{children:"Example of multiple servers:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n // uses default Musicbrainz server https://musicbrainz.org\n },\n // additional server\n {\n "contact": "contact@mydomain.com",\n "url": "https://my.mb.mirror.domain.com"\n }\n ]\n },\n }\n ]\n}\n'})})]}),"\n",(0,r.jsx)(n.h3,{id:"stage-configuration",children:"Stage Configuration"}),"\n",(0,r.jsxs)(n.p,{children:["All of the properties found in ",(0,r.jsx)(n.a,{href:"#matching-with-musicbrainz",children:(0,r.jsx)(n.strong,{children:"Matching with Musicbrainz"})})," section are configured in ",(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," as ",(0,r.jsx)(n.code,{children:"defaults"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"Example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ],\n "defaults": {\n "releaseStatusPriority": ["official"],\n "releaseAllowEmpty": true\n }\n },\n }\n ]\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"rules-and-hooks",children:"Rules and Hooks"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"https://foxxmd.github.io/multi-scrobbler/configuration/transforms/#stage",children:"Add your Stage"})," to a Source or Client by specifying it in a ",(0,r.jsx)(n.a,{href:"/configuration/transforms/#hook",children:"Hook"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB"\n }\n ]\n }\n }\n }\n]\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Each ",(0,r.jsx)(n.a,{href:"/configuration/transforms#stage-rules",children:(0,r.jsx)(n.strong,{children:"Stage Rule"})})," should be either a boolean, specifying if the transformed data should be used for this field, or a ",(0,r.jsxs)(n.a,{href:"/configuration/transforms#conditional-modification",children:[(0,r.jsx)(n.code,{children:"when"})," condition"]}),":"]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB"\n // ...\n "title": false, // will not apply any changes to Play title\n "artists": {\n "when": {/* ... */}, // will only apply changes to Play artists if "when" is satisfied\n /* ... */\n },\n "album": true // will always apply changes to Play album\n "meta": true // adds MusicBrainz MBIDs to scrobble data\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,r.jsxs)(n.admonition,{title:"Per Component Override",type:"tip",children:[(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"defaults"})," you set in ",(0,r.jsx)(n.a,{href:"#stage-configuration",children:"Stage Configuration"})," ",(0,r.jsx)(n.a,{href:"/configuration/transforms/#overriding-configuration",children:"can be overriden/added to"})," (per property) in each Hook."]}),(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "releaseStatusPriority": ["psuedo-release"], // override from defaults\n "searchOrder": ["freetext"] // add new property, combined with defaults\n }\n ]\n }\n }\n }\n]\n'})})]})]}),"\n",(0,r.jsx)(n.h2,{id:"env-configuration",children:"ENV Configuration"}),"\n",(0,r.jsxs)(n.p,{children:["The general configuration shown above can also be configured from a selection of ",(0,r.jsx)(n.em,{children:"presets"})," using ",(0,r.jsx)(n.a,{href:"/configuration?configType=env#configuration-types",children:"ENV Config"})," for individual Sources/Clients."]}),"\n",(0,r.jsxs)(n.p,{children:["You must ",(0,r.jsx)(n.strong,{children:"always"})," include the ",(0,r.jsx)(n.code,{children:"MB_CONTACT"})," ENV to ",(0,r.jsx)(n.a,{href:"#api-setup",children:"identify your application"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ini",children:"MB_CONTACT=contact@mydomain.com\n"})}),"\n",(0,r.jsxs)(n.p,{children:["To configure ",(0,r.jsx)(n.a,{href:"#stage-configuration",children:"stage defaults"})," use ",(0,r.jsx)(n.code,{children:"MB_PRESETS"})," with a comma-delimited list of presets you wish to apply. More than one preset can be applied, in which case they combine. You must choose at least one of the presets below:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"default"})," - Adds the default ",(0,r.jsx)(n.a,{href:"#search-methods",children:(0,r.jsx)(n.code,{children:"searchOrder"})})," ",(0,r.jsx)(n.code,{children:"isrc"}),"+",(0,r.jsx)(n.code,{children:"basic"})," (same as using no presets that affect ",(0,r.jsx)(n.code,{children:"searchOrder"}),")"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"id"})," - Adds ",(0,r.jsx)(n.a,{href:"#search-methods",children:(0,r.jsx)(n.code,{children:"searchOrder"})})," methods that priorites searching by ID: ",(0,r.jsx)(n.code,{children:"isrc"})," + ",(0,r.jsx)(n.code,{children:"mbidrecording"})," + ",(0,r.jsx)(n.code,{children:"basicOrIds"})," + ",(0,r.jsx)(n.code,{children:"basic"})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"native"})," - Adds ",(0,r.jsx)(n.strong,{children:"Extracted Artists"})," (",(0,r.jsx)(n.code,{children:"artist"}),") in ",(0,r.jsx)(n.code,{children:"native"})," mode to ",(0,r.jsx)(n.a,{href:"#search-methods",children:(0,r.jsx)(n.code,{children:"searchOrder"})})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"aggressive"})," - Adds ",(0,r.jsx)(n.strong,{children:"Free Text Search"})," (",(0,r.jsx)(n.code,{children:"freetext"}),") to ",(0,r.jsx)(n.a,{href:"#search-methods",children:(0,r.jsx)(n.code,{children:"searchOrder"})})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"fields"})," - Applies ",(0,r.jsx)(n.a,{href:"#field-scoring",children:"Field Scoring"})," to match sorting so match text is baised towards similarity to original scrobble"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"sensible"})," - Applies the ",(0,r.jsx)(n.a,{href:"#sensible-default",children:"Sensible Default"})," configuration for ",(0,r.jsx)(n.a,{href:"#sorting-releases",children:"sorting releases"})]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Finally, use ENV ",(0,r.jsx)(n.code,{children:"*_TRANSFORMS=musicbrainz"})," on each Source/Client you wish to apply this stage to. This applies the stage in the ",(0,r.jsxs)(n.a,{href:"/configuration/transforms#lifecycle-hooks",children:[(0,r.jsx)(n.code,{children:"preTransform"})," Hook"]})," with all ",(0,r.jsx)(n.a,{href:"#rules-and-hooks",children:"Rules"})," enabled."]}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"*"})," stands for the prefix used for each Source/Client's ENV keys. Refer to the individual Source/Client Configuration sections to find this. Example:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["All ",(0,r.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic Source"})," ENVs look like ",(0,r.jsx)(n.code,{children:"SUBSONIC_USER=myuser"})," etc..."]}),"\n",(0,r.jsxs)(n.li,{children:["Use ",(0,r.jsx)(n.code,{children:"SUBSONIC_TRANSFORMS=musicbrainz"})]}),"\n"]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example Full Docker Deploy with ENV Configuration"}),(0,r.jsxs)(n.p,{children:["Using ",(0,r.jsx)(n.a,{href:"/quickstart#create-docker-compose-file",children:"Jellyfin + Maloja"})," example from Quickstart:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:'services:\n multi-scrobbler:\n image: foxxmd/multi-scrobbler\n container_name: multi-scrobbler\n environment:\n // highlight-start\n - MB_CONTACT=contact@mydomain.com\n # searches with sensible release sorting with the searchOrder\n # isrc => basic => artist (native mode)\n - MB_PRESETS=default,sensible,native\n // highlight-end\n - JELLYFIN_URL=192.168.0.110:8096\n - JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56b\n - JELLYFIN_USER=MyUser\n // highlight-start\n # applies musicbrainz Stage to preTransform of Jellyfin source\n - JELLYFIN_TRANSFORMS=musicbrainz\n // highlight-end\n\n # maloja receives enhanced scrobble from Jellyfin\n - MALOJA_URL=http://192.168.0.100:42010\n - MALOJA_API_KEY=myApiKey\n\n\n volumes:\n - "./config:/config"\n ports:\n - "9078:9078"\n restart: unless-stopped\n'})})]}),"\n",(0,r.jsx)(n.h2,{id:"matching-with-musicbrainz",children:"Matching with Musicbrainz"}),"\n",(0,r.jsxs)(n.admonition,{type:"note",children:[(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"All properties found in this section are optional."})}),(0,r.jsxs)(n.p,{children:["If you just want a sensible default for configuraion refer to the ",(0,r.jsx)(n.a,{href:"#best-practices",children:"Best Practices"})," section."]})]}),"\n",(0,r.jsx)(n.p,{children:"Matching your Scrobble's Play data with a result from Musicbrainz is comprised of two steps:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"#searching",children:(0,r.jsx)(n.strong,{children:"Searching"})})," Musicbrainz using parts of your Scrobble as queries"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"#refining",children:(0,r.jsx)(n.strong,{children:"Refining"})})," matched results to select the desired/allowed match"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"Both steps have separate configuration."}),"\n",(0,r.jsx)(n.h3,{id:"searching",children:"Searching"}),"\n",(0,r.jsx)(n.h4,{id:"should-ms-search",children:"Should MS Search?"}),"\n",(0,r.jsx)(n.p,{children:"Before MS begins a search it checks if your Scrobble data already contains:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"https://musicbrainz.org/doc/MusicBrainz_Identifier",children:"MBIDs"})," for ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/doc/Artist",children:"artists"}),", ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/doc/Release",children:"album"}),", and specific ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/doc/Recording",children:"track/recording"})]}),"\n",(0,r.jsx)(n.li,{children:"and duration"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["If it already has all required data types then the entire Musicbrainz Stage ",(0,r.jsxs)(n.a,{href:"/configuration/transforms/#flow-control",children:["is ",(0,r.jsx)(n.strong,{children:"skipped"}),"."]})," If any are missing then a search is performed."]}),"\n",(0,r.jsx)(n.p,{children:"Define which data types are required using:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"searchWhenMissing"})," (defaults to all) - A list containing any of: ",(0,r.jsx)(n.code,{children:"artists"})," ",(0,r.jsx)(n.code,{children:"title"})," ",(0,r.jsx)(n.code,{children:"album"})," ",(0,r.jsx)(n.code,{children:"duration"})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"forceSearch"})," (default ",(0,r.jsx)(n.code,{children:"false"}),") - Force searching even if all required data is present"]}),"\n"]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // only search if MBIDs for artists or album is missing\n "searchWithMissing": ["artists", "album"]\n\n // uncomment to make the stage always search, even if above data is present\n //forceSearch: true\n }\n'})})]}),"\n",(0,r.jsxs)(n.p,{children:["If required data is missing then the next step is making ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/doc/MusicBrainz_API/Search",children:"search queries to the Musicbrainz database"})," to try to get potential candidates."]}),"\n",(0,r.jsx)(n.h4,{id:"search-methods",children:"Search Methods"}),"\n",(0,r.jsxs)(n.p,{children:["Multi-scrobbler can search for Musicbrainz matches using ",(0,r.jsx)(n.strong,{children:"multiple"})," methods. You control:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"the search methods that are used"}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"the order"})," in which search methods are executed"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"All search methods will be run in the order they were defined until one method returns at least one match."}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"The methods you choose, and the order they run in, can drastically change how many API calls are needed to get a match, and what types of matches are returned."})," See ",(0,r.jsx)(n.a,{href:"#search-considerations",children:(0,r.jsx)(n.strong,{children:"Search Considerations"})})," for guidance on how to decide on methods based on Source."]}),"\n",(0,r.jsxs)(n.p,{children:["To set search methods and their order use the ",(0,r.jsx)(n.code,{children:"searchOrder"})," option in your ",(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // only searches using these methods, in the order they are listed here\n "searchOrder": ["isrc", "mbidrecording", "basic","freetext"]\n }\n'})}),"\n",(0,r.jsx)(n.admonition,{title:"Default Search Methods",type:"tip",children:(0,r.jsxs)(n.p,{children:["If ",(0,r.jsx)(n.code,{children:"searchOrder"})," is undefined then Multi-scrobbler will default to using ",(0,r.jsx)(n.code,{children:"isrc"})," then ",(0,r.jsx)(n.code,{children:"basic"})," method."]})}),"\n",(0,r.jsx)("h4",{children:"Available Methods"}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsxs)("summary",{children:["ISRC (",(0,r.jsx)(n.code,{children:"isrc"}),")"]}),(0,r.jsxs)(n.p,{children:["If your Scrobble data contains an ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/doc/ISRC",children:"ISRC"})," (usually from Spotify) then Multi-scrobbler can search using this ID, only."]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // will only search using isrc\n "searchOrder": ["isrc"]\n }\n'})})]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsxs)("summary",{children:["Track MBID (",(0,r.jsx)(n.code,{children:"mbidrecording"}),")"]}),(0,r.jsxs)(n.p,{children:["If your Scrobble data contains an ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/doc/Recording",children:"Track (Recording) MBID"})," then Multi-scrobbler can search using this MBID, only."]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // will only search using recording mbid\n "searchOrder": ["mbidrecording"]\n }\n'})})]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsxs)("summary",{children:["Album, Artist, and Title Fields (",(0,r.jsx)(n.code,{children:"basic"}),")"]}),(0,r.jsx)(n.p,{children:"Will search Musicbrainz using any/all available text fields from your scrobble: Album, Artists(s), and Title. Does not leverage any existing MBIDs."}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // will only search using album, artist(s), and title\n "searchOrder": ["basic"]\n }\n'})})]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsxs)("summary",{children:["Album/Artist/Title OR MBIDs (",(0,r.jsx)(n.code,{children:"basicOrIds"}),")"]}),(0,r.jsxs)(n.p,{children:["This is the same as the above ",(0,r.jsx)(n.strong,{children:"Album, Artist, and Title Fields"})," search except that if the scrobble data contains any corresponding MBIDs it will use those instead of the plain text field."]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // will only search using album, artist(s), and title\n "searchOrder": ["basicOrIds"]\n }\n'})}),(0,r.jsx)(n.p,{children:"Example:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility (feat. Wheatus)",\n "artists": ["Bowling For Soup"]\n "albumMbid": "c9f91cdc-984e-4303-9a51-4ac0dfa2348f",\n "album": "My Cool Album"\n}\n'})}),(0,r.jsxs)(n.p,{children:["Multi-scrobbler will search Musicbrainz using ",(0,r.jsx)(n.code,{children:"title"}),", ",(0,r.jsx)(n.code,{children:"artists"}),", and ",(0,r.jsx)(n.code,{children:"albumMbid"})," but NOT ",(0,r.jsx)(n.code,{children:"album"}),"."]})]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsxs)("summary",{children:["Album And Title Only (",(0,r.jsx)(n.code,{children:"album"}),")"]}),(0,r.jsxs)(n.p,{children:["If your Scrobble data contains a title, artist(s), and an album (all three fields) then Multi-scrobbler can search using only title and album by using ",(0,r.jsx)(n.code,{children:"album"})," for ",(0,r.jsx)(n.code,{children:"searchOrder"}),":"]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // will only search using only title + album name\n "searchOrder": ["album"]\n }\n'})})]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsxs)("summary",{children:["Extracted Artists (",(0,r.jsx)(n.code,{children:"artist"}),")"]}),(0,r.jsxs)(n.p,{children:["If your scrobble data contains only ",(0,r.jsx)(n.strong,{children:"one"})," artist string then Multi-scrobbler can attempt to extract multiple artists from your artist and track string."]}),(0,r.jsxs)(n.p,{children:["Use ",(0,r.jsx)(n.code,{children:"artist"})," with ",(0,r.jsx)(n.code,{children:"searchOrder"})," and optionally specify the extraction mode with ",(0,r.jsx)(n.code,{children:"searchArtistMethod"}),":"]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // will only search using title/album + extracted artists\n "searchOrder": ["artist"],\n //searchArtistMethod: \'native\' // optional\n }\n'})}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"searchArtistMethod"})," can be set to ",(0,r.jsx)(n.strong,{children:"Native"})," or ",(0,r.jsx)(n.strong,{children:"Naive"})," mode:"]}),(0,r.jsxs)(t.A,{groupId:"searchArtistMethod",queryString:!0,children:[(0,r.jsxs)(o.A,{value:"native",label:"Native (Recommended)",children:[(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Native"})," (",(0,r.jsx)(n.code,{children:"native"}),") mode uses an aggressive configuration of the ",(0,r.jsx)(n.a,{href:"/configuration/transforms/native",children:"Native Stage"}),' to extract artists using common delimiters and common "joined" artist patterns from the artist and title string of your Scrobble data.']}),(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsx)(n.p,{children:"If you already have a Native Stage configured you should use that instead, running it before the Musicbrainz stage."})}),(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Native Mode Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility (feat. Wheatus)",\n "artists": ["Bowling For Soup & Punk Rock Factory, My Cool Band"]\n}\n'})}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Extracts ",(0,r.jsx)(n.code,{children:"Bowling For Soup"})," ",(0,r.jsx)(n.code,{children:"Punk Rock Factory"})," ",(0,r.jsx)(n.code,{children:"My Cool Band"})," from artist string"]}),"\n",(0,r.jsxs)(n.li,{children:["Extracts ",(0,r.jsx)(n.code,{children:"Wheatus"})," from title string","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Removes ",(0,r.jsx)(n.code,{children:"(feat. Wheatus)"})," from title string because it found an artist there"]}),"\n"]}),"\n"]}),"\n"]}),(0,r.jsx)(n.p,{children:"Resulting data used for Musicbrainz search:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility",\n "artists": ["Bowling For Soup", "Punk Rock Factory", "My Cool Band", "Wheatus"]\n}\n'})})]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // will only search using title/album + native extracted artists\n "searchOrder": ["artist"],\n "searchArtistMethod": "native"\n }\n'})})]}),(0,r.jsxs)(o.A,{value:"naive",label:"Naive",children:[(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Naive"})," (",(0,r.jsx)(n.code,{children:"naive"}),") mode looks for the first found common delimiter in the artist string. If it finds one then it uses the preceding value as the only artist in the Musicbrainz search. It does not try to extract additional artists from the artist string, or extract anything from the title string."]}),(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Naive Mode Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility (feat. Wheatus)",\n "artists": ["Bowling For Soup, Punk Rock Factory & Wheatus"]\n}\n'})}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Finds ",(0,r.jsx)(n.code,{children:"&"}),' as first common delimiter, extracts "Bowling For Soup"']}),"\n"]}),(0,r.jsx)(n.p,{children:"Resulting data used for Musicbrainz search:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility (feat. Wheatus)",\n "artists": ["Bowling For Soup"]\n}\n'})})]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // will only search using title/album + naive extracted artists\n "searchOrder": ["artist"],\n "searchArtistMethod": "naive"\n }\n'})})]})]})]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsxs)("summary",{children:["Free Text (",(0,r.jsx)(n.code,{children:"freetext"}),")"]}),(0,r.jsxs)(n.p,{children:["This will search Musicbrainz for ",(0,r.jsx)(n.strong,{children:"all"})," text of your Scrobble data artist/title/album, without constraint."]}),(0,r.jsxs)(n.admonition,{type:"warning",children:[(0,r.jsxs)(n.p,{children:["Free text search is ",(0,r.jsx)(n.strong,{children:"unconstrained"})," which means that Musicbrainz will match text in ",(0,r.jsx)(n.strong,{children:"any"})," part of a Recording (artist, release, or recording field), regardless of where it is found. This may lead to results that are unexpected and undesired."]}),(0,r.jsxs)(n.p,{children:["If you know your Source's Play data is well organized you ",(0,r.jsx)(n.strong,{children:"should not"})," enable this. Free Text search should only be used if:"]}),(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"your music is not well organized or"}),"\n",(0,r.jsxs)(n.li,{children:["there may be many alternative titles/artists for the music you listen to (such as with ",(0,r.jsx)(n.a,{href:"https://wiki.musicbrainz.org/Release#Status",children:"psuedo-releases"}),")"]}),"\n",(0,r.jsxs)(n.li,{children:["you can tolerate that matches may not be accurate for releases.","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Generally, if you keep ",(0,r.jsx)(n.a,{href:"#score",children:"score"})," high then matched releases should ",(0,r.jsx)(n.em,{children:"at least"}),' be the right artist or what you would expect within a reason, as a match. The release may not be the one you actually listened to but it would be "close enough".']}),"\n"]}),"\n"]}),"\n"]})]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," example:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // ...\n // will only search using free text search of all artist/album/title\n "searchOrder": ["freetext"],\n }\n'})})]}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["If all defined search methods do not return any results then the stage is marked as ",(0,r.jsxs)(n.a,{href:"/configuration/transforms/#flow-control",children:[(0,r.jsx)(n.strong,{children:"failed"})," (",(0,r.jsx)(n.code,{children:"onFailure"}),") for ",(0,r.jsx)(n.strong,{children:"Flow Control"})]}),"."]})}),"\n",(0,r.jsx)(n.h3,{id:"refining",children:"Refining"}),"\n",(0,r.jsx)(n.h3,{id:"score",children:"Score"}),"\n",(0,r.jsxs)(n.p,{children:["Each match returned by MusicBrainz contains a numeric score representing how close it was to the search parameters. Set ",(0,r.jsx)(n.code,{children:"score"})," in configuration to set a minimum score that must be met by matches. Default is ",(0,r.jsx)(n.code,{children:"90"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'{\n // ...\n "score": 90 // matches must score 90 or higher to be considered\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"filtering",children:"Filtering"}),"\n",(0,r.jsxs)(n.p,{children:["There are several attributes associated with the ",(0,r.jsx)(n.a,{href:"https://wiki.musicbrainz.org/Release",children:"Release"})," a ",(0,r.jsx)(n.a,{href:"https://wiki.musicbrainz.org/Recording",children:"Recording"})," (individual Track/song) belongs to that you may be interested in controlling. MS can use these attributes to filter what the final Recording selected to match against your Play data is."]}),"\n",(0,r.jsx)(n.p,{children:"An easy way to think about this:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Do you prefer Albums, Singles, or EPs?"}),"\n",(0,r.jsx)(n.li,{children:"Do you always want to use an official album release, or are bootleg or cancelled albums okay to use?"}),"\n",(0,r.jsx)(n.li,{children:"Should a track that belongs to a Compilation album be allowed to match?"}),"\n",(0,r.jsx)(n.li,{children:"What country do you prefer an album release to be from?"}),"\n"]}),"\n",(0,r.jsx)(n.h5,{id:"release-attributes",children:"Release Attributes"}),"\n",(0,r.jsxs)(n.p,{children:["These attributes (",(0,r.jsx)(n.code,{children:"attribute_name"})," in config) are:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.a,{href:"https://wiki.musicbrainz.org/Release#Status",children:["Release ",(0,r.jsx)(n.strong,{children:"Status"})]})," (",(0,r.jsx)(n.code,{children:"releaseStatus"}),") - Is this release/album/ep official, promotional, bootleg, etc..."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.a,{href:"https://wiki.musicbrainz.org/Release_Group/Type#Primary_types",children:["Release Group ",(0,r.jsx)(n.strong,{children:"Primary Type"})]})," (",(0,r.jsx)(n.code,{children:"releaseGroupPrimaryType"}),") - Is this release an Album, Single, EP, etc..."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.a,{href:"https://wiki.musicbrainz.org/Release_Group/Type#Secondary_types",children:["Release Group ",(0,r.jsx)(n.strong,{children:"Secondary Type"})]})," (",(0,r.jsx)(n.code,{children:"releaseGroupSecondaryType"}),") - Is this release a compilation, soundtrack, remix, etc..."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.a,{href:"https://beta.musicbrainz.org/doc/Release/Country",children:["Release ",(0,r.jsx)(n.strong,{children:"Country"})]})," (",(0,r.jsx)(n.code,{children:"releaseCountry"}),") - What ISO2 country was this released in? US, GB, MX, etc...","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["MusicBrainz uses the special code ",(0,r.jsx)(n.code,{children:"XW"}),' to represent a "Worldwide" release AKA release not made specifically for a country']}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Each of the above attributes can be used to ",(0,r.jsx)(n.strong,{children:"filter"})," matches using ",(0,r.jsx)(n.code,{children:"allow"})," (explicitly include) or ",(0,r.jsx)(n.code,{children:"deny"})," (exclude) terms. The correct property name for each is like so:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:'[attribute_name]Allow = [...]\n[attribute_name]Deny\n\nEX to explicitly allow only release status "official"\n\n"releaseStatusAllow": ["official"]\n\nEX to exclude compilations\n\n"releaseGroupSecondaryTypeDeny": ["compilation"]\n'})}),"\n",(0,r.jsx)(n.h4,{id:"empty-releases",children:"Empty Releases"}),"\n",(0,r.jsx)(n.p,{children:"Sometimes a Recording may not have any associated Releases. This may be because there is not enough information about the Recording yet, or it was never included on an actual Release."}),"\n",(0,r.jsxs)(n.p,{children:["You may want to filter the majority of your matches by releases but allow matching a Recording that has no releases to begin with. To allow this set ",(0,r.jsx)(n.code,{children:"releaseAllowEmpty"})," to ",(0,r.jsx)(n.code,{children:"true"})," in configuration:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // ...\n "releaseAllowEmpty": true // don\'t remove a match during filtering just because it has no releases\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"sorting",children:"Sorting"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.em,{children:"After"})," matches have been ",(0,r.jsx)(n.a,{href:"#filtering",children:"filtered"}),", the remaining matches ",(0,r.jsx)(n.em,{children:"and their releases"}),"* can be sorted. Matches/Releases with an attribute that does not match are ",(0,r.jsx)(n.strong,{children:"not"})," removed, but they are sorted lower than matches that do match."]}),"\n",(0,r.jsxs)(n.p,{children:["Sorting can be a good alternative to filtering: with filters there is a possibility your filters may eliminate all matches; if you want to ensure that ",(0,r.jsx)(n.strong,{children:"some"})," match will be used then sorting can ensure that the ",(0,r.jsx)(n.strong,{children:"best choice"})," out of those given will always be used, without accidentally ending up with ",(0,r.jsx)(n.strong,{children:"no choice"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"Sorting can be used instead of filters, or in conjunction with filters, it's your choice."}),"\n",(0,r.jsx)(n.h4,{id:"sorting-releases",children:"Release Attributes"}),"\n",(0,r.jsxs)(n.p,{children:["Each ",(0,r.jsx)(n.a,{href:"#release-attributes",children:"Release Attribute"})," has one additional property that can be used to ",(0,r.jsx)(n.strong,{children:"rank"})," releases based on the order of the values you give it. This is the ",(0,r.jsx)(n.code,{children:"priority"})," property."]}),"\n",(0,r.jsx)(n.p,{children:"In configuration:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:'[attribute_name]Priority = [...]\n\nEX to prefer "official" releases over everything else\n\n"releaseStatusPriority": ["official"]\n\nEX to prefer albums, then singles, over everything else\n\n"releaseGroupPrimaryTypePriority": ["album", "single"]\n'})}),"\n",(0,r.jsx)(n.h4,{id:"field-scoring",children:"Field Scoring"}),"\n",(0,r.jsx)(n.p,{children:'Matches can additionally be sorted/ranked based on how similar their fields are to the original Scrobble. Use these properties for sorting when you want the final Musicbrainz match to be as closely matched to the original Scrobble, as opposed to what may be the most "correct" match based on the Musicbrainz score or suggested Release.'}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example Scenario"}),(0,r.jsxs)(n.p,{children:["You scrobble ",(0,r.jsx)(n.a,{href:"https://open.spotify.com/track/21trRQA61afzljSK9V0SHX",children:"this track from spotify"}),", which has an English title, Album name, and Artist name."]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{ \n "track": "Price",\n "artists": ["ATLUS Sound Team"],\n "album": "PERSONA5 ORIGINAL SOUNDTRACK"\n}\n'})}),(0,r.jsxs)(n.p,{children:["However, the ",(0,r.jsx)(n.a,{href:"https://beta.musicbrainz.org/search?query=isrc%3AJPK651601515&type=recording&limit=25&method=advanced",children:"Recording found by Musicbrainz"})," has multiple Releases (albums). The first suggested release is a ",(0,r.jsx)(n.em,{children:"related"})," Album with similar name (",(0,r.jsx)(n.code,{children:"Persona 25th Anniversary Deluxe Vinyl Box Set"}),") but it's not the same as your album. Additionally, the artists for this album are in Japanese, rather than English."]}),(0,r.jsxs)(n.p,{children:["You have used ",(0,r.jsx)(n.code,{children:"releaseStatusPriority"})," to prioritize ",(0,r.jsx)(n.code,{children:"psuedo-release"})," which does work but there are still multiple releases chosen, and some are non-English (",(0,r.jsx)(n.code,{children:"\u300E\u30DA\u30EB\u30BD\u30CA5\u300F\u30AA\u30EA\u30B8\u30CA\u30EB\u30FB\u30B5\u30A6\u30F3\u30C9\u30C8\u30E9\u30C3\u30AF"}),")."]}),(0,r.jsxs)(n.p,{children:["Using ",(0,r.jsx)(n.code,{children:'"albumWeight": 0.4'})," and ",(0,r.jsx)(n.code,{children:'"artistWeight": 0.3'})," ensures that the releases that have an album name, and artist names, more similar to your original scrobble are ranked higher. Resulting in ",(0,r.jsxs)(n.a,{href:"https://beta.musicbrainz.org/release/82de33b1-1cd6-4236-b116-561d0ecc8acf",children:["the correct Release (",(0,r.jsx)(n.code,{children:"PERSONA5 ORIGINAL SOUNDTRACK"}),")"]})," being chosen as the final match."]})]}),"\n",(0,r.jsxs)(n.p,{children:["Add one or more of these weight properties to your ",(0,r.jsx)(n.a,{href:"/configuration/transforms#configuring-stages",children:"Stage Configuration"})," to enable Field Scoring:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'// ...\n"defaults": {\n // ...\n // enables album text similiarity scoring\n "albumWeight": 0.33,\n // enables title text similiarity scoring\n "titleWeight": 0.33,\n // enables artist text similiarity scoring\n "artistWeight": 0.33\n }\n'})}),"\n",(0,r.jsxs)(n.p,{children:["While not enforced, it's a good idea to keep these weights under ",(0,r.jsx)(n.code,{children:"1"}),". And if using more than one weight, they should be add up to ",(0,r.jsx)(n.code,{children:"1"}),"."]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"How Scoring is Combined"}),(0,r.jsxs)(n.p,{children:["The score similarity from ",(0,r.jsx)(n.code,{children:"albumWeight"})," will be added to the score accumulated by ",(0,r.jsx)(n.a,{href:"#sorting-releases",children:"Release Attributes"})," to affect the final ranking of each Release for a Match/Recording."]}),(0,r.jsx)(n.p,{children:"In the final sorting of Recordings, title score + artist score is combined with the top ranked release album score. The top ranked score is then used to choose the matched Recording."})]}),"\n",(0,r.jsx)(n.h2,{id:"best-practices",children:"Best Practices"}),"\n",(0,r.jsx)(n.h3,{id:"caching",children:"Caching"}),"\n",(0,r.jsxs)(n.p,{children:["You ",(0,r.jsx)(n.strong,{children:"should"})," setup ",(0,r.jsx)(n.a,{href:"/configuration/transforms#caching",children:"metadata caching"})," to reduce API calls, improve transform performance, and reduce memory usage when using this stage."]}),"\n",(0,r.jsx)(n.h3,{id:"sensible-default",children:"Sensible Default"}),"\n",(0,r.jsxs)(n.p,{children:["Generally, the Musicbrainz Stage can be used without any of the ",(0,r.jsx)(n.a,{href:"#configuration",children:"optional configuration"})," and you should still see good results from matches. The top ",(0,r.jsx)(n.a,{href:"#score",children:"scored"})," match is, anecdotally, good enough for correcting and filling in surface-level play data like Title and Artist names."]}),"\n",(0,r.jsx)(n.p,{children:"For a more opinionated match that will mirror what you would expect from data from large music services:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'{\n // use official release over anything else\n "releaseStatusPriority": ["official"],\n // prefer album, then single, then ep\n "releaseGroupPrimaryTypePriority": ["album", "single", "ep"],\n // prefer worldwide release\n "releaseCountryPriority": ["XW"]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Consider adding ",(0,r.jsxs)(n.a,{href:"#search-methods",children:["Artist Extraction (",(0,r.jsx)(n.code,{children:"artist"}),")"]})," in ",(0,r.jsx)(n.strong,{children:"Native Mode"})," if any of your Sources do not support multiple artists or your music collection is not tagged well."]}),"\n",(0,r.jsx)(i,{children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",children:'{\n // use official release over anything else\n "releaseStatusPriority": ["official"],\n // prefer album, then single, then ep\n "releaseGroupPrimaryTypePriority": ["album", "single", "ep"],\n // prefer worldwide release\n "releaseCountryPriority": ["XW"],\n // search using extracted artist as last search attempt\n "searchOrder": ["isrc", "basic", "artist"],\n "searchArtistMethod": "native"\n}\n'})})}),"\n",(0,r.jsx)(n.h3,{id:"search-considerations",children:"Search Considerations"}),"\n",(0,r.jsxs)(n.p,{children:["How complete, and accurate, your scrobble data is from a certain Source should dictate what ",(0,r.jsx)(n.a,{href:"#search-methods",children:"Search Methods"})," you should use for matching."]}),"\n",(0,r.jsxs)(n.p,{children:["Musicbrainz agressively normalizes ",(0,r.jsx)(n.strong,{children:"artist, album, and title"})," fields in its database which means that if your Scrobble data contains major innaccuracies or phrases fields in a non-normal way, it's possible Musicbrainz won't find any matches. This is a more common occurrence when trying to match Scrobble data from Sources that don't support multiple artist data, like ",(0,r.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," and ",(0,r.jsx)(n.a,{href:"/configuration/sources/lastfm-source",children:"Last.fm"}),"."]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Examples of Bad Data"}),(0,r.jsx)(n.p,{children:"Last.fm Scrobble returns an Artist in the title and combines two artists into one string:"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility (feat. Wheatus)",\n "artists": ["Bowling For Soup & Punk Rock Factory"]\n}\n'})}),(0,r.jsxs)(n.p,{children:["The corresponding ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/release/85bf284b-8b39-414e-8ad9-61f593072588",children:"Musicbrainz Recording"})," has all artists separated and no artist in the title:"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "title": "Endless Possibility",\n "artists": ["Bowling For Soup", "Punk Rock Factory", "Wheatus"]\n}\n'})})]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"In general, you should prioritize methods that search by unique identifiers"})," (",(0,r.jsx)(n.code,{children:"isrc"})," ",(0,r.jsx)(n.code,{children:"mbidRecording"})," ",(0,r.jsx)(n.code,{children:"basicOrIds"}),") if you know your Sources may contain ",(0,r.jsx)(n.em,{children:"accurate"})," data for these. Such as: Spotify, Jellyfin, Plex, and Listenbrainz (Source)."]}),"\n",(0,r.jsx)(n.p,{children:"Use field-based methods if your Sources do not return IDs consistently (or at all). Add more field-based methods the more inaccurate your scrobble data may be, but be aware that more aggressive/broad searches may return less accurate results."}),"\n",(0,r.jsxs)(s,{type:"tip",summary:"Matching Scrobbles from Last.fm",children:[(0,r.jsx)(n.p,{children:"Last.fm is inconsistent when it comes to matching your scrobbles. It will accept (almost) any fields for track/title/album and aggressively attempt to match/correct these, regardless of your intentions."}),(0,r.jsxs)(n.p,{children:["Unless the client scrobbling to Last.fm is also sending an ",(0,r.jsx)(n.code,{children:"mbid"}),", it's likely your scrobble will not be ",(0,r.jsx)(n.strong,{children:"consistently"})," matched correctly and so the ",(0,r.jsx)(n.code,{children:"mbid"})," supplied by last.fm in its scrobble data will be wrong."]}),(0,r.jsxs)(n.p,{children:["Therefore, you should ",(0,r.jsx)(n.em,{children:"avoid"})," using ",(0,r.jsx)(n.code,{children:"mbidRecording"})," and ",(0,r.jsx)(n.code,{children:"basicOrIds"})," as the primary search methods (or understand the risks of using them). Instead, prefer to use field-based methods ",(0,r.jsx)(n.em,{children:"before"})," falling back to id-based methods."]})]}),"\n",(0,r.jsx)(n.h3,{id:"filter-considerations",children:"Filter Considerations"}),"\n",(0,r.jsx)(n.p,{children:"When using your own filters consider:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Prefer ",(0,r.jsx)(n.code,{children:"deny"})," over ",(0,r.jsx)(n.code,{children:"allow"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Releases come in all kinds of formats. Since ",(0,r.jsx)(n.code,{children:"allow"})," is explicit you may filter out your desired match without realizing it (correct data except for release type). Or the Musicbrainz data for a higher scored match may be appropriate but you did not include it, exhaustively."]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["Prefer ",(0,r.jsx)(n.a,{href:"#sorting",children:"Sorting"})," over ",(0,r.jsx)(n.a,{href:"#filtering",children:"Filtering"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Sorting does not eliminate any matches. It is, generally, better to get ",(0,r.jsx)(n.strong,{children:"some"})," match than it is to have your Play data completely uncorrected because filtering eliminated all matches"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"using-partial-match",children:"Using Partial Match"}),"\n",(0,r.jsxs)(n.p,{children:["Use ",(0,r.jsx)(n.a,{href:"#rules-and-hooks",children:"Rules"})," to apply MusicBrainz match data selectively."]}),"\n",(0,r.jsxs)(n.p,{children:["If you know that your music collection is well organized and you do not want to change the artists/title/album etc... sent to a ",(0,r.jsx)(n.a,{href:"/configuration/clients",children:"Client"}),", you can still benefit from matches by only applying MBIDs using ",(0,r.jsx)(n.code,{children:"meta"})," so that any Client that supports Musicbrainz data (",(0,r.jsx)(n.a,{href:"/configuration/clients/koito",children:"Koito"}),", ",(0,r.jsx)(n.a,{href:"/configuration/clients/tealfm",children:"Tealfm"}),", ",(0,r.jsx)(n.a,{href:"/configuration/clients/listenbrainz",children:"Listenbrainz"}),", ",(0,r.jsx)(n.a,{href:"/configuration/clients/rocksky",children:"Rocksky"}),") can still get that data."]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "title": false,\n "artists": false,\n "album": false,\n "albumArtists": false,\n "meta": true\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,r.jsx)(n.h2,{id:"logging",children:"Logging"}),"\n",(0,r.jsx)(n.p,{children:"If"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Musicbrainz is not returning matches"}),"\n",(0,r.jsx)(n.li,{children:"Multi-scrobbler is using the wrong match"}),"\n",(0,r.jsx)(n.li,{children:"or the resulting enhanced Scrobble is not what you expected"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Enable logging by turning on ",(0,r.jsx)(n.a,{href:"/configuration#debug-mode",children:(0,r.jsx)(n.strong,{children:"Debug Mode"})})," to help diagnose any issues with the Musicbrainz API and Scrobble enhancement. ",(0,r.jsx)(n.strong,{children:"Before creating an issue"})," please enable logging and include any logs with your issue as this is needed to debug."]}),"\n",(0,r.jsxs)(n.p,{children:["If you have multiple Modification Stages and need to see the diff for your Play between each Stage, enable ",(0,r.jsx)(n.code,{children:'"log": "all"'})," in the individual ",(0,r.jsx)(n.a,{href:"#rules-and-hooks",children:"Modification Stage"}),"."]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(n.p,{children:["In a ",(0,r.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," ",(0,r.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n // highlight-start\n "log": "all",\n // highlight-end\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,r.jsx)(n.h2,{id:"examples",children:"Examples"}),"\n",(0,r.jsx)(n.h3,{id:"minimal",children:"Minimal"}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(n.p,{children:["Your ",(0,r.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ]\n },\n }\n ]\n}\n'})}),(0,r.jsxs)(n.p,{children:["In a ",(0,r.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," ",(0,r.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB"\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,r.jsx)(n.h3,{id:"sensible-default-1",children:"Sensible Default"}),"\n",(0,r.jsxs)(n.p,{children:["Using the config shown in ",(0,r.jsx)(n.a,{href:"#sensible-default",children:"Sensible Default"})," with File/AIO config, instead of ",(0,r.jsx)(n.a,{href:"#env-configuration",children:"ENV Config"}),"."]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(n.p,{children:["Your ",(0,r.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ]\n },\n "defaults": {\n "releaseStatusPriority": ["official"],\n "releaseGroupPrimaryTypePriority": ["album", "single", "ep"],\n "releaseCountryPriority": ["XW"],\n "searchArtistMethod": "native"\n "searchOrder": ["isrc", "basic", "artist"]\n }\n }\n ]\n}\n'})}),(0,r.jsxs)(n.p,{children:["In a ",(0,r.jsx)(n.a,{href:"/configuration/sources/subsonic",children:"Subsonic"})," ",(0,r.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="subsonic.json"',children:'[\n { \n "name": "MySubsonic",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB"\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,r.jsx)(n.h3,{id:"add-metadata-only-no-scrobble-modification",children:"Add Metadata Only (No Scrobble Modification)"}),"\n",(0,r.jsxs)(n.p,{children:["If you do not want your scrobble data to be modified (artists/album/title) but still want to benefit from associating the scrobble with a Musicbrainz match (for Scrobble ",(0,r.jsx)(n.a,{href:"/configuration/clients",children:"Clients"})," like ",(0,r.jsx)(n.a,{href:"/configuration/clients/tealfm",children:"Tealfm"}),", ",(0,r.jsx)(n.a,{href:"/configuration/clients/rocksky",children:"Rocksky"}),", and ",(0,r.jsx)(n.a,{href:"/configuration/clients/koito",children:"Koito"}),"), then use ",(0,r.jsx)(n.a,{href:"#using-partial-match",children:"Partial Match"})," rules to specify only ",(0,r.jsx)(n.code,{children:"meta"})," to be updated."]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(n.p,{children:["Your ",(0,r.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ]\n },\n "defaults": {\n // maybe something like Sensible Default?\n }\n }\n ]\n}\n'})}),(0,r.jsxs)(n.p,{children:["In a ",(0,r.jsx)(n.a,{href:"/configuration/sources/jellyfin",children:"Jellyfin"})," ",(0,r.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="jellyfin.json"',children:'[\n { \n "name": "MyJellyfin",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "title": false,\n "artists": false,\n "album": false,\n "albumArtists": false,\n "meta": true // only update scrobble metadata with MBIDs\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,r.jsx)(n.h3,{id:"use-native-stage-if-no-musicbrainz-matches",children:"Use Native Stage if no Musicbrainz Matches"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Musicbrainz Stage configured with","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"#sensible-default",children:"Sensible Defaults"})}),"\n",(0,r.jsxs)(n.li,{children:["Fallback search with ",(0,r.jsx)(n.a,{href:"#search-methods",children:"Artist Extraction"})]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"/configuration/transforms/native/#default-stage",children:"Default Native Stage"})}),"\n",(0,r.jsxs)(n.li,{children:["Uses ",(0,r.jsx)(n.a,{href:"/configuration/transforms#flow-control",children:"Flow Control"})," to run Native only if there are no Musicbrainz matches"]}),"\n"]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(n.p,{children:["Your ",(0,r.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ]\n },\n "defaults": {\n "releaseStatusPriority": ["official"],\n "releaseGroupPrimaryTypePriority": ["album", "single", "ep"],\n "releaseCountryPriority": ["XW"],\n "searchArtistMethod": "native",\n "searchOrder": ["isrc","basic", "artist"]\n }\n }\n ]\n}\n'})}),(0,r.jsxs)(n.p,{children:["In a ",(0,r.jsx)(n.a,{href:"/configuration/sources/jellyfin",children:"Jellyfin"})," ",(0,r.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="jellyfin.json"',children:'[\n { \n "name": "MyJellyfin",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n // if musicbrainz is successful then do NOT run native,\n // only run native if musicbrainz fails to find a match (onFailure)\n "type": "musicbrainz",\n "name": "MyMB"\n "onSuccess": "stop",\n "onFailure": "continue"\n },\n {\n "type": "native"\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,r.jsx)(n.h3,{id:"override-stage-configuration-example",children:"Different Search Methods Based on Source (Last.fm and Spotify)"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.a,{href:"#search-considerations",children:"Last.fm has unreliable MBIDs"})," and Spotify provides an ",(0,r.jsx)(n.a,{href:"https://musicbrainz.org/doc/ISRC",children:"ISRC"})," and consistent formatting."]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["For Spotify, we want to rely on ID ",(0,r.jsx)(n.a,{href:"#search-methods",children:"searches"})," and matches that are ",(0,r.jsx)(n.a,{href:"#field-scoring",children:"close to original scrobble text"})]}),"\n",(0,r.jsx)(n.li,{children:"For Last.fm, we want to use only text searches and take whatever correction is given to us"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Use ",(0,r.jsx)(n.a,{href:"#rules-and-hooks",children:(0,r.jsx)(n.strong,{children:"Per Component Overrides"})})," with the default (or ",(0,r.jsx)(n.a,{href:"#sensible-default",children:"sensible"}),") ",(0,r.jsx)(n.a,{href:"#stage-configuration",children:"Stage Configuration"})," to specify different search behavior for each Source."]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsx)(n.p,{children:"Stage Configuration"}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ],\n "defaults": {} // maybe use sensible?\n },\n }\n ]\n}\n'})}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="lastfm.json"',children:'[\n { \n "name": "MyLFM",\n "configureAs": "source"\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "searchArtistMethod": "native"\n // does not use any ID searches\n // fallback to more aggressive/broad searches\n "searchOrder": ["basic", "artist", "freetext"]\n }\n ]\n }\n }\n }\n]\n'})}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="spotify.json"',children:'[\n { \n "name": "MySpotify",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n // use ISRC as first search\n "searchOrder": ["isrc", "basic"]\n // bias matches towards similarity to original data\n "albumWeight": 0.33,\n "titleWeight": 0.33,\n "artistWeight": 0.33\n }\n ]\n }\n }\n }\n]\n'})})]}),"\n",(0,r.jsx)(n.h3,{id:"use-metadata-for-discord-album-art",children:"Use Metadata for Discord Album Art"}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.a,{href:"/configuration/clients/discord",children:"Discord"})," Client can fetch album art images from ",(0,r.jsx)(n.a,{href:"http://coverartarchive.org/",children:"Cover Art Archive"})," if the Scrobble data contains a Release MBID. Use the Musicbrainz Stage on the ",(0,r.jsx)(n.code,{children:"preCompare"})," hook for your Discord client so that missing Musicbrainz IDs are populated for this functionality."]}),"\n",(0,r.jsxs)(i,{children:[(0,r.jsx)("summary",{children:"Example"}),(0,r.jsxs)(t.A,{groupId:"configType",queryString:!0,children:[(0,r.jsxs)(o.A,{value:"env",label:"ENV",children:[(0,r.jsxs)(n.p,{children:["Using the ",(0,r.jsx)(n.a,{href:"#env-configuration",children:"ENV Configuration"})," from above, add these fields to your docker compose ",(0,r.jsx)(n.code,{children:"environment:"})]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"- MB_CONTACT=contact@mydomain.com\n- MB_PRESETS=id,native,sensible\n- DISCORD_TRANSFORMS=musicbrainz\n"})})]}),(0,r.jsxs)(o.A,{value:"file",label:"File",children:[(0,r.jsxs)(n.p,{children:["Your ",(0,r.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ]\n },\n "defaults": {\n // sensible defaults\n "releaseStatusPriority": ["official"],\n "releaseGroupPrimaryTypePriority": ["album", "single", "ep"],\n "releaseCountryPriority": ["XW"],\n "searchArtistMethod": "native",\n "searchOrder": ["isrc","basic", "artist"]\n }\n }\n ]\n}\n'})}),(0,r.jsxs)(n.p,{children:["In your ",(0,r.jsx)(n.a,{href:"/configuration/clients/discord",children:"Discord"})," ",(0,r.jsx)(n.a,{href:"/configuration?configType=file#configuration-types",children:"File Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="discord.json"',children:'[\n { \n "name": "MS",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n }\n ]\n }\n }\n }\n]\n'})})]}),(0,r.jsxs)(o.A,{value:"aio",label:"AIO",children:[(0,r.jsxs)(n.p,{children:["Your ",(0,r.jsx)(n.a,{href:"/configuration?configType=aio#configuration-types",children:"AIO Config"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json5",metastring:'title="config.json"',children:'{\n // ...\n "transformers": [\n {\n "type": "musicbrainz",\n "name": "MyMB",\n "data": {\n "apis": [\n {\n "contact": "contact@mydomain.com"\n }\n ]\n },\n "defaults": {\n // sensible defaults\n "releaseStatusPriority": ["official"],\n "releaseGroupPrimaryTypePriority": ["album", "single", "ep"],\n "releaseCountryPriority": ["XW"],\n "searchArtistMethod": "native",\n "searchOrder": ["isrc","basic", "artist"]\n }\n }\n ],\n "sources": [\n { \n "name": "MS",\n "type": "discord",\n "data": { /* ... */},\n "options": {\n "playTransform": {\n "preCompare": [\n {\n "type": "musicbrainz",\n }\n ]\n }\n }\n }\n ]\n}\n'})})]})]})]})]})}function m(e={}){let{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(u,{...e})}):u(e)}function f(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}}}]); \ No newline at end of file diff --git a/assets/js/e7a53bbd.0f0a8063.js b/assets/js/e7a53bbd.0f0a8063.js new file mode 100644 index 00000000..ead7494b --- /dev/null +++ b/assets/js/e7a53bbd.0f0a8063.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["1353"],{1317(n,e,a){a.r(e),a.d(e,{metadata:()=>t,default:()=>p,frontMatter:()=>i,contentTitle:()=>s,toc:()=>c,assets:()=>l});var t=JSON.parse('{"id":"configuration/kitchensink","title":"Kitchen Sink","description":"Kitchen Sink Example","source":"@site/docs/configuration/kitchensink.mdx","sourceDirName":"configuration","slug":"/configuration/kitchensink","permalink":"/multi-scrobbler/configuration/kitchensink","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/kitchensink.mdx","tags":[],"version":"current","sidebarPosition":3,"frontMatter":{"sidebar_position":3,"title":"Kitchen Sink","description":"Kitchen Sink Example"},"sidebar":"tutorialSidebar","previous":{"title":"Youtube Music","permalink":"/multi-scrobbler/configuration/sources/youtube-music"},"next":{"title":"Enhancing Scrobbles","permalink":"/multi-scrobbler/configuration/transforms/"}}'),r=a(4848),o=a(8453);let i={sidebar_position:3,title:"Kitchen Sink",description:"Kitchen Sink Example"},s="Example Complex Scenario",l={},c=[{value:"All-in-one Config",id:"all-in-one-config",level:3},{value:"Separate JSON files",id:"separate-json-files",level:3}];function d(n){let e={a:"a",code:"code",h1:"h1",h3:"h3",header:"header",li:"li",p:"p",pre:"pre",ul:"ul",...(0,o.R)(),...n.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(e.header,{children:(0,r.jsx)(e.h1,{id:"example-complex-scenario",children:"Example Complex Scenario"})}),"\n",(0,r.jsx)(e.p,{children:"Scenario:"}),"\n",(0,r.jsxs)(e.ul,{children:["\n",(0,r.jsx)(e.li,{children:"You want to scrobble plays for yourself (Foxx), Fred, and Mary"}),"\n",(0,r.jsxs)(e.li,{children:["Each person has their own ",(0,r.jsx)(e.a,{href:"/configuration/clients/maloja",children:"Maloja"})," server"]}),"\n",(0,r.jsx)(e.li,{children:"Each person has their own Spotify account"}),"\n",(0,r.jsxs)(e.li,{children:["You have your own Airsonic (",(0,r.jsx)(e.a,{href:"/configuration/sources/subsonic",children:"subsonic"}),") server you to scrobble from"]}),"\n",(0,r.jsxs)(e.li,{children:["You have your own ",(0,r.jsx)(e.a,{href:"/configuration/sources/youtube-music",children:"Youtube Music"})," account you want to scrobble from"]}),"\n",(0,r.jsxs)(e.li,{children:["Mary has her own ",(0,r.jsx)(e.a,{href:"/configuration/clients/lastfm",children:"Last.fm"})," account she also wants to scrobble to"]}),"\n",(0,r.jsxs)(e.li,{children:["Fred has his own ",(0,r.jsx)(e.a,{href:"/configuration/sources/spotify",children:"Spotify"})," application and provides you with just his access and refresh token because he doesn't trust you (wtf Fred)"]}),"\n",(0,r.jsx)(e.li,{children:"Fred has a Plex server and wants to scrobble everything he plays"}),"\n",(0,r.jsxs)(e.li,{children:["Mary uses Fred's Plex server but only wants to scrobble her plays from the ",(0,r.jsx)(e.code,{children:"podcast"})," library"]}),"\n",(0,r.jsxs)(e.li,{children:["The three of you have a shared library on ",(0,r.jsx)(e.a,{href:"/configuration/sources/plex",children:"Plex"})," called ",(0,r.jsx)(e.code,{children:"party"})," that you only play when you are hanging out. You want plays from that library to be scrobbled to everyone's servers."]}),"\n",(0,r.jsxs)(e.li,{children:["Fred also has his own ",(0,r.jsx)(e.a,{href:"/configuration/sources/jellyfin",children:"Jellyfin server"})," and wants to scrobble everything he plays"]}),"\n",(0,r.jsxs)(e.li,{children:["You have an android music app that can scrobble to a ",(0,r.jsx)(e.a,{href:"/configuration/sources/listenbrainz-endpoint",children:"custom listenbrainz server"})]}),"\n"]}),"\n",(0,r.jsx)(e.h3,{id:"all-in-one-config",children:"All-in-one Config"}),"\n",(0,r.jsx)(e.pre,{children:(0,r.jsx)(e.code,{className:"language-json5",metastring:'title="CONFIG_DIR/config.json"',children:'{\n "sourceDefaults": {\n "maxPollRetries": 0, // optional, default # of automatic polling restarts on error. can be overridden by property in individual config\n "maxRequestRetries": 1, // optional, default # of http request retries a source can make before error is thrown. can be overridden by property in individual config\n "retryMultiplier": 1.5, // optional, default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying). can be overridden by property in individual config\n },\n "clientDefaults": {\n "maxRequestRetries": 1, // optional, default # of http request retries a client can make before error is thrown. can be overridden by property in individual config\n "retryMultiplier": 1.5, // optional, default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying). can be overridden by property in individual config\n },\n "sources": [\n {\n "type": "spotify",\n "name": "foxxSpot",\n "clients": ["foxxMaloja"],\n "data": {\n "clientId": "foxxSpotifyAppId", \n "clientSecret": "foxxSpotifyAppSecret",\n },\n "options": {\n "maxRequestRetries": 2, // override default max retries because spotify can...spotty\n }\n },\n {\n "type": "spotify",\n "name": "marySpot",\n "clients": ["maryMaloja"],\n "data": {\n "clientId": "foxxSpotifyAppId", // only need one application, it can be used by all users of this multi-scrobbler instance \n "clientSecret": "foxxSpotifyAppSecret",\n }\n },\n {\n "type": "spotify",\n "name": "fredSpot",\n "clients": ["fredMaloja"],\n "data": {\n "accessToken": "fredsToken",\n "refreshToken": "fredsRefreshToken",\n "interval": 120, // he also wants a slower check interval because his application already has heavy api usage\n }\n },\n {\n "type": "plex",\n "name": "fredPlex",\n "clients": ["fredMaloja"],\n "data": {\n "token": "JtRnwQWD__XMJF8sT3jc",\n "url": "192.168.0.233:32400",\n "usersallow": ["fredUser"]\n }\n },\n {\n "type": "plex",\n "name": "maryPlex",\n "clients": ["maryMaloja"],\n "data": {\n "token": "JtRnwQWD__XMJF8sT3jc",\n "url": "192.168.0.233:32400",\n "usersallow": ["maryUser"], // still need to specify mary as user so not all users who play from \'podcasts\' get scrobbled\n "librariesAllow": ["podcasts"]\n }\n },\n {\n "type": "plex",\n "name": "partyPlex",\n // omitting clients (or making it empty) will make this Source scrobble to all Clients\n "data": {\n "token": "JtRnwQWD__XMJF8sT3jc",\n "url": "192.168.0.233:32400",\n "librariesAllow": ["party"]\n }\n },\n {\n "type": "jellyfin",\n "name": "FredJelly",\n // omitting clients (or making it empty) will make this Source scrobble to all Clients\n "data": {\n "url": "http://localhost:8096",\n "user": "FoxxMD",\n "apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c",\n "usersAllow": ["fredUser"]\n }\n },\n {\n "type": "subsonic",\n "name": "foxxAirsonic",\n "clients": ["foxxMaloja"],\n "data": {\n "user": "foxx",\n "password": "foxxPassword",\n "url": "https://airsonic.foxx.example"\n }\n },\n {\n "type": "ytmusic",\n "name": "foxxYoutube",\n "clients": ["foxxMaloja"],\n "data": {\n "cookie": "__Secure-3PAPISID=3AxsXpy0MKGu75Qb/AkISXGqOnSDn1jEKn; DEVICE_INFO=ChxOekU0Tmpjek5EWTBPRGd3TlRBMk16QXpNdz09EJbS8Z0GGJbS8Z0G; ...",\n }\n },\n {\n "type": "endpointlz",\n "name": "listenbrainzfoxx",\n "clients": ["foxxMaloja"],\n "data": {\n "token": "myToken"\n }\n }\n ],\n "clients": [\n {\n "type": "maloja",\n "name": "foxxMaloja",\n "data": {\n "url": "https://maloja.foxx.example",\n "apiKey": "foxxApiKey"\n }\n },\n {\n "type": "maloja",\n "name": "fredMaloja",\n "data": {\n "url": "https://maloja.fred.example",\n "apiKey": "fredApiKey"\n }\n },\n {\n "type": "maloja",\n "name": "maryMaloja",\n "data": {\n "url": "https://maloja.mary.example",\n "apiKey": "maryApiKey"\n }\n },\n {\n "type": "lastfm",\n "name": "maryLFM",\n "data": {\n "apiKey": "maryApiKey",\n "secret": "marySecret",\n "redirectUri": "http://localhost:9078/lastfm/callback"\n }\n }\n ]\n}\n\n'})}),"\n",(0,r.jsx)(e.h3,{id:"separate-json-files",children:"Separate JSON files"}),"\n",(0,r.jsx)(e.pre,{children:(0,r.jsx)(e.code,{className:"language-json5",metastring:'title="CONFIG_DIR/spotify.json"',children:'[\n {\n // may omit \'type\' property since app knows this is file is for spotify configs\n "name": "foxxSpot",\n "clients": ["foxxMaloja"],\n "data": {\n "clientId": "foxxSpotifyAppId",\n "clientSecret": "foxxSpotifyAppSecret"\n }\n },\n {\n "name": "marySpot",\n "clients": ["maryMaloja"],\n "data": {\n "clientId": "foxxSpotifyAppId",\n "clientSecret": "foxxSpotifyAppSecret"\n }\n },\n {\n "name": "fredSpot",\n "clients": ["fredMaloja"],\n "data": {\n "accessToken": "fredsToken",\n "refreshToken": "fredsRefreshToken",\n "interval": 120\n }\n },\n]\n'})}),"\n",(0,r.jsx)(e.pre,{children:(0,r.jsx)(e.code,{className:"language-json5",metastring:'title="CONFIG_DIR/plex.json"',children:'[\n {\n "name": "fredPlex",\n "clients": ["fredMaloja"],\n "data": {\n "token": "JtRnwQWD__XMJF8sT3jc",\n "url": "192.168.0.233:32400",\n "usersallow": ["fredUser"]\n }\n },\n {\n "name": "maryPlex",\n "clients": ["maryMaloja"],\n "data": {\n "token": "JtRnwQWD__XMJF8sT3jc",\n "url": "192.168.0.233:32400",\n "usersallow": ["maryUser"],\n "librariesAllow": ["podcasts"]\n }\n },\n {\n "name": "partyPlex",\n "data": {\n "token": "JtRnwQWD__XMJF8sT3jc",\n "url": "192.168.0.233:32400",\n "librariesAllow": ["party"]\n }\n }\n]\n'})}),"\n",(0,r.jsx)(e.pre,{children:(0,r.jsx)(e.code,{className:"language-json5",metastring:'title="CONFIG_DIR/jellyfin.json"',children:'[\n {\n "name": "FredJelly",\n "data": {\n "url": "http://localhost:8096",\n "user": "FoxxMD",\n "apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c",\n "usersAllow": ["fredUser"]\n }\n }\n]\n'})}),"\n",(0,r.jsx)(e.pre,{children:(0,r.jsx)(e.code,{className:"language-json5",metastring:'title="CONFIG_DIR/ytmusic.json"',children:'[\n {\n "name": "foxxYoutube",\n "clients": ["foxxMaloja"],\n "data": {\n "cookie": "__Secure-3PAPISID=3AxsXpy0MKGu75Qb/AkISXGqOnSDn1jEKn; DEVICE_INFO=ChxOekU0Tmpjek5EWTBPRGd3TlRBMk16QXpNdz09EJbS8Z0GGJbS8Z0G; ...",\n }\n }\n]\n'})}),"\n",(0,r.jsx)(e.pre,{children:(0,r.jsx)(e.code,{className:"language-json5",metastring:'title="CONFIG_DIR/endpointlz.json"',children:'[\n {\n "name": "listenbrainzfoxx",\n "clients": ["foxxMaloja"],\n "data": {\n "token": "myToken",\n }\n }\n]\n'})}),"\n",(0,r.jsx)(e.pre,{children:(0,r.jsx)(e.code,{className:"language-json5",metastring:'title="CONFIG_DIR/maloja.json"',children:'[\n {\n "name": "foxxMaloja",\n "data": {\n "url": "https://maloja.foxx.example",\n "apiKey": "foxxApiKey"\n }\n },\n {\n "name": "fredMaloja",\n "data": {\n "url": "https://maloja.fred.example",\n "apiKey": "fredApiKey"\n }\n },\n {\n "name": "maryMaloja",\n "data": {\n "url": "https://maloja.mary.example",\n "apiKey": "maryApiKey"\n }\n }\n]\n'})}),"\n",(0,r.jsx)(e.pre,{children:(0,r.jsx)(e.code,{className:"language-json5",metastring:'title="CONFIG_DIR/lastfm.json"',children:'[\n {\n "name": "maryLFM",\n "data": {\n "apiKey": "maryApiKey",\n "secret": "marySecret",\n "redirectUri": "http://localhost:9078/lastfm/callback"\n }\n }\n]\n'})})]})}function p(n={}){let{wrapper:e}={...(0,o.R)(),...n.components};return e?(0,r.jsx)(e,{...n,children:(0,r.jsx)(d,{...n})}):d(n)}}}]); \ No newline at end of file diff --git a/assets/js/e7a53bbd.391da7e0.js b/assets/js/e7a53bbd.391da7e0.js deleted file mode 100644 index 5fb202e4..00000000 --- a/assets/js/e7a53bbd.391da7e0.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["1353"],{1317(n,e,a){a.r(e),a.d(e,{metadata:()=>r,default:()=>p,frontMatter:()=>i,contentTitle:()=>s,toc:()=>c,assets:()=>l});var r=JSON.parse('{"id":"configuration/kitchensink","title":"Kitchen Sink","description":"Scenario:","source":"@site/docs/configuration/kitchensink.mdx","sourceDirName":"configuration","slug":"/configuration/kitchensink","permalink":"/multi-scrobbler/configuration/kitchensink","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/configuration/kitchensink.mdx","tags":[],"version":"current","sidebarPosition":3,"frontMatter":{"sidebar_position":3,"title":"Kitchen Sink"},"sidebar":"tutorialSidebar","previous":{"title":"Youtube Music","permalink":"/multi-scrobbler/configuration/sources/youtube-music"},"next":{"title":"Enhancing Scrobbles","permalink":"/multi-scrobbler/configuration/transforms/"}}'),t=a(4848),o=a(8453);let i={sidebar_position:3,title:"Kitchen Sink"},s="Example Complex Scenario",l={},c=[{value:"All-in-one Config",id:"all-in-one-config",level:3},{value:"Separate JSON files",id:"separate-json-files",level:3}];function d(n){let e={a:"a",code:"code",h1:"h1",h3:"h3",header:"header",li:"li",p:"p",pre:"pre",ul:"ul",...(0,o.R)(),...n.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(e.header,{children:(0,t.jsx)(e.h1,{id:"example-complex-scenario",children:"Example Complex Scenario"})}),"\n",(0,t.jsx)(e.p,{children:"Scenario:"}),"\n",(0,t.jsxs)(e.ul,{children:["\n",(0,t.jsx)(e.li,{children:"You want to scrobble plays for yourself (Foxx), Fred, and Mary"}),"\n",(0,t.jsxs)(e.li,{children:["Each person has their own ",(0,t.jsx)(e.a,{href:"/configuration/clients/maloja",children:"Maloja"})," server"]}),"\n",(0,t.jsx)(e.li,{children:"Each person has their own Spotify account"}),"\n",(0,t.jsxs)(e.li,{children:["You have your own Airsonic (",(0,t.jsx)(e.a,{href:"/configuration/sources/subsonic",children:"subsonic"}),") server you to scrobble from"]}),"\n",(0,t.jsxs)(e.li,{children:["You have your own ",(0,t.jsx)(e.a,{href:"/configuration/sources/youtube-music",children:"Youtube Music"})," account you want to scrobble from"]}),"\n",(0,t.jsxs)(e.li,{children:["Mary has her own ",(0,t.jsx)(e.a,{href:"/configuration/clients/lastfm",children:"Last.fm"})," account she also wants to scrobble to"]}),"\n",(0,t.jsxs)(e.li,{children:["Fred has his own ",(0,t.jsx)(e.a,{href:"/configuration/sources/spotify",children:"Spotify"})," application and provides you with just his access and refresh token because he doesn't trust you (wtf Fred)"]}),"\n",(0,t.jsx)(e.li,{children:"Fred has a Plex server and wants to scrobble everything he plays"}),"\n",(0,t.jsxs)(e.li,{children:["Mary uses Fred's Plex server but only wants to scrobble her plays from the ",(0,t.jsx)(e.code,{children:"podcast"})," library"]}),"\n",(0,t.jsxs)(e.li,{children:["The three of you have a shared library on ",(0,t.jsx)(e.a,{href:"/configuration/sources/plex",children:"Plex"})," called ",(0,t.jsx)(e.code,{children:"party"})," that you only play when you are hanging out. You want plays from that library to be scrobbled to everyone's servers."]}),"\n",(0,t.jsxs)(e.li,{children:["Fred also has his own ",(0,t.jsx)(e.a,{href:"/configuration/sources/jellyfin",children:"Jellyfin server"})," and wants to scrobble everything he plays"]}),"\n",(0,t.jsxs)(e.li,{children:["You have an android music app that can scrobble to a ",(0,t.jsx)(e.a,{href:"/configuration/sources/listenbrainz-endpoint",children:"custom listenbrainz server"})]}),"\n"]}),"\n",(0,t.jsx)(e.h3,{id:"all-in-one-config",children:"All-in-one Config"}),"\n",(0,t.jsx)(e.pre,{children:(0,t.jsx)(e.code,{className:"language-json5",metastring:'title="CONFIG_DIR/config.json"',children:'{\n "sourceDefaults": {\n "maxPollRetries": 0, // optional, default # of automatic polling restarts on error. can be overridden by property in individual config\n "maxRequestRetries": 1, // optional, default # of http request retries a source can make before error is thrown. can be overridden by property in individual config\n "retryMultiplier": 1.5, // optional, default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying). can be overridden by property in individual config\n },\n "clientDefaults": {\n "maxRequestRetries": 1, // optional, default # of http request retries a client can make before error is thrown. can be overridden by property in individual config\n "retryMultiplier": 1.5, // optional, default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying). can be overridden by property in individual config\n },\n "sources": [\n {\n "type": "spotify",\n "name": "foxxSpot",\n "clients": ["foxxMaloja"],\n "data": {\n "clientId": "foxxSpotifyAppId", \n "clientSecret": "foxxSpotifyAppSecret",\n },\n "options": {\n "maxRequestRetries": 2, // override default max retries because spotify can...spotty\n }\n },\n {\n "type": "spotify",\n "name": "marySpot",\n "clients": ["maryMaloja"],\n "data": {\n "clientId": "foxxSpotifyAppId", // only need one application, it can be used by all users of this multi-scrobbler instance \n "clientSecret": "foxxSpotifyAppSecret",\n }\n },\n {\n "type": "spotify",\n "name": "fredSpot",\n "clients": ["fredMaloja"],\n "data": {\n "accessToken": "fredsToken",\n "refreshToken": "fredsRefreshToken",\n "interval": 120, // he also wants a slower check interval because his application already has heavy api usage\n }\n },\n {\n "type": "plex",\n "name": "fredPlex",\n "clients": ["fredMaloja"],\n "data": {\n "token": "JtRnwQWD__XMJF8sT3jc",\n "url": "192.168.0.233:32400",\n "usersallow": ["fredUser"]\n }\n },\n {\n "type": "plex",\n "name": "maryPlex",\n "clients": ["maryMaloja"],\n "data": {\n "token": "JtRnwQWD__XMJF8sT3jc",\n "url": "192.168.0.233:32400",\n "usersallow": ["maryUser"], // still need to specify mary as user so not all users who play from \'podcasts\' get scrobbled\n "librariesAllow": ["podcasts"]\n }\n },\n {\n "type": "plex",\n "name": "partyPlex",\n // omitting clients (or making it empty) will make this Source scrobble to all Clients\n "data": {\n "token": "JtRnwQWD__XMJF8sT3jc",\n "url": "192.168.0.233:32400",\n "librariesAllow": ["party"]\n }\n },\n {\n "type": "jellyfin",\n "name": "FredJelly",\n // omitting clients (or making it empty) will make this Source scrobble to all Clients\n "data": {\n "url": "http://localhost:8096",\n "user": "FoxxMD",\n "apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c",\n "usersAllow": ["fredUser"]\n }\n },\n {\n "type": "subsonic",\n "name": "foxxAirsonic",\n "clients": ["foxxMaloja"],\n "data": {\n "user": "foxx",\n "password": "foxxPassword",\n "url": "https://airsonic.foxx.example"\n }\n },\n {\n "type": "ytmusic",\n "name": "foxxYoutube",\n "clients": ["foxxMaloja"],\n "data": {\n "cookie": "__Secure-3PAPISID=3AxsXpy0MKGu75Qb/AkISXGqOnSDn1jEKn; DEVICE_INFO=ChxOekU0Tmpjek5EWTBPRGd3TlRBMk16QXpNdz09EJbS8Z0GGJbS8Z0G; ...",\n }\n },\n {\n "type": "endpointlz",\n "name": "listenbrainzfoxx",\n "clients": ["foxxMaloja"],\n "data": {\n "token": "myToken"\n }\n }\n ],\n "clients": [\n {\n "type": "maloja",\n "name": "foxxMaloja",\n "data": {\n "url": "https://maloja.foxx.example",\n "apiKey": "foxxApiKey"\n }\n },\n {\n "type": "maloja",\n "name": "fredMaloja",\n "data": {\n "url": "https://maloja.fred.example",\n "apiKey": "fredApiKey"\n }\n },\n {\n "type": "maloja",\n "name": "maryMaloja",\n "data": {\n "url": "https://maloja.mary.example",\n "apiKey": "maryApiKey"\n }\n },\n {\n "type": "lastfm",\n "name": "maryLFM",\n "data": {\n "apiKey": "maryApiKey",\n "secret": "marySecret",\n "redirectUri": "http://localhost:9078/lastfm/callback"\n }\n }\n ]\n}\n\n'})}),"\n",(0,t.jsx)(e.h3,{id:"separate-json-files",children:"Separate JSON files"}),"\n",(0,t.jsx)(e.pre,{children:(0,t.jsx)(e.code,{className:"language-json5",metastring:'title="CONFIG_DIR/spotify.json"',children:'[\n {\n // may omit \'type\' property since app knows this is file is for spotify configs\n "name": "foxxSpot",\n "clients": ["foxxMaloja"],\n "data": {\n "clientId": "foxxSpotifyAppId",\n "clientSecret": "foxxSpotifyAppSecret"\n }\n },\n {\n "name": "marySpot",\n "clients": ["maryMaloja"],\n "data": {\n "clientId": "foxxSpotifyAppId",\n "clientSecret": "foxxSpotifyAppSecret"\n }\n },\n {\n "name": "fredSpot",\n "clients": ["fredMaloja"],\n "data": {\n "accessToken": "fredsToken",\n "refreshToken": "fredsRefreshToken",\n "interval": 120\n }\n },\n]\n'})}),"\n",(0,t.jsx)(e.pre,{children:(0,t.jsx)(e.code,{className:"language-json5",metastring:'title="CONFIG_DIR/plex.json"',children:'[\n {\n "name": "fredPlex",\n "clients": ["fredMaloja"],\n "data": {\n "token": "JtRnwQWD__XMJF8sT3jc",\n "url": "192.168.0.233:32400",\n "usersallow": ["fredUser"]\n }\n },\n {\n "name": "maryPlex",\n "clients": ["maryMaloja"],\n "data": {\n "token": "JtRnwQWD__XMJF8sT3jc",\n "url": "192.168.0.233:32400",\n "usersallow": ["maryUser"],\n "librariesAllow": ["podcasts"]\n }\n },\n {\n "name": "partyPlex",\n "data": {\n "token": "JtRnwQWD__XMJF8sT3jc",\n "url": "192.168.0.233:32400",\n "librariesAllow": ["party"]\n }\n }\n]\n'})}),"\n",(0,t.jsx)(e.pre,{children:(0,t.jsx)(e.code,{className:"language-json5",metastring:'title="CONFIG_DIR/jellyfin.json"',children:'[\n {\n "name": "FredJelly",\n "data": {\n "url": "http://localhost:8096",\n "user": "FoxxMD",\n "apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c",\n "usersAllow": ["fredUser"]\n }\n }\n]\n'})}),"\n",(0,t.jsx)(e.pre,{children:(0,t.jsx)(e.code,{className:"language-json5",metastring:'title="CONFIG_DIR/ytmusic.json"',children:'[\n {\n "name": "foxxYoutube",\n "clients": ["foxxMaloja"],\n "data": {\n "cookie": "__Secure-3PAPISID=3AxsXpy0MKGu75Qb/AkISXGqOnSDn1jEKn; DEVICE_INFO=ChxOekU0Tmpjek5EWTBPRGd3TlRBMk16QXpNdz09EJbS8Z0GGJbS8Z0G; ...",\n }\n }\n]\n'})}),"\n",(0,t.jsx)(e.pre,{children:(0,t.jsx)(e.code,{className:"language-json5",metastring:'title="CONFIG_DIR/endpointlz.json"',children:'[\n {\n "name": "listenbrainzfoxx",\n "clients": ["foxxMaloja"],\n "data": {\n "token": "myToken",\n }\n }\n]\n'})}),"\n",(0,t.jsx)(e.pre,{children:(0,t.jsx)(e.code,{className:"language-json5",metastring:'title="CONFIG_DIR/maloja.json"',children:'[\n {\n "name": "foxxMaloja",\n "data": {\n "url": "https://maloja.foxx.example",\n "apiKey": "foxxApiKey"\n }\n },\n {\n "name": "fredMaloja",\n "data": {\n "url": "https://maloja.fred.example",\n "apiKey": "fredApiKey"\n }\n },\n {\n "name": "maryMaloja",\n "data": {\n "url": "https://maloja.mary.example",\n "apiKey": "maryApiKey"\n }\n }\n]\n'})}),"\n",(0,t.jsx)(e.pre,{children:(0,t.jsx)(e.code,{className:"language-json5",metastring:'title="CONFIG_DIR/lastfm.json"',children:'[\n {\n "name": "maryLFM",\n "data": {\n "apiKey": "maryApiKey",\n "secret": "marySecret",\n "redirectUri": "http://localhost:9078/lastfm/callback"\n }\n }\n]\n'})})]})}function p(n={}){let{wrapper:e}={...(0,o.R)(),...n.components};return e?(0,t.jsx)(e,{...n,children:(0,t.jsx)(d,{...n})}):d(n)}}}]); \ No newline at end of file diff --git a/assets/js/main.f1a88529.js b/assets/js/main.44ab25b2.js similarity index 71% rename from assets/js/main.f1a88529.js rename to assets/js/main.44ab25b2.js index 8b9d5dc3..451724f7 100644 --- a/assets/js/main.f1a88529.js +++ b/assets/js/main.44ab25b2.js @@ -1,4 +1,4 @@ -(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["1889"],{6464(e,t,n){"use strict";n.r(t)},6448(e,t,n){"use strict";n.r(t)},8708(e,t,n){"use strict";n.d(t,{A:()=>f}),n(6540);var r=n(3259),a=n.n(r),o=n(4054);let i={"04d926a5":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("5730"),n.e("1495"),n.e("4461")]).then(n.bind(n,9625)),"@site/docs/configuration/configuration.mdx",9625],"07b3b246":[()=>Promise.all([n.e("5005"),n.e("7340")]).then(n.bind(n,4122)),"@site/docs/development/dev-client.md",4122],"08938dd7":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("7417")]).then(n.bind(n,1469)),"@site/docs/configuration/sources/rocksky-source.mdx",1469],"094000d3":[()=>Promise.all([n.e("5005"),n.e("8270")]).then(n.bind(n,938)),"@site/docs/configuration/transforms/transforms.mdx",938],"0ce74438":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("5730"),n.e("9311"),n.e("8406")]).then(n.bind(n,1891)),"@site/src/pages/playground.tsx",1891],"138e0e15":[()=>n.e("1234").then(n.t.bind(n,1597,19)),"@generated/@easyops-cn/docusaurus-search-local/default/__plugin.json",1597],"14eb3368":[()=>Promise.all([n.e("4014"),n.e("7234")]).then(n.bind(n,2646)),"@theme/DocCategoryGeneratedIndexPage",2646],"153c3ceb":[()=>Promise.all([n.e("5005"),n.e("1309")]).then(n.bind(n,9127)),"@site/docs/development/flatpak.md",9127],0x11113f9:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("5730"),n.e("9311"),n.e("1495"),n.e("434"),n.e("106")]).then(n.bind(n,1065)),"@theme/DocItem",1065],"1a4e3797":[()=>Promise.all([n.e("4014"),n.e("5847")]).then(n.bind(n,9272)),"@theme/SearchPage",9272],"1bb2dc41":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("2609")]).then(n.bind(n,442)),"@site/docs/configuration/clients/discord/discord-headless.mdx",442],"1fb61236":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("5093")]).then(n.bind(n,4745)),"@site/docs/configuration/sources/plex.mdx",4745],"202c4968":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("2068")]).then(n.bind(n,3668)),"@site/docs/configuration/sources/koito-source.mdx",3668],"24a5b6e6":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("1499")]).then(n.bind(n,6273)),"@site/docs/configuration/sources/azuracast.mdx",6273],"294f31e2":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("1920")]).then(n.bind(n,1389)),"@site/docs/configuration/sources/subsonic.mdx",1389],"2a24ad03":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("899")]).then(n.bind(n,9052)),"@site/docs/configuration/sources/sources.mdx",9052],"2ecbbad4":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("6473")]).then(n.bind(n,3535)),"@site/docs/configuration/sources/youtube-music.mdx",3535],"30829cd8":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("5077")]).then(n.bind(n,1762)),"@site/docs/configuration/sources/tealfm-source.mdx",1762],"33c4ed3f":[()=>Promise.all([n.e("5005"),n.e("429")]).then(n.bind(n,8779)),"@site/docs/development/dev-source.md",8779],"3827a595":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("6412")]).then(n.bind(n,7617)),"@site/docs/configuration/sources/lastfm-endpoint.mdx",7617],"41cd626a":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("3841")]).then(n.bind(n,1286)),"@site/docs/configuration/sources/vlc.mdx",1286],"425b8dd3":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("2072")]).then(n.bind(n,8899)),"@site/docs/configuration/sources/musikcube.mdx",8899],"44b2647e":[()=>Promise.all([n.e("5005"),n.e("6942")]).then(n.bind(n,5077)),"@site/docs/configuration/transforms/user.mdx",5077],"490e20a0":[()=>Promise.all([n.e("5005"),n.e("4479")]).then(n.bind(n,2989)),"@site/docs/help.mdx",2989],"493c00ca":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("941")]).then(n.bind(n,7527)),"@site/docs/configuration/sources/yamaha-musiccast.mdx",7527],"498c77bd":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("7430")]).then(n.bind(n,1951)),"@site/docs/configuration/sources/jriver.mdx",1951],"4edc808e":[()=>Promise.all([n.e("5005"),n.e("2677")]).then(n.bind(n,1733)),"@site/docs/index.mdx",1733],"4f1fc707":[()=>n.e("8430").then(n.t.bind(n,3676,19)),"@generated/docusaurus-plugin-content-docs/default/p/multi-scrobbler-category-updating-f22.json",3676],"4f7d60c5":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("9515")]).then(n.bind(n,6028)),"@site/docs/configuration/clients/listenbrainz.mdx",6028],"5930ad03":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("9077")]).then(n.bind(n,7072)),"@site/docs/configuration/sources/mopidy.mdx",7072],"5a378701":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("3922")]).then(n.bind(n,4703)),"@site/docs/configuration/sources/jellyfin.mdx",4703],"5a6ebb5c":[()=>Promise.all([n.e("5005"),n.e("7211")]).then(n.bind(n,2558)),"@site/docs/configuration/clients/discord/discord.mdx",2558],"5e95c892":[()=>n.e("1668").then(n.bind(n,7121)),"@theme/DocsRoot",7121],"5e9f5e1a":[()=>Promise.resolve().then(n.bind(n,4784)),"@generated/docusaurus.config",4784],0x3dd57b1:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("3895")]).then(n.bind(n,7838)),"@site/docs/configuration/clients/lastfm.mdx",7838],"72d905a9":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("7227")]).then(n.bind(n,4063)),"@site/docs/configuration/sources/google-cast.mdx",4063],0x45e4909:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("8123")]).then(n.bind(n,7075)),"@site/docs/configuration/sources/librefm-source.mdx",7075],0x476864f:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("7314")]).then(n.bind(n,4591)),"@site/docs/quickstart.mdx",4591],"778b0fa6":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("2613")]).then(n.bind(n,3161)),"@site/docs/configuration/sources/spotify.mdx",3161],"7b4f75c2":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("9494")]).then(n.bind(n,2754)),"@site/docs/configuration/clients/librefm.mdx",2754],"7d837a1d":[()=>n.e("4787").then(n.t.bind(n,476,19)),"@generated/docusaurus-plugin-content-docs/default/p/multi-scrobbler-ae0.json",476],"7ebd3339":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("9536")]).then(n.bind(n,7983)),"@site/docs/configuration/sources/sonos.mdx",7983],"812e048c":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("9991")]).then(n.bind(n,1885)),"@site/docs/configuration/clients/tealfm.mdx",1885],"8b39be93":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("3963")]).then(n.bind(n,1724)),"@site/docs/configuration/sources/icecast.mdx",1724],"91a78c9f":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("2076")]).then(n.bind(n,7079)),"@site/docs/configuration/sources/mpd.mdx",7079],"925abbc7":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("4123")]).then(n.bind(n,4183)),"@site/docs/configuration/sources/deezer.mdx",4183],"97d34c1a":[()=>n.e("9698").then(n.t.bind(n,4907,19)),"@generated/docusaurus-plugin-content-docs/default/p/multi-scrobbler-category-development-f99.json",4907],"9bcab3db":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("490")]).then(n.bind(n,5345)),"@site/docs/configuration/sources/lastfm-source.mdx",5345],"9fd2a592":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("9212")]).then(n.bind(n,1639)),"@site/docs/configuration/sources/webscrobbler.mdx",1639],a16703cd:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("5515")]).then(n.bind(n,662)),"@site/docs/configuration/clients/clients.mdx",662],a7456010:[()=>n.e("3056").then(n.t.bind(n,8552,19)),"@generated/docusaurus-plugin-content-pages/default/__plugin.json",8552],a7bd4aaa:[()=>n.e("5575").then(n.bind(n,4532)),"@theme/DocVersionRoot",4532],a93ae359:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("7212")]).then(n.bind(n,136)),"@site/docs/configuration/sources/listenbrainz-endpoint.mdx",136],a94703ab:[()=>Promise.all([n.e("4014"),n.e("6785")]).then(n.bind(n,607)),"@theme/DocRoot",607],aba21aa0:[()=>n.e("3747").then(n.t.bind(n,7093,19)),"@generated/docusaurus-plugin-content-docs/default/__plugin.json",7093],ad895e75:[()=>Promise.all([n.e("5005"),n.e("8160")]).then(n.bind(n,1943)),"@site/docs/FAQ.md",1943],af9480c3:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("232")]).then(n.bind(n,6613)),"@site/docs/configuration/sources/listenbrainz-source.mdx",6613],b46f7c12:[()=>Promise.all([n.e("5005"),n.e("6691")]).then(n.bind(n,2038)),"@site/docs/installation/service.md",2038],bf69edfb:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("7736")]).then(n.bind(n,6684)),"@site/docs/installation/installation.mdx",6684],c2e935db:[()=>Promise.all([n.e("5005"),n.e("1785")]).then(n.bind(n,8537)),"@site/docs/development/dev-common.md",8537],cdda7d53:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("3763")]).then(n.bind(n,5178)),"@site/docs/configuration/clients/maloja.mdx",5178],d3a18f08:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("8037")]).then(n.bind(n,8106)),"@site/docs/updating/updating.mdx",8106],d50915e5:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("7220")]).then(n.bind(n,7954)),"@site/docs/configuration/sources/yandex-music.mdx",7954],d53edd15:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("5730"),n.e("9311"),n.e("1495"),n.e("2077")]).then(n.bind(n,6185)),"@site/docs/configuration/sources/kodi.mdx",6185],dfa6d5b1:[()=>Promise.all([n.e("5005"),n.e("539")]).then(n.bind(n,3743)),"@site/docs/configuration/transforms/native.mdx",3743],e6ac3f64:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("1184")]).then(n.bind(n,265)),"@site/docs/configuration/transforms/musicbrainz.mdx",265],e7a53bbd:[()=>Promise.all([n.e("5005"),n.e("1353")]).then(n.bind(n,1317)),"@site/docs/configuration/kitchensink.mdx",1317],e82a470f:[()=>Promise.all([n.e("5005"),n.e("1703")]).then(n.bind(n,9831)),"@site/docs/configuration/clients/rocksky.mdx",9831],ea69cd8d:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("8968")]).then(n.bind(n,1980)),"@site/docs/configuration/clients/discord/discord-app.mdx",1980],f18fd350:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("2351")]).then(n.bind(n,9238)),"@site/docs/configuration/clients/koito.mdx",9238],fa357871:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("5614")]).then(n.bind(n,5855)),"@site/docs/configuration/sources/maloja.mdx",5855],fee59995:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("9627")]).then(n.bind(n,5218)),"@site/docs/configuration/sources/mpris.mdx",5218]};var l=n(4848);function s({error:e,retry:t,pastDelay:n}){return e?(0,l.jsxs)("div",{style:{textAlign:"center",color:"#fff",backgroundColor:"#fa383e",borderColor:"#fa383e",borderStyle:"solid",borderRadius:"0.25rem",borderWidth:"1px",boxSizing:"border-box",display:"block",padding:"1rem",flex:"0 0 50%",marginLeft:"25%",marginRight:"25%",marginTop:"5rem",maxWidth:"50%",width:"100%"},children:[(0,l.jsx)("p",{children:String(e)}),(0,l.jsx)("div",{children:(0,l.jsx)("button",{type:"button",onClick:t,children:"Retry"})})]}):n?(0,l.jsx)("div",{style:{display:"flex",justifyContent:"center",alignItems:"center",height:"100vh"},children:(0,l.jsx)("svg",{id:"loader",style:{width:128,height:110,position:"absolute",top:"calc(100vh - 64%)"},viewBox:"0 0 45 45",xmlns:"http://www.w3.org/2000/svg",stroke:"#61dafb",children:(0,l.jsxs)("g",{fill:"none",fillRule:"evenodd",transform:"translate(1 1)",strokeWidth:"2",children:[(0,l.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,l.jsx)("animate",{attributeName:"r",begin:"1.5s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-opacity",begin:"1.5s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-width",begin:"1.5s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,l.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,l.jsx)("animate",{attributeName:"r",begin:"3s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-opacity",begin:"3s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-width",begin:"3s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,l.jsx)("circle",{cx:"22",cy:"22",r:"8",children:(0,l.jsx)("animate",{attributeName:"r",begin:"0s",dur:"1.5s",values:"6;1;2;3;4;5;6",calcMode:"linear",repeatCount:"indefinite"})})]})})}):null}var u=n(6921),c=n(3102);function d(e,t){if("*"===e)return a()({loading:s,loader:()=>n.e("4494").then(n.bind(n,2237)),modules:["@theme/NotFound"],webpack:()=>[2237],render(e,t){let n=e.default;return(0,l.jsx)(c.W,{value:{plugin:{name:"native",id:"default"}},children:(0,l.jsx)(n,{...t})})}});let r=o[`${e}-${t}`],d={},f=[],p=[];return Object.entries((0,u.A)(r)).forEach(([e,t])=>{let n=i[t];n&&(d[e]=n[0],f.push(n[1]),p.push(n[2]))}),a().Map({loading:s,loader:d,modules:f,webpack:()=>p,render(t,n){let a=JSON.parse(JSON.stringify(r));Object.entries(t).forEach(([t,n])=>{let r=n.default;if(!r)throw Error(`The page component at ${e} doesn't have a default export. This makes it impossible to render anything. Consider default-exporting a React component.`);("object"==typeof r||"function"==typeof r)&&Object.keys(n).filter(e=>"default"!==e).forEach(e=>{r[e]=n[e]});let o=a,i=t.split(".");i.slice(0,-1).forEach(e=>{o=o[e]}),o[i[i.length-1]]=r});let o=a.__comp;delete a.__comp;let i=a.__context;delete a.__context;let s=a.__props;return delete a.__props,(0,l.jsx)(c.W,{value:i,children:(0,l.jsx)(o,{...a,...s,...n})})}})}let f=[{path:"/multi-scrobbler/playground/",component:d("/multi-scrobbler/playground/","18f"),exact:!0},{path:"/multi-scrobbler/search/",component:d("/multi-scrobbler/search/","71d"),exact:!0},{path:"/multi-scrobbler/",component:d("/multi-scrobbler/","f39"),routes:[{path:"/multi-scrobbler/",component:d("/multi-scrobbler/","120"),routes:[{path:"/multi-scrobbler/",component:d("/multi-scrobbler/","e74"),routes:[{path:"/multi-scrobbler/category/development/",component:d("/multi-scrobbler/category/development/","626"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/category/updating/",component:d("/multi-scrobbler/category/updating/","719"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/",component:d("/multi-scrobbler/configuration/","230"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/",component:d("/multi-scrobbler/configuration/clients/","099"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/discord/",component:d("/multi-scrobbler/configuration/clients/discord/","d20"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/discord/discord-app/",component:d("/multi-scrobbler/configuration/clients/discord/discord-app/","5ba"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/discord/discord-headless/",component:d("/multi-scrobbler/configuration/clients/discord/discord-headless/","bb1"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/koito/",component:d("/multi-scrobbler/configuration/clients/koito/","bcb"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/lastfm/",component:d("/multi-scrobbler/configuration/clients/lastfm/","5c9"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/librefm/",component:d("/multi-scrobbler/configuration/clients/librefm/","dd4"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/listenbrainz/",component:d("/multi-scrobbler/configuration/clients/listenbrainz/","be2"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/maloja/",component:d("/multi-scrobbler/configuration/clients/maloja/","dc4"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/rocksky/",component:d("/multi-scrobbler/configuration/clients/rocksky/","489"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/tealfm/",component:d("/multi-scrobbler/configuration/clients/tealfm/","743"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/kitchensink/",component:d("/multi-scrobbler/configuration/kitchensink/","7d2"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/",component:d("/multi-scrobbler/configuration/sources/","479"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/azuracast/",component:d("/multi-scrobbler/configuration/sources/azuracast/","434"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/deezer/",component:d("/multi-scrobbler/configuration/sources/deezer/","d7a"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/google-cast/",component:d("/multi-scrobbler/configuration/sources/google-cast/","434"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/icecast/",component:d("/multi-scrobbler/configuration/sources/icecast/","dfe"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/jellyfin/",component:d("/multi-scrobbler/configuration/sources/jellyfin/","2fe"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/jriver/",component:d("/multi-scrobbler/configuration/sources/jriver/","a36"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/kodi/",component:d("/multi-scrobbler/configuration/sources/kodi/","ab6"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/koito-source/",component:d("/multi-scrobbler/configuration/sources/koito-source/","d25"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/lastfm-endpoint/",component:d("/multi-scrobbler/configuration/sources/lastfm-endpoint/","d0b"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/lastfm-source/",component:d("/multi-scrobbler/configuration/sources/lastfm-source/","f93"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/librefm-source/",component:d("/multi-scrobbler/configuration/sources/librefm-source/","2b0"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/listenbrainz-endpoint/",component:d("/multi-scrobbler/configuration/sources/listenbrainz-endpoint/","b80"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/listenbrainz-source/",component:d("/multi-scrobbler/configuration/sources/listenbrainz-source/","8ff"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/maloja/",component:d("/multi-scrobbler/configuration/sources/maloja/","ed2"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/mopidy/",component:d("/multi-scrobbler/configuration/sources/mopidy/","b30"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/mpd/",component:d("/multi-scrobbler/configuration/sources/mpd/","e96"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/mpris/",component:d("/multi-scrobbler/configuration/sources/mpris/","751"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/musikcube/",component:d("/multi-scrobbler/configuration/sources/musikcube/","515"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/plex/",component:d("/multi-scrobbler/configuration/sources/plex/","468"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/rocksky-source/",component:d("/multi-scrobbler/configuration/sources/rocksky-source/","c8d"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/sonos/",component:d("/multi-scrobbler/configuration/sources/sonos/","bcf"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/spotify/",component:d("/multi-scrobbler/configuration/sources/spotify/","0b5"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/subsonic/",component:d("/multi-scrobbler/configuration/sources/subsonic/","c34"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/tealfm-source/",component:d("/multi-scrobbler/configuration/sources/tealfm-source/","912"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/vlc/",component:d("/multi-scrobbler/configuration/sources/vlc/","918"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/webscrobbler/",component:d("/multi-scrobbler/configuration/sources/webscrobbler/","3e7"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/yamaha-musiccast/",component:d("/multi-scrobbler/configuration/sources/yamaha-musiccast/","5e1"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/yandex-music/",component:d("/multi-scrobbler/configuration/sources/yandex-music/","1b9"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/youtube-music/",component:d("/multi-scrobbler/configuration/sources/youtube-music/","e7a"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/transforms/",component:d("/multi-scrobbler/configuration/transforms/","5d3"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/transforms/musicbrainz/",component:d("/multi-scrobbler/configuration/transforms/musicbrainz/","7d6"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/transforms/native/",component:d("/multi-scrobbler/configuration/transforms/native/","57a"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/transforms/user/",component:d("/multi-scrobbler/configuration/transforms/user/","9c5"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/development/dev-client/",component:d("/multi-scrobbler/development/dev-client/","d6f"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/development/dev-common/",component:d("/multi-scrobbler/development/dev-common/","878"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/development/dev-source/",component:d("/multi-scrobbler/development/dev-source/","98f"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/development/flatpak/",component:d("/multi-scrobbler/development/flatpak/","b8b"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/FAQ/",component:d("/multi-scrobbler/FAQ/","07c"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/help/",component:d("/multi-scrobbler/help/","6b4"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/installation/",component:d("/multi-scrobbler/installation/","371"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/installation/service/",component:d("/multi-scrobbler/installation/service/","97c"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/quickstart/",component:d("/multi-scrobbler/quickstart/","cfc"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/updating/",component:d("/multi-scrobbler/updating/","065"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/",component:d("/multi-scrobbler/","e36"),exact:!0,sidebar:"tutorialSidebar"}]}]}]},{path:"*",component:d("*")}]},6125(e,t,n){"use strict";n.d(t,{o:()=>o,x:()=>i});var r=n(6540),a=n(4848);let o=r.createContext(!1);function i({children:e}){let[t,n]=(0,r.useState)(!1);return(0,r.useEffect)(()=>{n(!0)},[]),(0,a.jsx)(o.Provider,{value:t,children:e})}},8474(e,t,n){"use strict";var r=n(6540),a=n(5338),o=n(545),i=n(4625),l=n(4784),s=n(8193);let u=[n(6464),n(7073),n(6294),n(6448)];var c=n(8708),d=n(6347),f=n(2831),p=n(4848);function m({children:e}){return(0,p.jsx)(p.Fragment,{children:e})}var h=n(4563);let g=e=>e.defaultFormatter(e);function b({children:e}){return(0,p.jsx)(h.AL,{formatter:g,children:e})}function y({children:e}){return(0,p.jsx)(b,{children:e})}var v=n(5260),k=n(4586),w=n(6025),x=n(6342),S=n(5500),E=n(2131),_=n(4849),A=n(440),C=n(1463);function T(){let{i18n:{currentLocale:e,defaultLocale:t,localeConfigs:n}}=(0,k.default)(),r=(0,E.o)(),a=n[e].htmlLang;return(0,p.jsxs)(v.A,{children:[Object.entries(n).map(([e,{htmlLang:t}])=>(0,p.jsx)("link",{rel:"alternate",href:r.createUrl({locale:e,fullyQualified:!0}),hrefLang:t},e)),(0,p.jsx)("link",{rel:"alternate",href:r.createUrl({locale:t,fullyQualified:!0}),hrefLang:"x-default"}),(0,p.jsx)("meta",{property:"og:locale",content:a.replace("-","_")}),Object.values(n).filter(e=>a!==e.htmlLang).map(e=>(0,p.jsx)("meta",{property:"og:locale:alternate",content:e.htmlLang.replace("-","_")},`meta-og-${e.htmlLang}`))]})}function P({permalink:e}){let{siteConfig:{url:t}}=(0,k.default)(),n=function(){let{siteConfig:{url:e,baseUrl:t,trailingSlash:n}}=(0,k.default)(),{pathname:r}=(0,d.zy)();return e+(0,A.applyTrailingSlash)((0,w.Ay)(r),{trailingSlash:n,baseUrl:t})}(),r=e?`${t}${e}`:n;return(0,p.jsxs)(v.A,{children:[(0,p.jsx)("meta",{property:"og:url",content:r}),(0,p.jsx)("link",{rel:"canonical",href:r})]})}function O(){let{i18n:{currentLocale:e}}=(0,k.default)(),{metadata:t,image:n}=(0,x.p)();return(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(v.A,{children:[(0,p.jsx)("meta",{name:"twitter:card",content:"summary_large_image"}),(0,p.jsx)("body",{className:_.w})]}),n&&(0,p.jsx)(S.be,{image:n}),(0,p.jsx)(P,{}),(0,p.jsx)(T,{}),(0,p.jsx)(C.A,{tag:"default",locale:e}),(0,p.jsx)(v.A,{children:t.map((e,t)=>(0,p.jsx)("meta",{...e},t))})]})}let L=new Map;var j=n(6125),N=n(2603),R=n(205);function I(e,...t){let n=u.map(n=>{let r=n.default?.[e]??n[e];return r?.(...t)});return()=>n.forEach(e=>e?.())}let F=function({children:e,location:t,previousLocation:n}){return(0,R.A)(()=>{n!==t&&(!function({location:e,previousLocation:t}){if(!t)return;let n=e.pathname===t.pathname,r=e.hash===t.hash,a=e.search===t.search;if(n&&r&&!a)return;let{hash:o}=e;if(o){let e=decodeURIComponent(o.substring(1)),t=document.getElementById(e);t?.scrollIntoView()}else window.scrollTo(0,0)}({location:t,previousLocation:n}),I("onRouteDidUpdate",{previousLocation:n,location:t}))},[n,t]),e};function D(e){return Promise.all(Array.from(new Set([e,decodeURI(e)])).map(e=>(0,f.u)(c.A,e)).flat().map(e=>e.route.component.preload?.()))}class z extends r.Component{previousLocation;routeUpdateCleanupCb;constructor(e){super(e),this.previousLocation=null,this.routeUpdateCleanupCb=s.A.canUseDOM?I("onRouteUpdate",{previousLocation:null,location:this.props.location}):()=>{},this.state={nextRouteHasLoaded:!0}}shouldComponentUpdate(e,t){if(e.location===this.props.location)return t.nextRouteHasLoaded;let n=e.location;return this.previousLocation=this.props.location,this.setState({nextRouteHasLoaded:!1}),this.routeUpdateCleanupCb=I("onRouteUpdate",{previousLocation:this.previousLocation,location:n}),D(n.pathname).then(()=>{this.routeUpdateCleanupCb(),this.setState({nextRouteHasLoaded:!0})}).catch(e=>{console.warn(e),window.location.reload()}),!1}render(){let{children:e,location:t}=this.props;return(0,p.jsx)(F,{previousLocation:this.previousLocation,location:t,children:(0,p.jsx)(d.qh,{location:t,render:()=>e})})}}let M="__docusaurus-base-url-issue-banner-suggestion-container";function $(){let{siteConfig:{baseUrl:e}}=(0,k.default)();return(0,p.jsx)(p.Fragment,{children:!s.A.canUseDOM&&(0,p.jsx)(v.A,{children:(0,p.jsx)("script",{children:` +(self.webpackChunkmulti_scrobbler_docs=self.webpackChunkmulti_scrobbler_docs||[]).push([["1889"],{6464(e,t,n){"use strict";n.r(t)},6448(e,t,n){"use strict";n.r(t)},8708(e,t,n){"use strict";n.d(t,{A:()=>f}),n(6540);var r=n(3259),a=n.n(r),o=n(4054);let i={"04d926a5":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("5730"),n.e("1495"),n.e("4461")]).then(n.bind(n,9625)),"@site/docs/configuration/configuration.mdx",9625],"08938dd7":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("7417")]).then(n.bind(n,1469)),"@site/docs/configuration/sources/rocksky-source.mdx",1469],"094000d3":[()=>Promise.all([n.e("5005"),n.e("8270")]).then(n.bind(n,938)),"@site/docs/configuration/transforms/transforms.mdx",938],"0ce74438":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("5730"),n.e("9311"),n.e("8406")]).then(n.bind(n,1891)),"@site/src/pages/playground.tsx",1891],"138e0e15":[()=>n.e("1234").then(n.t.bind(n,1597,19)),"@generated/@easyops-cn/docusaurus-search-local/default/__plugin.json",1597],"14eb3368":[()=>Promise.all([n.e("4014"),n.e("7234")]).then(n.bind(n,2646)),"@theme/DocCategoryGeneratedIndexPage",2646],"153c3ceb":[()=>Promise.all([n.e("5005"),n.e("1309")]).then(n.bind(n,9127)),"@site/docs/development/flatpak.md",9127],0x11113f9:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("5730"),n.e("9311"),n.e("1495"),n.e("434"),n.e("106")]).then(n.bind(n,1065)),"@theme/DocItem",1065],"1a4e3797":[()=>Promise.all([n.e("4014"),n.e("5847")]).then(n.bind(n,9272)),"@theme/SearchPage",9272],"1bb2dc41":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("2609")]).then(n.bind(n,442)),"@site/docs/configuration/clients/discord/discord-headless.mdx",442],"1fb61236":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("5093")]).then(n.bind(n,4745)),"@site/docs/configuration/sources/plex.mdx",4745],"202c4968":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("2068")]).then(n.bind(n,3668)),"@site/docs/configuration/sources/koito-source.mdx",3668],"24a5b6e6":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("1499")]).then(n.bind(n,6273)),"@site/docs/configuration/sources/azuracast.mdx",6273],"294f31e2":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("1920")]).then(n.bind(n,1389)),"@site/docs/configuration/sources/subsonic.mdx",1389],"2a24ad03":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("899")]).then(n.bind(n,9052)),"@site/docs/configuration/sources/sources.mdx",9052],"2ecbbad4":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("6473")]).then(n.bind(n,3535)),"@site/docs/configuration/sources/youtube-music.mdx",3535],"30829cd8":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("5077")]).then(n.bind(n,1762)),"@site/docs/configuration/sources/tealfm-source.mdx",1762],"33c4ed3f":[()=>Promise.all([n.e("5005"),n.e("429")]).then(n.bind(n,8779)),"@site/docs/development/dev-source.md",8779],"3827a595":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("6412")]).then(n.bind(n,7617)),"@site/docs/configuration/sources/lastfm-endpoint.mdx",7617],"41cd626a":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("3841")]).then(n.bind(n,1286)),"@site/docs/configuration/sources/vlc.mdx",1286],"425b8dd3":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("2072")]).then(n.bind(n,8899)),"@site/docs/configuration/sources/musikcube.mdx",8899],"44b2647e":[()=>Promise.all([n.e("5005"),n.e("6942")]).then(n.bind(n,5077)),"@site/docs/configuration/transforms/user.mdx",5077],"490e20a0":[()=>Promise.all([n.e("5005"),n.e("4479")]).then(n.bind(n,2989)),"@site/docs/help.mdx",2989],"493c00ca":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("941")]).then(n.bind(n,7527)),"@site/docs/configuration/sources/yamaha-musiccast.mdx",7527],"498c77bd":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("7430")]).then(n.bind(n,1951)),"@site/docs/configuration/sources/jriver.mdx",1951],"4edc808e":[()=>Promise.all([n.e("5005"),n.e("2677")]).then(n.bind(n,1733)),"@site/docs/index.mdx",1733],"4f1fc707":[()=>n.e("8430").then(n.t.bind(n,3676,19)),"@generated/docusaurus-plugin-content-docs/default/p/multi-scrobbler-category-updating-f22.json",3676],"4f7d60c5":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("9515")]).then(n.bind(n,6028)),"@site/docs/configuration/clients/listenbrainz.mdx",6028],"5930ad03":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("9077")]).then(n.bind(n,7072)),"@site/docs/configuration/sources/mopidy.mdx",7072],"5a378701":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("3922")]).then(n.bind(n,4703)),"@site/docs/configuration/sources/jellyfin.mdx",4703],"5a6ebb5c":[()=>Promise.all([n.e("5005"),n.e("7211")]).then(n.bind(n,2558)),"@site/docs/configuration/clients/discord/discord.mdx",2558],"5e95c892":[()=>n.e("1668").then(n.bind(n,7121)),"@theme/DocsRoot",7121],"5e9f5e1a":[()=>Promise.resolve().then(n.bind(n,4784)),"@generated/docusaurus.config",4784],0x3dd57b1:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("3895")]).then(n.bind(n,7838)),"@site/docs/configuration/clients/lastfm.mdx",7838],"72d905a9":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("7227")]).then(n.bind(n,4063)),"@site/docs/configuration/sources/google-cast.mdx",4063],0x45e4909:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("8123")]).then(n.bind(n,7075)),"@site/docs/configuration/sources/librefm-source.mdx",7075],0x476864f:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("7314")]).then(n.bind(n,4591)),"@site/docs/quickstart.mdx",4591],"778b0fa6":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("2613")]).then(n.bind(n,3161)),"@site/docs/configuration/sources/spotify.mdx",3161],"7b4f75c2":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("9494")]).then(n.bind(n,2754)),"@site/docs/configuration/clients/librefm.mdx",2754],"7d837a1d":[()=>n.e("4787").then(n.t.bind(n,476,19)),"@generated/docusaurus-plugin-content-docs/default/p/multi-scrobbler-ae0.json",476],"7ebd3339":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("9536")]).then(n.bind(n,7983)),"@site/docs/configuration/sources/sonos.mdx",7983],"812e048c":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("9991")]).then(n.bind(n,1885)),"@site/docs/configuration/clients/tealfm.mdx",1885],"8b39be93":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("3963")]).then(n.bind(n,1724)),"@site/docs/configuration/sources/icecast.mdx",1724],"91a78c9f":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("2076")]).then(n.bind(n,7079)),"@site/docs/configuration/sources/mpd.mdx",7079],"925abbc7":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("4123")]).then(n.bind(n,4183)),"@site/docs/configuration/sources/deezer.mdx",4183],"97d34c1a":[()=>n.e("9698").then(n.t.bind(n,4907,19)),"@generated/docusaurus-plugin-content-docs/default/p/multi-scrobbler-category-development-f99.json",4907],"9bcab3db":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("490")]).then(n.bind(n,5345)),"@site/docs/configuration/sources/lastfm-source.mdx",5345],"9fd2a592":[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("9212")]).then(n.bind(n,1639)),"@site/docs/configuration/sources/webscrobbler.mdx",1639],a16703cd:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("5515")]).then(n.bind(n,662)),"@site/docs/configuration/clients/clients.mdx",662],a7456010:[()=>n.e("3056").then(n.t.bind(n,8552,19)),"@generated/docusaurus-plugin-content-pages/default/__plugin.json",8552],a7bd4aaa:[()=>n.e("5575").then(n.bind(n,4532)),"@theme/DocVersionRoot",4532],a93ae359:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("7212")]).then(n.bind(n,136)),"@site/docs/configuration/sources/listenbrainz-endpoint.mdx",136],a94703ab:[()=>Promise.all([n.e("4014"),n.e("6785")]).then(n.bind(n,607)),"@theme/DocRoot",607],aba21aa0:[()=>n.e("3747").then(n.t.bind(n,7093,19)),"@generated/docusaurus-plugin-content-docs/default/__plugin.json",7093],ad895e75:[()=>Promise.all([n.e("5005"),n.e("8160")]).then(n.bind(n,1943)),"@site/docs/FAQ.md",1943],af9480c3:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("232")]).then(n.bind(n,6613)),"@site/docs/configuration/sources/listenbrainz-source.mdx",6613],b46f7c12:[()=>Promise.all([n.e("5005"),n.e("6691")]).then(n.bind(n,2038)),"@site/docs/installation/service.md",2038],bf69edfb:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("7736")]).then(n.bind(n,6684)),"@site/docs/installation/installation.mdx",6684],c2e935db:[()=>Promise.all([n.e("5005"),n.e("1785")]).then(n.bind(n,8537)),"@site/docs/development/dev-common.md",8537],cdda7d53:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("3763")]).then(n.bind(n,5178)),"@site/docs/configuration/clients/maloja.mdx",5178],d3a18f08:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("8037")]).then(n.bind(n,8106)),"@site/docs/updating/updating.mdx",8106],d50915e5:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("7220")]).then(n.bind(n,7954)),"@site/docs/configuration/sources/yandex-music.mdx",7954],d53edd15:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("5730"),n.e("9311"),n.e("1495"),n.e("2077")]).then(n.bind(n,6185)),"@site/docs/configuration/sources/kodi.mdx",6185],dfa6d5b1:[()=>Promise.all([n.e("5005"),n.e("539")]).then(n.bind(n,3743)),"@site/docs/configuration/transforms/native.mdx",3743],e6ac3f64:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("1184")]).then(n.bind(n,265)),"@site/docs/configuration/transforms/musicbrainz.mdx",265],e7a53bbd:[()=>Promise.all([n.e("5005"),n.e("1353")]).then(n.bind(n,1317)),"@site/docs/configuration/kitchensink.mdx",1317],e82a470f:[()=>Promise.all([n.e("5005"),n.e("1703")]).then(n.bind(n,9831)),"@site/docs/configuration/clients/rocksky.mdx",9831],ea69cd8d:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("8968")]).then(n.bind(n,1980)),"@site/docs/configuration/clients/discord/discord-app.mdx",1980],f18fd350:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("2351")]).then(n.bind(n,9238)),"@site/docs/configuration/clients/koito.mdx",9238],fa357871:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("5614")]).then(n.bind(n,5855)),"@site/docs/configuration/sources/maloja.mdx",5855],fee59995:[()=>Promise.all([n.e("4014"),n.e("5005"),n.e("9627")]).then(n.bind(n,5218)),"@site/docs/configuration/sources/mpris.mdx",5218]};var l=n(4848);function s({error:e,retry:t,pastDelay:n}){return e?(0,l.jsxs)("div",{style:{textAlign:"center",color:"#fff",backgroundColor:"#fa383e",borderColor:"#fa383e",borderStyle:"solid",borderRadius:"0.25rem",borderWidth:"1px",boxSizing:"border-box",display:"block",padding:"1rem",flex:"0 0 50%",marginLeft:"25%",marginRight:"25%",marginTop:"5rem",maxWidth:"50%",width:"100%"},children:[(0,l.jsx)("p",{children:String(e)}),(0,l.jsx)("div",{children:(0,l.jsx)("button",{type:"button",onClick:t,children:"Retry"})})]}):n?(0,l.jsx)("div",{style:{display:"flex",justifyContent:"center",alignItems:"center",height:"100vh"},children:(0,l.jsx)("svg",{id:"loader",style:{width:128,height:110,position:"absolute",top:"calc(100vh - 64%)"},viewBox:"0 0 45 45",xmlns:"http://www.w3.org/2000/svg",stroke:"#61dafb",children:(0,l.jsxs)("g",{fill:"none",fillRule:"evenodd",transform:"translate(1 1)",strokeWidth:"2",children:[(0,l.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,l.jsx)("animate",{attributeName:"r",begin:"1.5s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-opacity",begin:"1.5s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-width",begin:"1.5s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,l.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,l.jsx)("animate",{attributeName:"r",begin:"3s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-opacity",begin:"3s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-width",begin:"3s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,l.jsx)("circle",{cx:"22",cy:"22",r:"8",children:(0,l.jsx)("animate",{attributeName:"r",begin:"0s",dur:"1.5s",values:"6;1;2;3;4;5;6",calcMode:"linear",repeatCount:"indefinite"})})]})})}):null}var u=n(6921),c=n(3102);function d(e,t){if("*"===e)return a()({loading:s,loader:()=>n.e("4494").then(n.bind(n,2237)),modules:["@theme/NotFound"],webpack:()=>[2237],render(e,t){let n=e.default;return(0,l.jsx)(c.W,{value:{plugin:{name:"native",id:"default"}},children:(0,l.jsx)(n,{...t})})}});let r=o[`${e}-${t}`],d={},f=[],p=[];return Object.entries((0,u.A)(r)).forEach(([e,t])=>{let n=i[t];n&&(d[e]=n[0],f.push(n[1]),p.push(n[2]))}),a().Map({loading:s,loader:d,modules:f,webpack:()=>p,render(t,n){let a=JSON.parse(JSON.stringify(r));Object.entries(t).forEach(([t,n])=>{let r=n.default;if(!r)throw Error(`The page component at ${e} doesn't have a default export. This makes it impossible to render anything. Consider default-exporting a React component.`);("object"==typeof r||"function"==typeof r)&&Object.keys(n).filter(e=>"default"!==e).forEach(e=>{r[e]=n[e]});let o=a,i=t.split(".");i.slice(0,-1).forEach(e=>{o=o[e]}),o[i[i.length-1]]=r});let o=a.__comp;delete a.__comp;let i=a.__context;delete a.__context;let s=a.__props;return delete a.__props,(0,l.jsx)(c.W,{value:i,children:(0,l.jsx)(o,{...a,...s,...n})})}})}let f=[{path:"/multi-scrobbler/playground/",component:d("/multi-scrobbler/playground/","18f"),exact:!0},{path:"/multi-scrobbler/search/",component:d("/multi-scrobbler/search/","71d"),exact:!0},{path:"/multi-scrobbler/",component:d("/multi-scrobbler/","833"),routes:[{path:"/multi-scrobbler/",component:d("/multi-scrobbler/","916"),routes:[{path:"/multi-scrobbler/",component:d("/multi-scrobbler/","410"),routes:[{path:"/multi-scrobbler/category/development/",component:d("/multi-scrobbler/category/development/","626"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/category/updating/",component:d("/multi-scrobbler/category/updating/","719"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/",component:d("/multi-scrobbler/configuration/","230"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/",component:d("/multi-scrobbler/configuration/clients/","099"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/discord/",component:d("/multi-scrobbler/configuration/clients/discord/","d20"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/discord/discord-app/",component:d("/multi-scrobbler/configuration/clients/discord/discord-app/","5ba"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/discord/discord-headless/",component:d("/multi-scrobbler/configuration/clients/discord/discord-headless/","bb1"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/koito/",component:d("/multi-scrobbler/configuration/clients/koito/","bcb"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/lastfm/",component:d("/multi-scrobbler/configuration/clients/lastfm/","5c9"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/librefm/",component:d("/multi-scrobbler/configuration/clients/librefm/","dd4"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/listenbrainz/",component:d("/multi-scrobbler/configuration/clients/listenbrainz/","be2"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/maloja/",component:d("/multi-scrobbler/configuration/clients/maloja/","dc4"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/rocksky/",component:d("/multi-scrobbler/configuration/clients/rocksky/","489"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/clients/tealfm/",component:d("/multi-scrobbler/configuration/clients/tealfm/","743"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/kitchensink/",component:d("/multi-scrobbler/configuration/kitchensink/","7d2"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/",component:d("/multi-scrobbler/configuration/sources/","479"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/azuracast/",component:d("/multi-scrobbler/configuration/sources/azuracast/","434"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/deezer/",component:d("/multi-scrobbler/configuration/sources/deezer/","d7a"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/google-cast/",component:d("/multi-scrobbler/configuration/sources/google-cast/","434"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/icecast/",component:d("/multi-scrobbler/configuration/sources/icecast/","dfe"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/jellyfin/",component:d("/multi-scrobbler/configuration/sources/jellyfin/","2fe"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/jriver/",component:d("/multi-scrobbler/configuration/sources/jriver/","a36"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/kodi/",component:d("/multi-scrobbler/configuration/sources/kodi/","ab6"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/koito-source/",component:d("/multi-scrobbler/configuration/sources/koito-source/","d25"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/lastfm-endpoint/",component:d("/multi-scrobbler/configuration/sources/lastfm-endpoint/","d0b"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/lastfm-source/",component:d("/multi-scrobbler/configuration/sources/lastfm-source/","f93"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/librefm-source/",component:d("/multi-scrobbler/configuration/sources/librefm-source/","2b0"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/listenbrainz-endpoint/",component:d("/multi-scrobbler/configuration/sources/listenbrainz-endpoint/","b80"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/listenbrainz-source/",component:d("/multi-scrobbler/configuration/sources/listenbrainz-source/","8ff"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/maloja/",component:d("/multi-scrobbler/configuration/sources/maloja/","ed2"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/mopidy/",component:d("/multi-scrobbler/configuration/sources/mopidy/","b30"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/mpd/",component:d("/multi-scrobbler/configuration/sources/mpd/","e96"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/mpris/",component:d("/multi-scrobbler/configuration/sources/mpris/","751"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/musikcube/",component:d("/multi-scrobbler/configuration/sources/musikcube/","515"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/plex/",component:d("/multi-scrobbler/configuration/sources/plex/","468"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/rocksky-source/",component:d("/multi-scrobbler/configuration/sources/rocksky-source/","c8d"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/sonos/",component:d("/multi-scrobbler/configuration/sources/sonos/","bcf"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/spotify/",component:d("/multi-scrobbler/configuration/sources/spotify/","0b5"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/subsonic/",component:d("/multi-scrobbler/configuration/sources/subsonic/","c34"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/tealfm-source/",component:d("/multi-scrobbler/configuration/sources/tealfm-source/","912"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/vlc/",component:d("/multi-scrobbler/configuration/sources/vlc/","918"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/webscrobbler/",component:d("/multi-scrobbler/configuration/sources/webscrobbler/","3e7"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/yamaha-musiccast/",component:d("/multi-scrobbler/configuration/sources/yamaha-musiccast/","5e1"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/yandex-music/",component:d("/multi-scrobbler/configuration/sources/yandex-music/","1b9"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/sources/youtube-music/",component:d("/multi-scrobbler/configuration/sources/youtube-music/","e7a"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/transforms/",component:d("/multi-scrobbler/configuration/transforms/","5d3"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/transforms/musicbrainz/",component:d("/multi-scrobbler/configuration/transforms/musicbrainz/","7d6"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/transforms/native/",component:d("/multi-scrobbler/configuration/transforms/native/","57a"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/configuration/transforms/user/",component:d("/multi-scrobbler/configuration/transforms/user/","9c5"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/development/dev-common/",component:d("/multi-scrobbler/development/dev-common/","878"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/development/dev-source/",component:d("/multi-scrobbler/development/dev-source/","98f"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/development/flatpak/",component:d("/multi-scrobbler/development/flatpak/","b8b"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/FAQ/",component:d("/multi-scrobbler/FAQ/","07c"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/help/",component:d("/multi-scrobbler/help/","6b4"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/installation/",component:d("/multi-scrobbler/installation/","371"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/installation/service/",component:d("/multi-scrobbler/installation/service/","97c"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/quickstart/",component:d("/multi-scrobbler/quickstart/","cfc"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/updating/",component:d("/multi-scrobbler/updating/","065"),exact:!0,sidebar:"tutorialSidebar"},{path:"/multi-scrobbler/",component:d("/multi-scrobbler/","e36"),exact:!0,sidebar:"tutorialSidebar"}]}]}]},{path:"*",component:d("*")}]},6125(e,t,n){"use strict";n.d(t,{o:()=>o,x:()=>i});var r=n(6540),a=n(4848);let o=r.createContext(!1);function i({children:e}){let[t,n]=(0,r.useState)(!1);return(0,r.useEffect)(()=>{n(!0)},[]),(0,a.jsx)(o.Provider,{value:t,children:e})}},8474(e,t,n){"use strict";var r=n(6540),a=n(5338),o=n(545),i=n(4625),l=n(4784),s=n(8193);let u=[n(6464),n(7073),n(6294),n(6448)];var c=n(8708),d=n(6347),f=n(2831),p=n(4848);function m({children:e}){return(0,p.jsx)(p.Fragment,{children:e})}var h=n(4563);let g=e=>e.defaultFormatter(e);function b({children:e}){return(0,p.jsx)(h.AL,{formatter:g,children:e})}function y({children:e}){return(0,p.jsx)(b,{children:e})}var v=n(5260),k=n(4586),w=n(6025),x=n(6342),S=n(5500),E=n(2131),_=n(4849),A=n(440),C=n(1463);function T(){let{i18n:{currentLocale:e,defaultLocale:t,localeConfigs:n}}=(0,k.default)(),r=(0,E.o)(),a=n[e].htmlLang;return(0,p.jsxs)(v.A,{children:[Object.entries(n).map(([e,{htmlLang:t}])=>(0,p.jsx)("link",{rel:"alternate",href:r.createUrl({locale:e,fullyQualified:!0}),hrefLang:t},e)),(0,p.jsx)("link",{rel:"alternate",href:r.createUrl({locale:t,fullyQualified:!0}),hrefLang:"x-default"}),(0,p.jsx)("meta",{property:"og:locale",content:a.replace("-","_")}),Object.values(n).filter(e=>a!==e.htmlLang).map(e=>(0,p.jsx)("meta",{property:"og:locale:alternate",content:e.htmlLang.replace("-","_")},`meta-og-${e.htmlLang}`))]})}function P({permalink:e}){let{siteConfig:{url:t}}=(0,k.default)(),n=function(){let{siteConfig:{url:e,baseUrl:t,trailingSlash:n}}=(0,k.default)(),{pathname:r}=(0,d.zy)();return e+(0,A.applyTrailingSlash)((0,w.Ay)(r),{trailingSlash:n,baseUrl:t})}(),r=e?`${t}${e}`:n;return(0,p.jsxs)(v.A,{children:[(0,p.jsx)("meta",{property:"og:url",content:r}),(0,p.jsx)("link",{rel:"canonical",href:r})]})}function O(){let{i18n:{currentLocale:e}}=(0,k.default)(),{metadata:t,image:n}=(0,x.p)();return(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(v.A,{children:[(0,p.jsx)("meta",{name:"twitter:card",content:"summary_large_image"}),(0,p.jsx)("body",{className:_.w})]}),n&&(0,p.jsx)(S.be,{image:n}),(0,p.jsx)(P,{}),(0,p.jsx)(T,{}),(0,p.jsx)(C.A,{tag:"default",locale:e}),(0,p.jsx)(v.A,{children:t.map((e,t)=>(0,p.jsx)("meta",{...e},t))})]})}let L=new Map;var j=n(6125),N=n(2603),R=n(205);function I(e,...t){let n=u.map(n=>{let r=n.default?.[e]??n[e];return r?.(...t)});return()=>n.forEach(e=>e?.())}let F=function({children:e,location:t,previousLocation:n}){return(0,R.A)(()=>{n!==t&&(!function({location:e,previousLocation:t}){if(!t)return;let n=e.pathname===t.pathname,r=e.hash===t.hash,a=e.search===t.search;if(n&&r&&!a)return;let{hash:o}=e;if(o){let e=decodeURIComponent(o.substring(1)),t=document.getElementById(e);t?.scrollIntoView()}else window.scrollTo(0,0)}({location:t,previousLocation:n}),I("onRouteDidUpdate",{previousLocation:n,location:t}))},[n,t]),e};function D(e){return Promise.all(Array.from(new Set([e,decodeURI(e)])).map(e=>(0,f.u)(c.A,e)).flat().map(e=>e.route.component.preload?.()))}class z extends r.Component{previousLocation;routeUpdateCleanupCb;constructor(e){super(e),this.previousLocation=null,this.routeUpdateCleanupCb=s.A.canUseDOM?I("onRouteUpdate",{previousLocation:null,location:this.props.location}):()=>{},this.state={nextRouteHasLoaded:!0}}shouldComponentUpdate(e,t){if(e.location===this.props.location)return t.nextRouteHasLoaded;let n=e.location;return this.previousLocation=this.props.location,this.setState({nextRouteHasLoaded:!1}),this.routeUpdateCleanupCb=I("onRouteUpdate",{previousLocation:this.previousLocation,location:n}),D(n.pathname).then(()=>{this.routeUpdateCleanupCb(),this.setState({nextRouteHasLoaded:!0})}).catch(e=>{console.warn(e),window.location.reload()}),!1}render(){let{children:e,location:t}=this.props;return(0,p.jsx)(F,{previousLocation:this.previousLocation,location:t,children:(0,p.jsx)(d.qh,{location:t,render:()=>e})})}}let M="__docusaurus-base-url-issue-banner-suggestion-container";function $(){let{siteConfig:{baseUrl:e}}=(0,k.default)();return(0,p.jsx)(p.Fragment,{children:!s.A.canUseDOM&&(0,p.jsx)(v.A,{children:(0,p.jsx)("script",{children:` document.addEventListener('DOMContentLoaded', function maybeInsertBanner() { var shouldInsert = typeof window['docusaurus'] === 'undefined'; shouldInsert && insertBanner(); @@ -24,7 +24,7 @@ function insertBanner() { : actualHomePagePath + '/'; suggestionContainer.innerHTML = suggestedBaseUrl; } -`})})})}function B(){let{siteConfig:{baseUrl:e,baseUrlIssueBanner:t}}=(0,k.default)(),{pathname:n}=(0,d.zy)();return t&&n===e?(0,p.jsx)($,{}):null}function U(){let{siteConfig:{favicon:e,title:t,noIndex:n},i18n:{currentLocale:r,localeConfigs:a}}=(0,k.default)(),o=(0,w.Ay)(e),{htmlLang:i,direction:l}=a[r];return(0,p.jsxs)(v.A,{children:[(0,p.jsx)("html",{lang:i,dir:l}),(0,p.jsx)("title",{children:t}),(0,p.jsx)("meta",{property:"og:title",content:t}),(0,p.jsx)("meta",{name:"viewport",content:"width=device-width, initial-scale=1.0"}),n&&(0,p.jsx)("meta",{name:"robots",content:"noindex, nofollow"}),e&&(0,p.jsx)("link",{rel:"icon",href:o})]})}var H=n(349),Q=n(2303);function V(){let e=(0,Q.A)();return(0,p.jsx)(v.A,{children:(0,p.jsx)("html",{"data-has-hydrated":e})})}let W=(0,f.v)(c.A);function q(){let e=function(e){if(L.has(e.pathname))return{...e,pathname:L.get(e.pathname)};if((0,f.u)(c.A,e.pathname).some(({route:e})=>!0===e.exact))return L.set(e.pathname,e.pathname),e;let t=e.pathname.trim().replace(/(?:\/index)?\.html$/,"")||"/";return L.set(e.pathname,t),{...e,pathname:t}}((0,d.zy)());return(0,p.jsx)(z,{location:e,children:W})}var G=n(4054);let Y=!function(e){try{return document.createElement("link").relList.supports(e)}catch{return!1}}("prefetch")?function(e){return new Promise((t,n)=>{let r=new XMLHttpRequest;r.open("GET",e,!0),r.withCredentials=!0,r.onload=()=>{200===r.status?t():n()},r.send(null)})}:function(e){return new Promise((t,n)=>{if("u"t(),r.onerror=()=>n();let a=document.getElementsByTagName("head")[0]??document.getElementsByName("script")[0]?.parentNode;a?.appendChild(r)})};var K=n(6921);let Z=new Set,X=new Set,J=()=>navigator.connection?.effectiveType.includes("2g")||navigator.connection?.saveData,ee=Object.freeze({prefetch:e=>!(J()||X.has(e)||Z.has(e))&&(Z.add(e),Promise.all((0,f.u)(c.A,e).flatMap(e=>{let t;return t=e.route.path,Object.entries(G).filter(([e])=>e.replace(/-[^-]+$/,"")===t).flatMap(([,e])=>Object.values((0,K.A)(e)))}).map(e=>{let t=n.gca(e);return t&&!t.includes("undefined")?Y(t).catch(()=>{}):Promise.resolve()}))),preload:e=>!(J()||X.has(e))&&(X.add(e),D(e))});if(s.A.canUseDOM){window.docusaurus=ee;let e=document.getElementById("__docusaurus"),t=(0,p.jsx)(o.vd,{children:(0,p.jsx)(function({children:e}){return"hash"===l.default.future.experimental_router?(0,p.jsx)(i.I9,{children:e}):(0,p.jsx)(i.Kd,{children:e})},{children:(0,p.jsx)(function(){return(0,p.jsx)(H.A,{children:(0,p.jsx)(N.l,{children:(0,p.jsxs)(j.x,{children:[(0,p.jsx)(m,{children:(0,p.jsxs)(y,{children:[(0,p.jsx)(U,{}),(0,p.jsx)(O,{}),(0,p.jsx)(B,{}),(0,p.jsx)(q,{})]})}),(0,p.jsx)(V,{})]})})})},{})})}),n=(e,t)=>{console.error("Docusaurus React Root onRecoverableError:",e,t)},s=()=>{window.docusaurusRoot?window.docusaurusRoot.render(t):window.docusaurusRoot=a.hydrateRoot(e,t,{onRecoverableError:n})};D(window.location.pathname).then(()=>{(0,r.startTransition)(s)})}},2603(e,t,n){"use strict";n.d(t,{l:()=>f,o:()=>d});var r=n(6540),a=n(4784),o=JSON.parse('{"docusaurus-plugin-content-docs":{"default":{"path":"/multi-scrobbler/","versions":[{"name":"current","label":"Next","isLast":true,"path":"/multi-scrobbler/","mainDocId":"index","docs":[{"id":"configuration/clients/clients","path":"/multi-scrobbler/configuration/clients/","sidebar":"tutorialSidebar"},{"id":"configuration/clients/discord/discord","path":"/multi-scrobbler/configuration/clients/discord/","sidebar":"tutorialSidebar"},{"id":"configuration/clients/discord/discord-app","path":"/multi-scrobbler/configuration/clients/discord/discord-app","sidebar":"tutorialSidebar"},{"id":"configuration/clients/discord/discord-headless","path":"/multi-scrobbler/configuration/clients/discord/discord-headless","sidebar":"tutorialSidebar"},{"id":"configuration/clients/koito","path":"/multi-scrobbler/configuration/clients/koito","sidebar":"tutorialSidebar"},{"id":"configuration/clients/lastfm","path":"/multi-scrobbler/configuration/clients/lastfm","sidebar":"tutorialSidebar"},{"id":"configuration/clients/librefm","path":"/multi-scrobbler/configuration/clients/librefm","sidebar":"tutorialSidebar"},{"id":"configuration/clients/listenbrainz","path":"/multi-scrobbler/configuration/clients/listenbrainz","sidebar":"tutorialSidebar"},{"id":"configuration/clients/maloja","path":"/multi-scrobbler/configuration/clients/maloja","sidebar":"tutorialSidebar"},{"id":"configuration/clients/rocksky","path":"/multi-scrobbler/configuration/clients/rocksky","sidebar":"tutorialSidebar"},{"id":"configuration/clients/tealfm","path":"/multi-scrobbler/configuration/clients/tealfm","sidebar":"tutorialSidebar"},{"id":"configuration/configuration","path":"/multi-scrobbler/configuration/","sidebar":"tutorialSidebar"},{"id":"configuration/kitchensink","path":"/multi-scrobbler/configuration/kitchensink","sidebar":"tutorialSidebar"},{"id":"configuration/sources/azuracast","path":"/multi-scrobbler/configuration/sources/azuracast","sidebar":"tutorialSidebar"},{"id":"configuration/sources/deezer","path":"/multi-scrobbler/configuration/sources/deezer","sidebar":"tutorialSidebar"},{"id":"configuration/sources/google-cast","path":"/multi-scrobbler/configuration/sources/google-cast","sidebar":"tutorialSidebar"},{"id":"configuration/sources/icecast","path":"/multi-scrobbler/configuration/sources/icecast","sidebar":"tutorialSidebar"},{"id":"configuration/sources/jellyfin","path":"/multi-scrobbler/configuration/sources/jellyfin","sidebar":"tutorialSidebar"},{"id":"configuration/sources/jriver","path":"/multi-scrobbler/configuration/sources/jriver","sidebar":"tutorialSidebar"},{"id":"configuration/sources/kodi","path":"/multi-scrobbler/configuration/sources/kodi","sidebar":"tutorialSidebar"},{"id":"configuration/sources/koito-source","path":"/multi-scrobbler/configuration/sources/koito-source","sidebar":"tutorialSidebar"},{"id":"configuration/sources/lastfm-endpoint","path":"/multi-scrobbler/configuration/sources/lastfm-endpoint","sidebar":"tutorialSidebar"},{"id":"configuration/sources/lastfm-source","path":"/multi-scrobbler/configuration/sources/lastfm-source","sidebar":"tutorialSidebar"},{"id":"configuration/sources/librefm-source","path":"/multi-scrobbler/configuration/sources/librefm-source","sidebar":"tutorialSidebar"},{"id":"configuration/sources/listenbrainz-endpoint","path":"/multi-scrobbler/configuration/sources/listenbrainz-endpoint","sidebar":"tutorialSidebar"},{"id":"configuration/sources/listenbrainz-source","path":"/multi-scrobbler/configuration/sources/listenbrainz-source","sidebar":"tutorialSidebar"},{"id":"configuration/sources/maloja","path":"/multi-scrobbler/configuration/sources/maloja","sidebar":"tutorialSidebar"},{"id":"configuration/sources/mopidy","path":"/multi-scrobbler/configuration/sources/mopidy","sidebar":"tutorialSidebar"},{"id":"configuration/sources/mpd","path":"/multi-scrobbler/configuration/sources/mpd","sidebar":"tutorialSidebar"},{"id":"configuration/sources/mpris","path":"/multi-scrobbler/configuration/sources/mpris","sidebar":"tutorialSidebar"},{"id":"configuration/sources/musikcube","path":"/multi-scrobbler/configuration/sources/musikcube","sidebar":"tutorialSidebar"},{"id":"configuration/sources/plex","path":"/multi-scrobbler/configuration/sources/plex","sidebar":"tutorialSidebar"},{"id":"configuration/sources/rocksky-source","path":"/multi-scrobbler/configuration/sources/rocksky-source","sidebar":"tutorialSidebar"},{"id":"configuration/sources/sonos","path":"/multi-scrobbler/configuration/sources/sonos","sidebar":"tutorialSidebar"},{"id":"configuration/sources/sources","path":"/multi-scrobbler/configuration/sources/","sidebar":"tutorialSidebar"},{"id":"configuration/sources/spotify","path":"/multi-scrobbler/configuration/sources/spotify","sidebar":"tutorialSidebar"},{"id":"configuration/sources/subsonic","path":"/multi-scrobbler/configuration/sources/subsonic","sidebar":"tutorialSidebar"},{"id":"configuration/sources/tealfm-source","path":"/multi-scrobbler/configuration/sources/tealfm-source","sidebar":"tutorialSidebar"},{"id":"configuration/sources/vlc","path":"/multi-scrobbler/configuration/sources/vlc","sidebar":"tutorialSidebar"},{"id":"configuration/sources/webscrobbler","path":"/multi-scrobbler/configuration/sources/webscrobbler","sidebar":"tutorialSidebar"},{"id":"configuration/sources/yamaha-musiccast","path":"/multi-scrobbler/configuration/sources/yamaha-musiccast","sidebar":"tutorialSidebar"},{"id":"configuration/sources/yandex-music","path":"/multi-scrobbler/configuration/sources/yandex-music","sidebar":"tutorialSidebar"},{"id":"configuration/sources/youtube-music","path":"/multi-scrobbler/configuration/sources/youtube-music","sidebar":"tutorialSidebar"},{"id":"configuration/transforms/musicbrainz","path":"/multi-scrobbler/configuration/transforms/musicbrainz","sidebar":"tutorialSidebar"},{"id":"configuration/transforms/native","path":"/multi-scrobbler/configuration/transforms/native","sidebar":"tutorialSidebar"},{"id":"configuration/transforms/transforms","path":"/multi-scrobbler/configuration/transforms/","sidebar":"tutorialSidebar"},{"id":"configuration/transforms/user","path":"/multi-scrobbler/configuration/transforms/user","sidebar":"tutorialSidebar"},{"id":"development/dev-client","path":"/multi-scrobbler/development/dev-client","sidebar":"tutorialSidebar"},{"id":"development/dev-common","path":"/multi-scrobbler/development/dev-common","sidebar":"tutorialSidebar"},{"id":"development/dev-source","path":"/multi-scrobbler/development/dev-source","sidebar":"tutorialSidebar"},{"id":"development/flatpak","path":"/multi-scrobbler/development/flatpak","sidebar":"tutorialSidebar"},{"id":"FAQ","path":"/multi-scrobbler/FAQ","sidebar":"tutorialSidebar"},{"id":"help","path":"/multi-scrobbler/help","sidebar":"tutorialSidebar"},{"id":"index","path":"/multi-scrobbler/","sidebar":"tutorialSidebar"},{"id":"installation/installation","path":"/multi-scrobbler/installation/","sidebar":"tutorialSidebar"},{"id":"installation/service","path":"/multi-scrobbler/installation/service","sidebar":"tutorialSidebar"},{"id":"quickstart","path":"/multi-scrobbler/quickstart","sidebar":"tutorialSidebar"},{"id":"updating/updating","path":"/multi-scrobbler/updating/","sidebar":"tutorialSidebar"},{"id":"/category/updating","path":"/multi-scrobbler/category/updating","sidebar":"tutorialSidebar"},{"id":"/category/development","path":"/multi-scrobbler/category/development","sidebar":"tutorialSidebar"}],"draftIds":[],"sidebars":{"tutorialSidebar":{"link":{"path":"/multi-scrobbler/","label":"index"}}}}],"breadcrumbs":true}}}'),i=JSON.parse('{"defaultLocale":"en","locales":["en"],"path":"i18n","currentLocale":"en","localeConfigs":{"en":{"label":"English","direction":"ltr","htmlLang":"en","calendar":"gregory","path":"en","translate":false,"url":"https://foxxmd.github.io","baseUrl":"/multi-scrobbler/"}}}'),l=n(2654),s=JSON.parse('{"docusaurusVersion":"3.9.2","siteVersion":"0.12.1","pluginVersions":{"docusaurus-plugin-content-docs":{"type":"package","name":"@docusaurus/plugin-content-docs","version":"3.9.2"},"docusaurus-plugin-content-pages":{"type":"package","name":"@docusaurus/plugin-content-pages","version":"3.9.2"},"docusaurus-plugin-sitemap":{"type":"package","name":"@docusaurus/plugin-sitemap","version":"3.9.2"},"docusaurus-plugin-svgr":{"type":"package","name":"@docusaurus/plugin-svgr","version":"3.9.2"},"docusaurus-theme-classic":{"type":"package","name":"@docusaurus/theme-classic","version":"3.9.2"},"docusaurus-plugin-sass":{"type":"package","name":"docusaurus-plugin-sass","version":"0.2.6"},"docusaurus-plugin-client-redirects":{"type":"package","name":"@docusaurus/plugin-client-redirects","version":"3.9.2"},"@easyops-cn/docusaurus-search-local":{"type":"package","name":"@easyops-cn/docusaurus-search-local","version":"0.51.1"},"docusaurus-theme-github-codeblock":{"type":"package"}}}'),u=n(4848);let c={siteConfig:a.default,siteMetadata:s,globalData:o,i18n:i,codeTranslations:l},d=r.createContext(c);function f({children:e}){return(0,u.jsx)(d.Provider,{value:c,children:e})}},349(e,t,n){"use strict";n.d(t,{A:()=>m});var r=n(6540),a=n(8193),o=n(5260),i=n(440),l=n(195),s=n(3102),u=n(4848);function c({error:e,tryAgain:t}){return(0,u.jsxs)("div",{style:{display:"flex",flexDirection:"column",justifyContent:"center",alignItems:"flex-start",minHeight:"100vh",width:"100%",maxWidth:"80ch",fontSize:"20px",margin:"0 auto",padding:"1rem"},children:[(0,u.jsx)("h1",{style:{fontSize:"3rem"},children:"This page crashed"}),(0,u.jsx)("button",{type:"button",onClick:t,style:{margin:"1rem 0",fontSize:"2rem",cursor:"pointer",borderRadius:20,padding:"1rem"},children:"Try again"}),(0,u.jsx)(d,{error:e})]})}function d({error:e}){let t=(0,i.getErrorCausalChain)(e).map(e=>e.message).join("\n\nCause:\n");return(0,u.jsx)("p",{style:{whiteSpace:"pre-wrap"},children:t})}function f({children:e}){return(0,u.jsx)(s.W,{value:{plugin:{name:"docusaurus-core-error-boundary",id:"default"}},children:e})}function p({error:e,tryAgain:t}){return(0,u.jsx)(f,{children:(0,u.jsxs)(m,{fallback:()=>(0,u.jsx)(c,{error:e,tryAgain:t}),children:[(0,u.jsx)(o.A,{children:(0,u.jsx)("title",{children:"Page Error"})}),(0,u.jsx)(l.A,{children:(0,u.jsx)(c,{error:e,tryAgain:t})})]})})}class m extends r.Component{constructor(e){super(e),this.state={error:null}}componentDidCatch(e){a.A.canUseDOM&&this.setState({error:e})}render(){let{children:e}=this.props,{error:t}=this.state;return t?(this.props.fallback??(e=>(0,u.jsx)(p,{...e})))({error:t,tryAgain:()=>this.setState({error:null})}):e??null}}},8193(e,t,n){"use strict";n.d(t,{A:()=>a});let r="u">typeof window&&"document"in window&&"createElement"in window.document,a={canUseDOM:r,canUseEventListeners:r&&("addEventListener"in window||"attachEvent"in window),canUseIntersectionObserver:r&&"IntersectionObserver"in window,canUseViewport:r&&"screen"in window}},5260(e,t,n){"use strict";n.d(t,{A:()=>o}),n(6540);var r=n(545),a=n(4848);function o(e){return(0,a.jsx)(r.mg,{...e})}},8774(e,t,n){"use strict";n.d(t,{A:()=>f});var r=n(6540),a=n(4625),o=n(440),i=n(4586),l=n(6654),s=n(8193),u=n(7988),c=n(6025),d=n(4848);let f=r.forwardRef(function({isNavLink:e,to:t,href:n,activeClassName:f,isActive:p,"data-noBrokenLinkCheck":m,autoAddBaseUrl:h=!0,...g},b){var y;let{siteConfig:v}=(0,i.default)(),{trailingSlash:k,baseUrl:w}=v,x=v.future.experimental_router,{withBaseUrl:S}=(0,c.hH)(),E=(0,u.A)(),_=(0,r.useRef)(null);(0,r.useImperativeHandle)(b,()=>_.current);let A=t||n,C=(0,l.A)(A),T=A?.replace("pathname://",""),P=void 0!==T?(y=T,h&&y.startsWith("/")?S(y):y):void 0;"hash"===x&&P?.startsWith("./")&&(P=P?.slice(1)),P&&C&&(P=(0,o.applyTrailingSlash)(P,{trailingSlash:k,baseUrl:w}));let O=(0,r.useRef)(!1),L=e?a.k2:a.N_,j=s.A.canUseIntersectionObserver,N=(0,r.useRef)(),R=()=>{O.current||null==P||(window.docusaurus.preload(P),O.current=!0)};(0,r.useEffect)(()=>(!j&&C&&s.A.canUseDOM&&null!=P&&window.docusaurus.prefetch(P),()=>{j&&N.current&&N.current.disconnect()}),[N,P,j,C]);let I=P?.startsWith("#")??!1,F=!g.target||"_self"===g.target,D=!P||!C||!F||I&&"hash"!==x;m||!I&&D||E.collectLink(P),g.id&&E.collectAnchor(g.id);let z={};return D?(0,d.jsx)("a",{ref:_,href:P,...A&&!C&&{target:"_blank",rel:"noopener noreferrer"},...g,...z}):(0,d.jsx)(L,{...g,onMouseEnter:R,onTouchStart:R,innerRef:e=>{_.current=e,j&&e&&C&&(N.current=new window.IntersectionObserver(t=>{t.forEach(t=>{e===t.target&&(t.isIntersecting||t.intersectionRatio>0)&&(N.current.unobserve(e),N.current.disconnect(),null!=P&&window.docusaurus.prefetch(P))})}),N.current.observe(e))},to:P,...e&&{isActive:p,activeClassName:f},...z})})},7856(e,t,n){"use strict";n.d(t,{A:()=>u,T:()=>s});var r=n(6540),a=n(4848);function o(e,t){let n=e.split(/(\{\w+\})/).map((e,n)=>{if(n%2==1){let n=t?.[e.slice(1,-1)];if(void 0!==n)return n}return e});return n.some(e=>(0,r.isValidElement)(e))?n.map((e,t)=>(0,r.isValidElement)(e)?r.cloneElement(e,{key:t}):e).filter(e=>""!==e):n.join("")}var i=n(2654);function l({id:e,message:t}){if(void 0===e&&void 0===t)throw Error("Docusaurus translation declarations must have at least a translation id or a default translation message");return i[e??t]??t??e}function s({message:e,id:t},n){return o(l({message:e,id:t}),n)}function u({children:e,id:t,values:n}){if(e&&"string"!=typeof e)throw console.warn("Illegal children",e),Error("The Docusaurus component only accept simple string values");let r=l({message:e,id:t});return(0,a.jsx)(a.Fragment,{children:o(r,n)})}},7065(e,t,n){"use strict";n.d(t,{W:()=>r});let r="default"},6654(e,t,n){"use strict";function r(e){return/^(?:\w*:|\/\/)/.test(e)}function a(e){return void 0!==e&&!r(e)}n.d(t,{A:()=>a,z:()=>r})},6025(e,t,n){"use strict";n.d(t,{Ay:()=>l,hH:()=>i});var r=n(6540),a=n(4586),o=n(6654);function i(){let{siteConfig:e}=(0,a.default)(),{baseUrl:t,url:n}=e,i=e.future.experimental_router;return{withBaseUrl:(0,r.useCallback)((e,r)=>(function({siteUrl:e,baseUrl:t,url:n,options:{forcePrependBaseUrl:r=!1,absolute:a=!1}={},router:i}){if(!n||n.startsWith("#")||(0,o.z)(n))return n;if("hash"===i)return n.startsWith("/")?`.${n}`:`./${n}`;if(r)return t+n.replace(/^\//,"");if(n===t.replace(/\/$/,""))return t;let l=n.startsWith(t)?n:t+n.replace(/^\//,"");return a?e+l:l})({siteUrl:n,baseUrl:t,url:e,options:r,router:i}),[n,t,i])}}function l(e,t={}){let{withBaseUrl:n}=i();return n(e,t)}},7988(e,t,n){"use strict";n.d(t,{A:()=>o});var r=n(6540);n(4848);let a=r.createContext({collectAnchor:()=>{},collectLink:()=>{}});function o(){return(0,r.useContext)(a)}},4586(e,t,n){"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(6540),a=n(2603);function o(){return(0,r.useContext)(a.o)}},2303(e,t,n){"use strict";n.d(t,{A:()=>o});var r=n(6540),a=n(6125);function o(){return(0,r.useContext)(a.o)}},205(e,t,n){"use strict";n.d(t,{A:()=>a});var r=n(6540);let a=n(8193).A.canUseDOM?r.useLayoutEffect:r.useEffect},6803(e,t,n){"use strict";n.d(t,{A:()=>o});var r=n(6540),a=n(3102);function o(){let e=r.useContext(a.o);if(!e)throw Error("Unexpected: no Docusaurus route context found");return e}},6921(e,t,n){"use strict";function r(e){let t={};return!function e(n,r){Object.entries(n).forEach(([n,a])=>{let o=r?`${r}.${n}`:n;"object"==typeof a&&a&&Object.keys(a).length>0?e(a,o):t[o]=a})}(e),t}n.d(t,{A:()=>r})},3102(e,t,n){"use strict";n.d(t,{W:()=>i,o:()=>o});var r=n(6540),a=n(4848);let o=r.createContext(null);function i({children:e,value:t}){let n=r.useContext(o),l=(0,r.useMemo)(()=>(function({parent:e,value:t}){if(!e){if(t){if(!("plugin"in t))throw Error("Unexpected: Docusaurus topmost route context has no `plugin` attribute")}else throw Error("Unexpected: no Docusaurus route context found");return t}let n={...e.data,...t?.data};return{plugin:e.plugin,data:n}})({parent:n,value:t}),[n,t]);return(0,a.jsx)(o.Provider,{value:l,children:e})}},3886(e,t,n){"use strict";n.d(t,{VQ:()=>p,XK:()=>g,g1:()=>h});var r=n(6540),a=n(9410),o=n(7065),i=n(6342),l=n(6447),s=n(9532),u=n(4848);let c=e=>`docs-preferred-version-${e}`,d=r.createContext(null);function f({children:e}){let t=function(){let e=(0,a.Gy)(),t=(0,i.p)().docs.versionPersistence,n=(0,r.useMemo)(()=>Object.keys(e),[e]),[o,s]=(0,r.useState)(()=>Object.fromEntries(n.map(e=>[e,{preferredVersionName:null}])));return(0,r.useEffect)(()=>{s(function({pluginIds:e,versionPersistence:t,allDocsData:n}){return Object.fromEntries(e.map(e=>{var r,a;let o,i,s;return[e,(i=e,s=t,o=(0,l.Wf)(c(i),{persistence:s}).get(),n[e].versions.some(e=>e.name===o)?{preferredVersionName:o}:(r=e,a=t,(0,l.Wf)(c(r),{persistence:a}).del(),{preferredVersionName:null}))]}))}({allDocsData:e,versionPersistence:t,pluginIds:n}))},[e,t,n]),[o,(0,r.useMemo)(()=>({savePreferredVersion:function(e,n){var r,a,o;r=e,a=t,o=n,(0,l.Wf)(c(r),{persistence:a}).set(o),s(t=>({...t,[e]:{preferredVersionName:n}}))}}),[t])]}();return(0,u.jsx)(d.Provider,{value:t,children:e})}function p({children:e}){return(0,u.jsx)(f,{children:e})}function m(){let e=(0,r.useContext)(d);if(!e)throw new s.dV("DocsPreferredVersionContextProvider");return e}function h(e=o.W){let t=(0,a.ht)(e),[n,i]=m(),{preferredVersionName:l}=n[e];return{preferredVersion:t.versions.find(e=>e.name===l)??null,savePreferredVersionName:(0,r.useCallback)(t=>{i.savePreferredVersion(e,t)},[i,e])}}function g(){let e=(0,a.Gy)(),[t]=m();return Object.fromEntries(Object.keys(e).map(n=>[n,function(n){let r=e[n],{preferredVersionName:a}=t[n];return r.versions.find(e=>e.name===a)??null}(n)]))}},2565(e,t,n){"use strict";n.d(t,{k:()=>o,v:()=>i});var r=n(9410),a=n(3886);function o(e,t){return`docs-${e}-${t}`}function i(){let e=(0,r.Gy)(),t=(0,r.gk)(),n=(0,a.XK)();return[...Object.keys(e).map(function(r){let a=t?.activePlugin.pluginId===r?t.activeVersion:void 0,i=n[r],l=e[r].versions.find(e=>e.isLast);return o(r,(a??i??l).name)})]}},609(e,t,n){"use strict";n.d(t,{V:()=>s,t:()=>u});var r=n(6540),a=n(9532),o=n(4848);let i=Symbol("EmptyContext"),l=r.createContext(i);function s({children:e,name:t,items:n}){let a=(0,r.useMemo)(()=>t&&n?{name:t,items:n}:null,[t,n]);return(0,o.jsx)(l.Provider,{value:a,children:e})}function u(){let e=(0,r.useContext)(l);if(e===i)throw new a.dV("DocsSidebarProvider");return e}},6972(e,t,n){"use strict";n.d(t,{$S:()=>m,B5:()=>E,Nr:()=>p,OF:()=>k,QB:()=>S,Vd:()=>w,Y:()=>y,a4:()=>h,cC:()=>f,d1:()=>_,fW:()=>x,w8:()=>b});var r=n(6540),a=n(6347),o=n(2831),i=n(9410),l=n(9169),s=n(1682),u=n(3886),c=n(3025),d=n(609);function f(e){let t=(0,c.r)();if(!e)return;let n=t.docs[e];if(!n)throw Error(`no version doc found by id=${e}`);return n}function p(e){if("link"===e.type&&!e.unlisted)return e.href;if("category"===e.type){if(e.href&&!e.linkUnlisted)return e.href;for(let t of e.items){let e=p(t);if(e)return e}return}}function m(){let{pathname:e}=(0,a.zy)(),t=(0,d.t)();if(!t)throw Error("Unexpected: cant find current sidebar in context");let n=v({sidebarItems:t.items,pathname:e,onlyCategories:!0}).slice(-1)[0];if(!n)throw Error(`${e} is not associated with a category. useCurrentSidebarCategory() should only be used on category index pages.`);return n}function h(){let{pathname:e}=(0,a.zy)(),t=(0,d.t)();if(!t)throw Error("Unexpected: cant find current sidebar in context");let n=v({sidebarItems:t.items,pathname:e,onlyCategories:!0}).slice(-1)[0];return n?.items??t.items}let g=(e,t)=>void 0!==e&&(0,l.ys)(e,t);function b(e,t){if("link"===e.type)return g(e.href,t);if("category"===e.type){let n;return g(e.href,t)||(n=e.items,n.some(e=>b(e,t)))}return!1}function y(e,t){return(0,r.useMemo)(()=>e.filter(e=>(function e(t,n){switch(t.type){case"category":return b(t,n)||void 0!==t.href&&!t.linkUnlisted||t.items.some(t=>e(t,n));case"link":return!t.unlisted||b(t,n);default:return!0}})(e,t)),[e,t])}function v({sidebarItems:e,pathname:t,onlyCategories:n=!1}){let r=[];return!function e(a){for(let o of a)if("category"===o.type&&((0,l.ys)(o.href,t)||e(o.items))||"link"===o.type&&(0,l.ys)(o.href,t))return n&&"category"!==o.type||r.unshift(o),!0;return!1}(e),r}function k(){let e=(0,d.t)(),{pathname:t}=(0,a.zy)();return!1!==(0,i.vT)()?.pluginData.breadcrumbs&&e?v({sidebarItems:e.items,pathname:t}):null}function w(e){let{activeVersion:t}=(0,i.zK)(e),{preferredVersion:n}=(0,u.g1)(e),a=(0,i.r7)(e);return(0,r.useMemo)(()=>(0,s.sb)([t,n,a].filter(Boolean)),[t,n,a])}function x(e,t){let n=w(t);return(0,r.useMemo)(()=>{let t=n.flatMap(e=>e.sidebars?Object.entries(e.sidebars):[]),r=t.find(t=>t[0]===e);if(!r)throw Error(`Can't find any sidebar with id "${e}" in version${n.length>1?"s":""} ${n.map(e=>e.name).join(", ")}". +`})})})}function B(){let{siteConfig:{baseUrl:e,baseUrlIssueBanner:t}}=(0,k.default)(),{pathname:n}=(0,d.zy)();return t&&n===e?(0,p.jsx)($,{}):null}function U(){let{siteConfig:{favicon:e,title:t,noIndex:n},i18n:{currentLocale:r,localeConfigs:a}}=(0,k.default)(),o=(0,w.Ay)(e),{htmlLang:i,direction:l}=a[r];return(0,p.jsxs)(v.A,{children:[(0,p.jsx)("html",{lang:i,dir:l}),(0,p.jsx)("title",{children:t}),(0,p.jsx)("meta",{property:"og:title",content:t}),(0,p.jsx)("meta",{name:"viewport",content:"width=device-width, initial-scale=1.0"}),n&&(0,p.jsx)("meta",{name:"robots",content:"noindex, nofollow"}),e&&(0,p.jsx)("link",{rel:"icon",href:o})]})}var H=n(349),Q=n(2303);function V(){let e=(0,Q.A)();return(0,p.jsx)(v.A,{children:(0,p.jsx)("html",{"data-has-hydrated":e})})}let W=(0,f.v)(c.A);function q(){let e=function(e){if(L.has(e.pathname))return{...e,pathname:L.get(e.pathname)};if((0,f.u)(c.A,e.pathname).some(({route:e})=>!0===e.exact))return L.set(e.pathname,e.pathname),e;let t=e.pathname.trim().replace(/(?:\/index)?\.html$/,"")||"/";return L.set(e.pathname,t),{...e,pathname:t}}((0,d.zy)());return(0,p.jsx)(z,{location:e,children:W})}var G=n(4054);let Y=!function(e){try{return document.createElement("link").relList.supports(e)}catch{return!1}}("prefetch")?function(e){return new Promise((t,n)=>{let r=new XMLHttpRequest;r.open("GET",e,!0),r.withCredentials=!0,r.onload=()=>{200===r.status?t():n()},r.send(null)})}:function(e){return new Promise((t,n)=>{if("u"t(),r.onerror=()=>n();let a=document.getElementsByTagName("head")[0]??document.getElementsByName("script")[0]?.parentNode;a?.appendChild(r)})};var K=n(6921);let Z=new Set,X=new Set,J=()=>navigator.connection?.effectiveType.includes("2g")||navigator.connection?.saveData,ee=Object.freeze({prefetch:e=>!(J()||X.has(e)||Z.has(e))&&(Z.add(e),Promise.all((0,f.u)(c.A,e).flatMap(e=>{let t;return t=e.route.path,Object.entries(G).filter(([e])=>e.replace(/-[^-]+$/,"")===t).flatMap(([,e])=>Object.values((0,K.A)(e)))}).map(e=>{let t=n.gca(e);return t&&!t.includes("undefined")?Y(t).catch(()=>{}):Promise.resolve()}))),preload:e=>!(J()||X.has(e))&&(X.add(e),D(e))});if(s.A.canUseDOM){window.docusaurus=ee;let e=document.getElementById("__docusaurus"),t=(0,p.jsx)(o.vd,{children:(0,p.jsx)(function({children:e}){return"hash"===l.default.future.experimental_router?(0,p.jsx)(i.I9,{children:e}):(0,p.jsx)(i.Kd,{children:e})},{children:(0,p.jsx)(function(){return(0,p.jsx)(H.A,{children:(0,p.jsx)(N.l,{children:(0,p.jsxs)(j.x,{children:[(0,p.jsx)(m,{children:(0,p.jsxs)(y,{children:[(0,p.jsx)(U,{}),(0,p.jsx)(O,{}),(0,p.jsx)(B,{}),(0,p.jsx)(q,{})]})}),(0,p.jsx)(V,{})]})})})},{})})}),n=(e,t)=>{console.error("Docusaurus React Root onRecoverableError:",e,t)},s=()=>{window.docusaurusRoot?window.docusaurusRoot.render(t):window.docusaurusRoot=a.hydrateRoot(e,t,{onRecoverableError:n})};D(window.location.pathname).then(()=>{(0,r.startTransition)(s)})}},2603(e,t,n){"use strict";n.d(t,{l:()=>f,o:()=>d});var r=n(6540),a=n(4784),o=JSON.parse('{"docusaurus-plugin-content-docs":{"default":{"path":"/multi-scrobbler/","versions":[{"name":"current","label":"Next","isLast":true,"path":"/multi-scrobbler/","mainDocId":"index","docs":[{"id":"configuration/clients/clients","path":"/multi-scrobbler/configuration/clients/","sidebar":"tutorialSidebar"},{"id":"configuration/clients/discord/discord","path":"/multi-scrobbler/configuration/clients/discord/","sidebar":"tutorialSidebar"},{"id":"configuration/clients/discord/discord-app","path":"/multi-scrobbler/configuration/clients/discord/discord-app","sidebar":"tutorialSidebar"},{"id":"configuration/clients/discord/discord-headless","path":"/multi-scrobbler/configuration/clients/discord/discord-headless","sidebar":"tutorialSidebar"},{"id":"configuration/clients/koito","path":"/multi-scrobbler/configuration/clients/koito","sidebar":"tutorialSidebar"},{"id":"configuration/clients/lastfm","path":"/multi-scrobbler/configuration/clients/lastfm","sidebar":"tutorialSidebar"},{"id":"configuration/clients/librefm","path":"/multi-scrobbler/configuration/clients/librefm","sidebar":"tutorialSidebar"},{"id":"configuration/clients/listenbrainz","path":"/multi-scrobbler/configuration/clients/listenbrainz","sidebar":"tutorialSidebar"},{"id":"configuration/clients/maloja","path":"/multi-scrobbler/configuration/clients/maloja","sidebar":"tutorialSidebar"},{"id":"configuration/clients/rocksky","path":"/multi-scrobbler/configuration/clients/rocksky","sidebar":"tutorialSidebar"},{"id":"configuration/clients/tealfm","path":"/multi-scrobbler/configuration/clients/tealfm","sidebar":"tutorialSidebar"},{"id":"configuration/configuration","path":"/multi-scrobbler/configuration/","sidebar":"tutorialSidebar"},{"id":"configuration/kitchensink","path":"/multi-scrobbler/configuration/kitchensink","sidebar":"tutorialSidebar"},{"id":"configuration/sources/azuracast","path":"/multi-scrobbler/configuration/sources/azuracast","sidebar":"tutorialSidebar"},{"id":"configuration/sources/deezer","path":"/multi-scrobbler/configuration/sources/deezer","sidebar":"tutorialSidebar"},{"id":"configuration/sources/google-cast","path":"/multi-scrobbler/configuration/sources/google-cast","sidebar":"tutorialSidebar"},{"id":"configuration/sources/icecast","path":"/multi-scrobbler/configuration/sources/icecast","sidebar":"tutorialSidebar"},{"id":"configuration/sources/jellyfin","path":"/multi-scrobbler/configuration/sources/jellyfin","sidebar":"tutorialSidebar"},{"id":"configuration/sources/jriver","path":"/multi-scrobbler/configuration/sources/jriver","sidebar":"tutorialSidebar"},{"id":"configuration/sources/kodi","path":"/multi-scrobbler/configuration/sources/kodi","sidebar":"tutorialSidebar"},{"id":"configuration/sources/koito-source","path":"/multi-scrobbler/configuration/sources/koito-source","sidebar":"tutorialSidebar"},{"id":"configuration/sources/lastfm-endpoint","path":"/multi-scrobbler/configuration/sources/lastfm-endpoint","sidebar":"tutorialSidebar"},{"id":"configuration/sources/lastfm-source","path":"/multi-scrobbler/configuration/sources/lastfm-source","sidebar":"tutorialSidebar"},{"id":"configuration/sources/librefm-source","path":"/multi-scrobbler/configuration/sources/librefm-source","sidebar":"tutorialSidebar"},{"id":"configuration/sources/listenbrainz-endpoint","path":"/multi-scrobbler/configuration/sources/listenbrainz-endpoint","sidebar":"tutorialSidebar"},{"id":"configuration/sources/listenbrainz-source","path":"/multi-scrobbler/configuration/sources/listenbrainz-source","sidebar":"tutorialSidebar"},{"id":"configuration/sources/maloja","path":"/multi-scrobbler/configuration/sources/maloja","sidebar":"tutorialSidebar"},{"id":"configuration/sources/mopidy","path":"/multi-scrobbler/configuration/sources/mopidy","sidebar":"tutorialSidebar"},{"id":"configuration/sources/mpd","path":"/multi-scrobbler/configuration/sources/mpd","sidebar":"tutorialSidebar"},{"id":"configuration/sources/mpris","path":"/multi-scrobbler/configuration/sources/mpris","sidebar":"tutorialSidebar"},{"id":"configuration/sources/musikcube","path":"/multi-scrobbler/configuration/sources/musikcube","sidebar":"tutorialSidebar"},{"id":"configuration/sources/plex","path":"/multi-scrobbler/configuration/sources/plex","sidebar":"tutorialSidebar"},{"id":"configuration/sources/rocksky-source","path":"/multi-scrobbler/configuration/sources/rocksky-source","sidebar":"tutorialSidebar"},{"id":"configuration/sources/sonos","path":"/multi-scrobbler/configuration/sources/sonos","sidebar":"tutorialSidebar"},{"id":"configuration/sources/sources","path":"/multi-scrobbler/configuration/sources/","sidebar":"tutorialSidebar"},{"id":"configuration/sources/spotify","path":"/multi-scrobbler/configuration/sources/spotify","sidebar":"tutorialSidebar"},{"id":"configuration/sources/subsonic","path":"/multi-scrobbler/configuration/sources/subsonic","sidebar":"tutorialSidebar"},{"id":"configuration/sources/tealfm-source","path":"/multi-scrobbler/configuration/sources/tealfm-source","sidebar":"tutorialSidebar"},{"id":"configuration/sources/vlc","path":"/multi-scrobbler/configuration/sources/vlc","sidebar":"tutorialSidebar"},{"id":"configuration/sources/webscrobbler","path":"/multi-scrobbler/configuration/sources/webscrobbler","sidebar":"tutorialSidebar"},{"id":"configuration/sources/yamaha-musiccast","path":"/multi-scrobbler/configuration/sources/yamaha-musiccast","sidebar":"tutorialSidebar"},{"id":"configuration/sources/yandex-music","path":"/multi-scrobbler/configuration/sources/yandex-music","sidebar":"tutorialSidebar"},{"id":"configuration/sources/youtube-music","path":"/multi-scrobbler/configuration/sources/youtube-music","sidebar":"tutorialSidebar"},{"id":"configuration/transforms/musicbrainz","path":"/multi-scrobbler/configuration/transforms/musicbrainz","sidebar":"tutorialSidebar"},{"id":"configuration/transforms/native","path":"/multi-scrobbler/configuration/transforms/native","sidebar":"tutorialSidebar"},{"id":"configuration/transforms/transforms","path":"/multi-scrobbler/configuration/transforms/","sidebar":"tutorialSidebar"},{"id":"configuration/transforms/user","path":"/multi-scrobbler/configuration/transforms/user","sidebar":"tutorialSidebar"},{"id":"development/dev-common","path":"/multi-scrobbler/development/dev-common","sidebar":"tutorialSidebar"},{"id":"development/dev-source","path":"/multi-scrobbler/development/dev-source","sidebar":"tutorialSidebar"},{"id":"development/flatpak","path":"/multi-scrobbler/development/flatpak","sidebar":"tutorialSidebar"},{"id":"FAQ","path":"/multi-scrobbler/FAQ","sidebar":"tutorialSidebar"},{"id":"help","path":"/multi-scrobbler/help","sidebar":"tutorialSidebar"},{"id":"index","path":"/multi-scrobbler/","sidebar":"tutorialSidebar"},{"id":"installation/installation","path":"/multi-scrobbler/installation/","sidebar":"tutorialSidebar"},{"id":"installation/service","path":"/multi-scrobbler/installation/service","sidebar":"tutorialSidebar"},{"id":"quickstart","path":"/multi-scrobbler/quickstart","sidebar":"tutorialSidebar"},{"id":"updating/updating","path":"/multi-scrobbler/updating/","sidebar":"tutorialSidebar"},{"id":"/category/updating","path":"/multi-scrobbler/category/updating","sidebar":"tutorialSidebar"},{"id":"/category/development","path":"/multi-scrobbler/category/development","sidebar":"tutorialSidebar"}],"draftIds":[],"sidebars":{"tutorialSidebar":{"link":{"path":"/multi-scrobbler/","label":"index"}}}}],"breadcrumbs":true}}}'),i=JSON.parse('{"defaultLocale":"en","locales":["en"],"path":"i18n","currentLocale":"en","localeConfigs":{"en":{"label":"English","direction":"ltr","htmlLang":"en","calendar":"gregory","path":"en","translate":false,"url":"https://foxxmd.github.io","baseUrl":"/multi-scrobbler/"}}}'),l=n(2654),s=JSON.parse('{"docusaurusVersion":"3.9.2","siteVersion":"0.12.1","pluginVersions":{"docusaurus-plugin-content-docs":{"type":"package","name":"@docusaurus/plugin-content-docs","version":"3.9.2"},"docusaurus-plugin-content-pages":{"type":"package","name":"@docusaurus/plugin-content-pages","version":"3.9.2"},"docusaurus-plugin-sitemap":{"type":"package","name":"@docusaurus/plugin-sitemap","version":"3.9.2"},"docusaurus-plugin-svgr":{"type":"package","name":"@docusaurus/plugin-svgr","version":"3.9.2"},"docusaurus-theme-classic":{"type":"package","name":"@docusaurus/theme-classic","version":"3.9.2"},"docusaurus-plugin-sass":{"type":"package","name":"docusaurus-plugin-sass","version":"0.2.6"},"docusaurus-plugin-client-redirects":{"type":"package","name":"@docusaurus/plugin-client-redirects","version":"3.9.2"},"docusaurus-og":{"type":"package","name":"@bony_chops/docusaurus-og","version":"1.0.3-beta.3"},"@easyops-cn/docusaurus-search-local":{"type":"package","name":"@easyops-cn/docusaurus-search-local","version":"0.51.1"},"docusaurus-theme-github-codeblock":{"type":"package"}}}'),u=n(4848);let c={siteConfig:a.default,siteMetadata:s,globalData:o,i18n:i,codeTranslations:l},d=r.createContext(c);function f({children:e}){return(0,u.jsx)(d.Provider,{value:c,children:e})}},349(e,t,n){"use strict";n.d(t,{A:()=>m});var r=n(6540),a=n(8193),o=n(5260),i=n(440),l=n(195),s=n(3102),u=n(4848);function c({error:e,tryAgain:t}){return(0,u.jsxs)("div",{style:{display:"flex",flexDirection:"column",justifyContent:"center",alignItems:"flex-start",minHeight:"100vh",width:"100%",maxWidth:"80ch",fontSize:"20px",margin:"0 auto",padding:"1rem"},children:[(0,u.jsx)("h1",{style:{fontSize:"3rem"},children:"This page crashed"}),(0,u.jsx)("button",{type:"button",onClick:t,style:{margin:"1rem 0",fontSize:"2rem",cursor:"pointer",borderRadius:20,padding:"1rem"},children:"Try again"}),(0,u.jsx)(d,{error:e})]})}function d({error:e}){let t=(0,i.getErrorCausalChain)(e).map(e=>e.message).join("\n\nCause:\n");return(0,u.jsx)("p",{style:{whiteSpace:"pre-wrap"},children:t})}function f({children:e}){return(0,u.jsx)(s.W,{value:{plugin:{name:"docusaurus-core-error-boundary",id:"default"}},children:e})}function p({error:e,tryAgain:t}){return(0,u.jsx)(f,{children:(0,u.jsxs)(m,{fallback:()=>(0,u.jsx)(c,{error:e,tryAgain:t}),children:[(0,u.jsx)(o.A,{children:(0,u.jsx)("title",{children:"Page Error"})}),(0,u.jsx)(l.A,{children:(0,u.jsx)(c,{error:e,tryAgain:t})})]})})}class m extends r.Component{constructor(e){super(e),this.state={error:null}}componentDidCatch(e){a.A.canUseDOM&&this.setState({error:e})}render(){let{children:e}=this.props,{error:t}=this.state;return t?(this.props.fallback??(e=>(0,u.jsx)(p,{...e})))({error:t,tryAgain:()=>this.setState({error:null})}):e??null}}},8193(e,t,n){"use strict";n.d(t,{A:()=>a});let r="u">typeof window&&"document"in window&&"createElement"in window.document,a={canUseDOM:r,canUseEventListeners:r&&("addEventListener"in window||"attachEvent"in window),canUseIntersectionObserver:r&&"IntersectionObserver"in window,canUseViewport:r&&"screen"in window}},5260(e,t,n){"use strict";n.d(t,{A:()=>o}),n(6540);var r=n(545),a=n(4848);function o(e){return(0,a.jsx)(r.mg,{...e})}},8774(e,t,n){"use strict";n.d(t,{A:()=>f});var r=n(6540),a=n(4625),o=n(440),i=n(4586),l=n(6654),s=n(8193),u=n(7988),c=n(6025),d=n(4848);let f=r.forwardRef(function({isNavLink:e,to:t,href:n,activeClassName:f,isActive:p,"data-noBrokenLinkCheck":m,autoAddBaseUrl:h=!0,...g},b){var y;let{siteConfig:v}=(0,i.default)(),{trailingSlash:k,baseUrl:w}=v,x=v.future.experimental_router,{withBaseUrl:S}=(0,c.hH)(),E=(0,u.A)(),_=(0,r.useRef)(null);(0,r.useImperativeHandle)(b,()=>_.current);let A=t||n,C=(0,l.A)(A),T=A?.replace("pathname://",""),P=void 0!==T?(y=T,h&&y.startsWith("/")?S(y):y):void 0;"hash"===x&&P?.startsWith("./")&&(P=P?.slice(1)),P&&C&&(P=(0,o.applyTrailingSlash)(P,{trailingSlash:k,baseUrl:w}));let O=(0,r.useRef)(!1),L=e?a.k2:a.N_,j=s.A.canUseIntersectionObserver,N=(0,r.useRef)(),R=()=>{O.current||null==P||(window.docusaurus.preload(P),O.current=!0)};(0,r.useEffect)(()=>(!j&&C&&s.A.canUseDOM&&null!=P&&window.docusaurus.prefetch(P),()=>{j&&N.current&&N.current.disconnect()}),[N,P,j,C]);let I=P?.startsWith("#")??!1,F=!g.target||"_self"===g.target,D=!P||!C||!F||I&&"hash"!==x;m||!I&&D||E.collectLink(P),g.id&&E.collectAnchor(g.id);let z={};return D?(0,d.jsx)("a",{ref:_,href:P,...A&&!C&&{target:"_blank",rel:"noopener noreferrer"},...g,...z}):(0,d.jsx)(L,{...g,onMouseEnter:R,onTouchStart:R,innerRef:e=>{_.current=e,j&&e&&C&&(N.current=new window.IntersectionObserver(t=>{t.forEach(t=>{e===t.target&&(t.isIntersecting||t.intersectionRatio>0)&&(N.current.unobserve(e),N.current.disconnect(),null!=P&&window.docusaurus.prefetch(P))})}),N.current.observe(e))},to:P,...e&&{isActive:p,activeClassName:f},...z})})},7856(e,t,n){"use strict";n.d(t,{A:()=>u,T:()=>s});var r=n(6540),a=n(4848);function o(e,t){let n=e.split(/(\{\w+\})/).map((e,n)=>{if(n%2==1){let n=t?.[e.slice(1,-1)];if(void 0!==n)return n}return e});return n.some(e=>(0,r.isValidElement)(e))?n.map((e,t)=>(0,r.isValidElement)(e)?r.cloneElement(e,{key:t}):e).filter(e=>""!==e):n.join("")}var i=n(2654);function l({id:e,message:t}){if(void 0===e&&void 0===t)throw Error("Docusaurus translation declarations must have at least a translation id or a default translation message");return i[e??t]??t??e}function s({message:e,id:t},n){return o(l({message:e,id:t}),n)}function u({children:e,id:t,values:n}){if(e&&"string"!=typeof e)throw console.warn("Illegal children",e),Error("The Docusaurus component only accept simple string values");let r=l({message:e,id:t});return(0,a.jsx)(a.Fragment,{children:o(r,n)})}},7065(e,t,n){"use strict";n.d(t,{W:()=>r});let r="default"},6654(e,t,n){"use strict";function r(e){return/^(?:\w*:|\/\/)/.test(e)}function a(e){return void 0!==e&&!r(e)}n.d(t,{A:()=>a,z:()=>r})},6025(e,t,n){"use strict";n.d(t,{Ay:()=>l,hH:()=>i});var r=n(6540),a=n(4586),o=n(6654);function i(){let{siteConfig:e}=(0,a.default)(),{baseUrl:t,url:n}=e,i=e.future.experimental_router;return{withBaseUrl:(0,r.useCallback)((e,r)=>(function({siteUrl:e,baseUrl:t,url:n,options:{forcePrependBaseUrl:r=!1,absolute:a=!1}={},router:i}){if(!n||n.startsWith("#")||(0,o.z)(n))return n;if("hash"===i)return n.startsWith("/")?`.${n}`:`./${n}`;if(r)return t+n.replace(/^\//,"");if(n===t.replace(/\/$/,""))return t;let l=n.startsWith(t)?n:t+n.replace(/^\//,"");return a?e+l:l})({siteUrl:n,baseUrl:t,url:e,options:r,router:i}),[n,t,i])}}function l(e,t={}){let{withBaseUrl:n}=i();return n(e,t)}},7988(e,t,n){"use strict";n.d(t,{A:()=>o});var r=n(6540);n(4848);let a=r.createContext({collectAnchor:()=>{},collectLink:()=>{}});function o(){return(0,r.useContext)(a)}},4586(e,t,n){"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(6540),a=n(2603);function o(){return(0,r.useContext)(a.o)}},2303(e,t,n){"use strict";n.d(t,{A:()=>o});var r=n(6540),a=n(6125);function o(){return(0,r.useContext)(a.o)}},205(e,t,n){"use strict";n.d(t,{A:()=>a});var r=n(6540);let a=n(8193).A.canUseDOM?r.useLayoutEffect:r.useEffect},6803(e,t,n){"use strict";n.d(t,{A:()=>o});var r=n(6540),a=n(3102);function o(){let e=r.useContext(a.o);if(!e)throw Error("Unexpected: no Docusaurus route context found");return e}},6921(e,t,n){"use strict";function r(e){let t={};return!function e(n,r){Object.entries(n).forEach(([n,a])=>{let o=r?`${r}.${n}`:n;"object"==typeof a&&a&&Object.keys(a).length>0?e(a,o):t[o]=a})}(e),t}n.d(t,{A:()=>r})},3102(e,t,n){"use strict";n.d(t,{W:()=>i,o:()=>o});var r=n(6540),a=n(4848);let o=r.createContext(null);function i({children:e,value:t}){let n=r.useContext(o),l=(0,r.useMemo)(()=>(function({parent:e,value:t}){if(!e){if(t){if(!("plugin"in t))throw Error("Unexpected: Docusaurus topmost route context has no `plugin` attribute")}else throw Error("Unexpected: no Docusaurus route context found");return t}let n={...e.data,...t?.data};return{plugin:e.plugin,data:n}})({parent:n,value:t}),[n,t]);return(0,a.jsx)(o.Provider,{value:l,children:e})}},3886(e,t,n){"use strict";n.d(t,{VQ:()=>p,XK:()=>g,g1:()=>h});var r=n(6540),a=n(9410),o=n(7065),i=n(6342),l=n(6447),s=n(9532),u=n(4848);let c=e=>`docs-preferred-version-${e}`,d=r.createContext(null);function f({children:e}){let t=function(){let e=(0,a.Gy)(),t=(0,i.p)().docs.versionPersistence,n=(0,r.useMemo)(()=>Object.keys(e),[e]),[o,s]=(0,r.useState)(()=>Object.fromEntries(n.map(e=>[e,{preferredVersionName:null}])));return(0,r.useEffect)(()=>{s(function({pluginIds:e,versionPersistence:t,allDocsData:n}){return Object.fromEntries(e.map(e=>{var r,a;let o,i,s;return[e,(i=e,s=t,o=(0,l.Wf)(c(i),{persistence:s}).get(),n[e].versions.some(e=>e.name===o)?{preferredVersionName:o}:(r=e,a=t,(0,l.Wf)(c(r),{persistence:a}).del(),{preferredVersionName:null}))]}))}({allDocsData:e,versionPersistence:t,pluginIds:n}))},[e,t,n]),[o,(0,r.useMemo)(()=>({savePreferredVersion:function(e,n){var r,a,o;r=e,a=t,o=n,(0,l.Wf)(c(r),{persistence:a}).set(o),s(t=>({...t,[e]:{preferredVersionName:n}}))}}),[t])]}();return(0,u.jsx)(d.Provider,{value:t,children:e})}function p({children:e}){return(0,u.jsx)(f,{children:e})}function m(){let e=(0,r.useContext)(d);if(!e)throw new s.dV("DocsPreferredVersionContextProvider");return e}function h(e=o.W){let t=(0,a.ht)(e),[n,i]=m(),{preferredVersionName:l}=n[e];return{preferredVersion:t.versions.find(e=>e.name===l)??null,savePreferredVersionName:(0,r.useCallback)(t=>{i.savePreferredVersion(e,t)},[i,e])}}function g(){let e=(0,a.Gy)(),[t]=m();return Object.fromEntries(Object.keys(e).map(n=>[n,function(n){let r=e[n],{preferredVersionName:a}=t[n];return r.versions.find(e=>e.name===a)??null}(n)]))}},2565(e,t,n){"use strict";n.d(t,{k:()=>o,v:()=>i});var r=n(9410),a=n(3886);function o(e,t){return`docs-${e}-${t}`}function i(){let e=(0,r.Gy)(),t=(0,r.gk)(),n=(0,a.XK)();return[...Object.keys(e).map(function(r){let a=t?.activePlugin.pluginId===r?t.activeVersion:void 0,i=n[r],l=e[r].versions.find(e=>e.isLast);return o(r,(a??i??l).name)})]}},609(e,t,n){"use strict";n.d(t,{V:()=>s,t:()=>u});var r=n(6540),a=n(9532),o=n(4848);let i=Symbol("EmptyContext"),l=r.createContext(i);function s({children:e,name:t,items:n}){let a=(0,r.useMemo)(()=>t&&n?{name:t,items:n}:null,[t,n]);return(0,o.jsx)(l.Provider,{value:a,children:e})}function u(){let e=(0,r.useContext)(l);if(e===i)throw new a.dV("DocsSidebarProvider");return e}},6972(e,t,n){"use strict";n.d(t,{$S:()=>m,B5:()=>E,Nr:()=>p,OF:()=>k,QB:()=>S,Vd:()=>w,Y:()=>y,a4:()=>h,cC:()=>f,d1:()=>_,fW:()=>x,w8:()=>b});var r=n(6540),a=n(6347),o=n(2831),i=n(9410),l=n(9169),s=n(1682),u=n(3886),c=n(3025),d=n(609);function f(e){let t=(0,c.r)();if(!e)return;let n=t.docs[e];if(!n)throw Error(`no version doc found by id=${e}`);return n}function p(e){if("link"===e.type&&!e.unlisted)return e.href;if("category"===e.type){if(e.href&&!e.linkUnlisted)return e.href;for(let t of e.items){let e=p(t);if(e)return e}return}}function m(){let{pathname:e}=(0,a.zy)(),t=(0,d.t)();if(!t)throw Error("Unexpected: cant find current sidebar in context");let n=v({sidebarItems:t.items,pathname:e,onlyCategories:!0}).slice(-1)[0];if(!n)throw Error(`${e} is not associated with a category. useCurrentSidebarCategory() should only be used on category index pages.`);return n}function h(){let{pathname:e}=(0,a.zy)(),t=(0,d.t)();if(!t)throw Error("Unexpected: cant find current sidebar in context");let n=v({sidebarItems:t.items,pathname:e,onlyCategories:!0}).slice(-1)[0];return n?.items??t.items}let g=(e,t)=>void 0!==e&&(0,l.ys)(e,t);function b(e,t){if("link"===e.type)return g(e.href,t);if("category"===e.type){let n;return g(e.href,t)||(n=e.items,n.some(e=>b(e,t)))}return!1}function y(e,t){return(0,r.useMemo)(()=>e.filter(e=>(function e(t,n){switch(t.type){case"category":return b(t,n)||void 0!==t.href&&!t.linkUnlisted||t.items.some(t=>e(t,n));case"link":return!t.unlisted||b(t,n);default:return!0}})(e,t)),[e,t])}function v({sidebarItems:e,pathname:t,onlyCategories:n=!1}){let r=[];return!function e(a){for(let o of a)if("category"===o.type&&((0,l.ys)(o.href,t)||e(o.items))||"link"===o.type&&(0,l.ys)(o.href,t))return n&&"category"!==o.type||r.unshift(o),!0;return!1}(e),r}function k(){let e=(0,d.t)(),{pathname:t}=(0,a.zy)();return!1!==(0,i.vT)()?.pluginData.breadcrumbs&&e?v({sidebarItems:e.items,pathname:t}):null}function w(e){let{activeVersion:t}=(0,i.zK)(e),{preferredVersion:n}=(0,u.g1)(e),a=(0,i.r7)(e);return(0,r.useMemo)(()=>(0,s.sb)([t,n,a].filter(Boolean)),[t,n,a])}function x(e,t){let n=w(t);return(0,r.useMemo)(()=>{let t=n.flatMap(e=>e.sidebars?Object.entries(e.sidebars):[]),r=t.find(t=>t[0]===e);if(!r)throw Error(`Can't find any sidebar with id "${e}" in version${n.length>1?"s":""} ${n.map(e=>e.name).join(", ")}". Available sidebar ids are: - ${t.map(e=>e[0]).join("\n- ")}`);return r[1]},[e,n])}function S(e,t){let n=w(t);return(0,r.useMemo)(()=>{let t=n.flatMap(e=>e.docs),r=t.find(t=>t.id===e);if(!r){if(n.flatMap(e=>e.draftIds).includes(e))return null;throw Error(`Couldn't find any doc with id "${e}" in version${n.length>1?"s":""} "${n.map(e=>e.name).join(", ")}". Available doc ids are: @@ -35,4 +35,4 @@ Please double-check the following navbar item (themeConfig.navbar.items) of your ${JSON.stringify(e,null,2)}`,{cause:t}),children:(0,c.jsx)(eO,{...e})},t))})}function eQ({left:e,right:t}){return(0,c.jsxs)("div",{className:"navbar__inner",children:[(0,c.jsx)("div",{className:(0,a.A)(h.G.layout.navbar.containerLeft,"navbar__items"),children:e}),(0,c.jsx)("div",{className:(0,a.A)(h.G.layout.navbar.containerRight,"navbar__items navbar__items--right"),children:t})]})}function eV(){let e=(0,E.M)(),t=(0,y.p)().navbar.items,[n,r]=function(e){function t(e){return(e.position??"right")==="left"}return[e.filter(t),e.filter(e=>!t(e))]}(t),a=t.find(e=>"search"===e.type);return(0,c.jsx)(eQ,{left:(0,c.jsxs)(c.Fragment,{children:[!e.disabled&&(0,c.jsx)(eU,{}),(0,c.jsx)(Q,{}),(0,c.jsx)(eH,{items:n})]}),right:(0,c.jsxs)(c.Fragment,{children:[(0,c.jsx)(eH,{items:r}),(0,c.jsx)(U,{className:"colorModeToggle_DEke"}),!a&&(0,c.jsx)(e_,{children:(0,c.jsx)(eE,{})})]})})}function eW(){return(0,c.jsx)(eF,{children:(0,c.jsx)(eV,{})})}function eq({item:e}){let{to:t,href:n,label:r,prependBaseUrlToHref:o,className:i,...l}=e,s=(0,G.Ay)(t),u=(0,G.Ay)(n,{forcePrependBaseUrl:!0});return(0,c.jsxs)(q.A,{className:(0,a.A)("footer__link-item",i),...n?{href:o?u:n}:{to:s},...l,children:[r,n&&!(0,Y.A)(n)&&(0,c.jsx)(Z.A,{})]})}function eG({item:e}){return e.html?(0,c.jsx)("li",{className:(0,a.A)("footer__item",e.className),dangerouslySetInnerHTML:{__html:e.html}}):(0,c.jsx)("li",{className:"footer__item",children:(0,c.jsx)(eq,{item:e})},e.href??e.to)}function eY({column:e}){return(0,c.jsxs)("div",{className:(0,a.A)(h.G.layout.footer.column,"col footer__col",e.className),children:[(0,c.jsx)("div",{className:"footer__title",children:e.title}),(0,c.jsx)("ul",{className:"footer__items clean-list",children:e.items.map((e,t)=>(0,c.jsx)(eG,{item:e},t))})]})}function eK({columns:e}){return(0,c.jsx)("div",{className:"row footer__links",children:e.map((e,t)=>(0,c.jsx)(eY,{column:e},t))})}function eZ(){return(0,c.jsx)("span",{className:"footer__link-separator",children:"\xb7"})}function eX({item:e}){return e.html?(0,c.jsx)("span",{className:(0,a.A)("footer__link-item",e.className),dangerouslySetInnerHTML:{__html:e.html}}):(0,c.jsx)(eq,{item:e})}function eJ({links:e}){return(0,c.jsx)("div",{className:"footer__links text--center",children:(0,c.jsx)("div",{className:"footer__links",children:e.map((t,n)=>(0,c.jsxs)(r.Fragment,{children:[(0,c.jsx)(eX,{item:t}),e.length!==n+1&&(0,c.jsx)(eZ,{})]},n))})})}function e0({links:e}){return"title"in e[0]?(0,c.jsx)(eK,{columns:e}):(0,c.jsx)(eJ,{links:e})}var e1=n(3362);function e2({logo:e}){let{withBaseUrl:t}=(0,G.hH)(),n={light:t(e.src),dark:t(e.srcDark??e.src)};return(0,c.jsx)(e1.A,{className:(0,a.A)("footer__logo",e.className),alt:e.alt,sources:n,width:e.width,height:e.height,style:e.style})}function e4({logo:e}){return e.href?(0,c.jsx)(q.A,{href:e.href,className:"footerLogoLink_BH7S",target:e.target,children:(0,c.jsx)(e2,{logo:e})}):(0,c.jsx)(e2,{logo:e})}function e3({copyright:e}){return(0,c.jsx)("div",{className:"footer__copyright",dangerouslySetInnerHTML:{__html:e}})}function e5({style:e,links:t,logo:n,copyright:r}){return(0,c.jsx)("footer",{className:(0,a.A)(h.G.layout.footer.container,"footer",{"footer--dark":"dark"===e}),children:(0,c.jsxs)("div",{className:"container container-fluid",children:[t,(n||r)&&(0,c.jsxs)("div",{className:"footer__bottom text--center",children:[n&&(0,c.jsx)("div",{className:"margin-bottom--sm",children:n}),r]})]})})}let e6=r.memo(function(){let{footer:e}=(0,y.p)();if(!e)return null;let{copyright:t,links:n,logo:r,style:a}=e;return(0,c.jsx)(e5,{style:a,links:n&&n.length>0&&(0,c.jsx)(e0,{links:n}),logo:r&&(0,c.jsx)(e4,{logo:r}),copyright:t&&(0,c.jsx)(e3,{copyright:t})})}),e8=(0,A.fM)([N.a,v.o,_.Tv,eC.VQ,i.Jx,function({children:e}){return(0,c.jsx)(C.y_,{children:(0,c.jsx)(E.e,{children:(0,c.jsx)(P,{children:e})})})}]);function e9({children:e}){return(0,c.jsx)(e8,{children:e})}var e7=n(4608);function te({error:e,tryAgain:t}){return(0,c.jsx)("main",{className:"container margin-vert--xl",children:(0,c.jsx)("div",{className:"row",children:(0,c.jsxs)("div",{className:"col col--6 col--offset-3",children:[(0,c.jsx)(e7.A,{as:"h1",className:"hero__title",children:(0,c.jsx)(s.A,{id:"theme.ErrorPageContent.title",description:"The title of the fallback page when the page crashed",children:"This page crashed."})}),(0,c.jsx)("div",{className:"margin-vert--lg",children:(0,c.jsx)(ez,{onClick:t,className:"button button--primary shadow--lw"})}),(0,c.jsx)("hr",{}),(0,c.jsx)("div",{className:"margin-vert--md",children:(0,c.jsx)(eM,{error:e})})]})})})}function tt(e){let{children:t,noFooter:n,wrapperClassName:r,title:l,description:s}=e;return(0,g.J)(),(0,c.jsxs)(e9,{children:[(0,c.jsx)(i.be,{title:l,description:s}),(0,c.jsx)(b,{}),(0,c.jsx)(S,{}),(0,c.jsx)(eW,{}),(0,c.jsx)("div",{id:d,className:(0,a.A)(h.G.layout.main.container,h.G.wrapper.main,"mainWrapper_z2l0",r),children:(0,c.jsx)(o.A,{fallback:e=>(0,c.jsx)(te,{...e}),children:t})}),!n&&(0,c.jsx)(e6,{})]})}},3465(e,t,n){"use strict";n.d(t,{A:()=>c}),n(6540);var r=n(8774),a=n(6025),o=n(4586),i=n(6342),l=n(3362),s=n(4848);function u({logo:e,alt:t,imageClassName:n}){let r={light:(0,a.Ay)(e.src),dark:(0,a.Ay)(e.srcDark||e.src)},o=(0,s.jsx)(l.A,{className:e.className,sources:r,height:e.height,width:e.width,alt:t,style:e.style});return n?(0,s.jsx)("div",{className:n,children:o}):o}function c(e){let{siteConfig:{title:t}}=(0,o.default)(),{navbar:{title:n,logo:l}}=(0,i.p)(),{imageClassName:c,titleClassName:d,...f}=e,p=(0,a.Ay)(l?.href||"/"),m=l?.alt??(n?"":t);return(0,s.jsxs)(r.A,{to:p,...f,...l?.target&&{target:l.target},children:[l&&(0,s.jsx)(u,{logo:l,alt:m,imageClassName:c}),null!=n&&(0,s.jsx)("b",{className:d,children:n})]})}},1463(e,t,n){"use strict";n.d(t,{A:()=>o}),n(6540);var r=n(5260),a=n(4848);function o({locale:e,version:t,tag:n}){return(0,a.jsxs)(r.A,{children:[e&&(0,a.jsx)("meta",{name:"docusaurus_locale",content:e}),t&&(0,a.jsx)("meta",{name:"docusaurus_version",content:t}),n&&(0,a.jsx)("meta",{name:"docusaurus_tag",content:n}),e&&(0,a.jsx)("meta",{name:"docsearch:language",content:e}),t&&(0,a.jsx)("meta",{name:"docsearch:version",content:t}),n&&(0,a.jsx)("meta",{name:"docsearch:docusaurus_tag",content:n})]})}},3362(e,t,n){"use strict";n.d(t,{A:()=>c});var r=n(6540),a=n(4164),o=n(2303),i=n(5293);let l={themedComponent:"themedComponent_mlkZ","themedComponent--light":"themedComponent--light_NVdE","themedComponent--dark":"themedComponent--dark_xIcU"};var s=n(4848);function u({className:e,children:t}){let n=(0,o.A)(),{colorMode:c}=(0,i.G)();return(0,s.jsx)(s.Fragment,{children:(n?"dark"===c?["dark"]:["light"]:["light","dark"]).map(n=>{let o=t({theme:n,className:(0,a.A)(e,l.themedComponent,l[`themedComponent--${n}`])});return(0,s.jsx)(r.Fragment,{children:o},n)})})}function c(e){let{sources:t,className:n,alt:r,...a}=e;return(0,s.jsx)(u,{className:n,children:({theme:e,className:n})=>(0,s.jsx)("img",{src:t[e],alt:r,className:n,...a})})}},1422(e,t,n){"use strict";n.d(t,{N:()=>p,u:()=>l});var r=n(6540),a=n(205),o=n(3109),i=n(4848);function l({initialState:e}){let[t,n]=(0,r.useState)(e??!1),a=(0,r.useCallback)(()=>{n(e=>!e)},[]);return{collapsed:t,setCollapsed:n,toggleCollapsed:a}}let s={display:"none",overflow:"hidden",height:"0px"},u={display:"block",overflow:"visible",height:"auto"};function c(e,t){let n=t?s:u;e.style.display=n.display,e.style.overflow=n.overflow,e.style.height=n.height}function d({as:e="div",collapsed:t,children:n,animation:a,onCollapseTransitionEnd:l,className:u}){let f=(0,r.useRef)(null);return!function({collapsibleRef:e,collapsed:t,animation:n}){let a=(0,r.useRef)(!1);(0,r.useEffect)(()=>{let r,i=e.current;function l(){let e,t,r,a=(e=i.scrollHeight,t=n?.duration??function(e){if((0,o.O)())return 1;let t=e/36;return Math.round((4+15*t**.25+t/5)*10)}(e),r=n?.easing??"ease-in-out",{transition:`height ${t}ms ${r}`,height:`${e}px`});i.style.transition=a.transition,i.style.height=a.height}if(!a.current){c(i,t),a.current=!0;return}return i.style.willChange="height",r=requestAnimationFrame(()=>{t?(l(),requestAnimationFrame(()=>{i.style.height=s.height,i.style.overflow=s.overflow})):(i.style.display="block",requestAnimationFrame(()=>{l()}))}),()=>cancelAnimationFrame(r)},[e,t,n])}({collapsibleRef:f,collapsed:t,animation:a}),(0,i.jsx)(e,{ref:f,onTransitionEnd:e=>{"height"===e.propertyName&&(c(f.current,t),l?.(t))},className:u,children:n})}function f({collapsed:e,...t}){let[n,o]=(0,r.useState)(!e),[l,s]=(0,r.useState)(e);return(0,a.A)(()=>{e||o(!0)},[e]),(0,a.A)(()=>{n&&s(e)},[n,e]),n?(0,i.jsx)(d,{...t,collapsed:l}):null}function p({lazy:e,...t}){return(0,i.jsx)(e?f:d,{...t})}},5041(e,t,n){"use strict";n.d(t,{M:()=>m,o:()=>p});var r=n(6540),a=n(2303),o=n(6447),i=n(9532),l=n(6342),s=n(4848);let u=(0,o.Wf)("docusaurus.announcement.dismiss"),c=(0,o.Wf)("docusaurus.announcement.id"),d=e=>u.set(String(e)),f=r.createContext(null);function p({children:e}){let t=function(){let{announcementBar:e}=(0,l.p)(),t=(0,a.A)(),[n,o]=(0,r.useState)(()=>!!t&&"true"===u.get());(0,r.useEffect)(()=>{o("true"===u.get())},[]);let i=(0,r.useCallback)(()=>{d(!0),o(!0)},[]);return(0,r.useEffect)(()=>{if(!e)return;let{id:t}=e,n=c.get();"annoucement-bar"===n&&(n="announcement-bar");let r=t!==n;c.set(t),r&&d(!1),(r||"true"!==u.get())&&o(!1)},[e]),(0,r.useMemo)(()=>({isActive:!!e&&!n,close:i}),[e,n,i])}();return(0,s.jsx)(f.Provider,{value:t,children:e})}function m(){let e=(0,r.useContext)(f);if(!e)throw new i.dV("AnnouncementBarProvider");return e}},5293(e,t,n){"use strict";n.d(t,{G:()=>k,a:()=>v});var r=n(6540),a=n(2303),o=n(9532),i=n(6447),l=n(6342),s=n(4848);function u(){return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}let c=r.createContext(void 0),d=(0,i.Wf)("theme"),f="system",p=e=>"dark"===e?"dark":"light",m=e=>null===e||e===f?null:p(e),h=()=>p(document.documentElement.getAttribute("data-theme")),g=e=>{document.documentElement.setAttribute("data-theme",p(e))},b=()=>m(document.documentElement.getAttribute("data-theme-choice")),y=e=>{document.documentElement.setAttribute("data-theme-choice",m(e)??f)};function v({children:e}){let t=function(){let{colorMode:{defaultMode:e,disableSwitch:t,respectPrefersColorScheme:n}}=(0,l.p)(),{colorMode:o,setColorModeState:i,colorModeChoice:s,setColorModeChoiceState:c}=function(){let{colorMode:{defaultMode:e}}=(0,l.p)(),t=(0,a.A)(),[n,o]=(0,r.useState)(t?h():e),[i,s]=(0,r.useState)(t?b():null);return(0,r.useEffect)(()=>{o(h()),s(b())},[]),{colorMode:n,setColorModeState:o,colorModeChoice:i,setColorModeChoiceState:s}}();(0,r.useEffect)(()=>{t&&d.del()},[t]);let f=(0,r.useCallback)((t,r={})=>{let{persist:a=!0}=r;if(null===t){let t=n?u():e;g(t),i(t),y(null),c(null)}else g(t),y(t),i(t),c(t);a&&(null===t?d.del():d.set(p(t)))},[i,c,n,e]);return(0,r.useEffect)(()=>d.listen(e=>{f(m(e.newValue))}),[f]),(0,r.useEffect)(()=>{if(null===s&&n){var e,t;let n;return e=e=>{i(e),g(e)},t=()=>e(u()),(n=window.matchMedia("(prefers-color-scheme: dark)")).addEventListener("change",t),()=>n.removeEventListener("change",t)}},[n,s,i]),(0,r.useMemo)(()=>({colorMode:o,colorModeChoice:s,setColorMode:f,get isDarkTheme(){return"dark"===o},setLightTheme(){f("light")},setDarkTheme(){f("dark")}}),[o,s,f])}();return(0,s.jsx)(c.Provider,{value:t,children:e})}function k(){let e=(0,r.useContext)(c);if(null==e)throw new o.dV("ColorModeProvider","Please see https://docusaurus.io/docs/api/themes/configuration#use-color-mode.");return e}},2069(e,t,n){"use strict";n.d(t,{M:()=>p,e:()=>f});var r=n(6540),a=n(5600),o=n(4581),i=n(7485),l=n(6342),s=n(9532),u=n(4848);let c=r.createContext(void 0);function d({handler:e}){return(0,i.$Z)(e),null}function f({children:e}){let t=function(){let e=function(){let e=(0,a.YL)(),{items:t}=(0,l.p)().navbar;return 0===t.length&&!e.component}(),t=(0,o.l)(),n=!e&&"mobile"===t,[i,s]=(0,r.useState)(!1),u=(0,r.useCallback)(()=>{s(e=>!e)},[]);return(0,r.useEffect)(()=>{"desktop"===t&&s(!1)},[t]),(0,r.useMemo)(()=>({disabled:e,shouldRender:n,toggle:u,shown:i}),[e,n,u,i])}();return(0,u.jsxs)(u.Fragment,{children:[t.shown&&(0,u.jsx)(d,{handler:()=>(t.toggle(),!1)}),(0,u.jsx)(c.Provider,{value:t,children:e})]})}function p(){let e=r.useContext(c);if(void 0===e)throw new s.dV("NavbarMobileSidebarProvider");return e}},5600(e,t,n){"use strict";n.d(t,{GX:()=>u,YL:()=>s,y_:()=>l});var r=n(6540),a=n(9532),o=n(4848);let i=r.createContext(null);function l({children:e}){let t=(0,r.useState)({component:null,props:null});return(0,o.jsx)(i.Provider,{value:t,children:e})}function s(){let e=(0,r.useContext)(i);if(!e)throw new a.dV("NavbarSecondaryMenuContentProvider");return e[0]}function u({component:e,props:t}){let n=(0,r.useContext)(i);if(!n)throw new a.dV("NavbarSecondaryMenuContentProvider");let[,o]=n,l=(0,a.Be)(t);return(0,r.useEffect)(()=>{o({component:e,props:l})},[o,e,l]),(0,r.useEffect)(()=>()=>o({component:null,props:null}),[o]),null}},4849(e,t,n){"use strict";n.d(t,{w:()=>a,J:()=>o});var r=n(6540);let a="navigation-with-keyboard";function o(){(0,r.useEffect)(()=>{function e(e){"keydown"===e.type&&"Tab"===e.key&&document.body.classList.add(a),"mousedown"===e.type&&document.body.classList.remove(a)}return document.addEventListener("keydown",e),document.addEventListener("mousedown",e),()=>{document.body.classList.remove(a),document.removeEventListener("keydown",e),document.removeEventListener("mousedown",e)}},[])}},4581(e,t,n){"use strict";n.d(t,{l:()=>o});var r=n(6540),a=n(8193);function o({desktopBreakpoint:e=996}={}){let[t,n]=(0,r.useState)(()=>"ssr");return(0,r.useEffect)(()=>{function t(){n(function(e){if(!a.A.canUseDOM)throw Error("getWindowSize() should only be called after React hydration");return window.innerWidth>e?"desktop":"mobile"}(e))}return t(),window.addEventListener("resize",t),()=>{window.removeEventListener("resize",t)}},[e]),t}},7559(e,t,n){"use strict";n.d(t,{G:()=>r});let r={page:{blogListPage:"blog-list-page",blogPostPage:"blog-post-page",blogTagsListPage:"blog-tags-list-page",blogTagPostListPage:"blog-tags-post-list-page",blogAuthorsListPage:"blog-authors-list-page",blogAuthorsPostsPage:"blog-authors-posts-page",docsDocPage:"docs-doc-page",docsTagsListPage:"docs-tags-list-page",docsTagDocListPage:"docs-tags-doc-list-page",mdxPage:"mdx-page"},wrapper:{main:"main-wrapper",blogPages:"blog-wrapper",docsPages:"docs-wrapper",mdxPages:"mdx-wrapper"},common:{editThisPage:"theme-edit-this-page",lastUpdated:"theme-last-updated",backToTopButton:"theme-back-to-top-button",codeBlock:"theme-code-block",admonition:"theme-admonition",unlistedBanner:"theme-unlisted-banner",draftBanner:"theme-draft-banner",admonitionType:e=>`theme-admonition-${e}`},announcementBar:{container:"theme-announcement-bar"},tabs:{container:"theme-tabs-container"},layout:{navbar:{container:"theme-layout-navbar",containerLeft:"theme-layout-navbar-left",containerRight:"theme-layout-navbar-right",mobileSidebar:{container:"theme-layout-navbar-sidebar",panel:"theme-layout-navbar-sidebar-panel"}},main:{container:"theme-layout-main"},footer:{container:"theme-layout-footer",column:"theme-layout-footer-column"}},docs:{docVersionBanner:"theme-doc-version-banner",docVersionBadge:"theme-doc-version-badge",docBreadcrumbs:"theme-doc-breadcrumbs",docMarkdown:"theme-doc-markdown",docTocMobile:"theme-doc-toc-mobile",docTocDesktop:"theme-doc-toc-desktop",docFooter:"theme-doc-footer",docFooterTagsRow:"theme-doc-footer-tags-row",docFooterEditMetaRow:"theme-doc-footer-edit-meta-row",docSidebarContainer:"theme-doc-sidebar-container",docSidebarMenu:"theme-doc-sidebar-menu",docSidebarItemCategory:"theme-doc-sidebar-item-category",docSidebarItemLink:"theme-doc-sidebar-item-link",docSidebarItemCategoryLevel:e=>`theme-doc-sidebar-item-category-level-${e}`,docSidebarItemLinkLevel:e=>`theme-doc-sidebar-item-link-level-${e}`},blog:{blogFooterTagsRow:"theme-blog-footer-tags-row",blogFooterEditMetaRow:"theme-blog-footer-edit-meta-row"},pages:{pageFooterEditMetaRow:"theme-pages-footer-edit-meta-row"}}},3109(e,t,n){"use strict";function r(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}n.d(t,{O:()=>r})},4271(e,t,n){"use strict";n.d(t,{v:()=>a});var r=n(6342);function a(e){let{navbar:{hideOnScroll:t}}=(0,r.p)();if(void 0!==e)return t?"anchorTargetHideOnScrollNavbar_vjPI":"anchorTargetStickyNavbar_Vzrq"}},7485(e,t,n){"use strict";n.d(t,{$Z:()=>i,Hl:()=>l,aZ:()=>s,jy:()=>u});var r=n(6540),a=n(6347),o=n(9532);function i(e){let t,n;t=(0,a.W6)(),n=(0,o._q)((t,n)=>{if("POP"===n)return e(t,n)}),(0,r.useEffect)(()=>t.block((e,t)=>n(e,t)),[t,n])}function l(e){let t=(0,a.W6)();return(0,r.useSyncExternalStore)(t.listen,()=>e(t),()=>e({...t,location:{...t.location,search:"",hash:"",state:void 0}}))}function s(e){return l(t=>null===e?null:new URLSearchParams(t.location.search).get(e))}function u(e,t){let n=(function(e,t){let n=new URLSearchParams;for(let r of e)for(let[e,a]of r.entries())"append"===t?n.append(e,a):n.set(e,a);return n})(e.map(e=>new URLSearchParams(e??"")),t).toString();return n?`?${n}`:n}},1682(e,t,n){"use strict";function r(e,t=(e,t)=>e===t){return e.filter((n,r)=>e.findIndex(e=>t(e,n))!==r)}function a(e){return Array.from(new Set(e))}n.d(t,{XI:()=>r,sb:()=>a})},5500(e,t,n){"use strict";n.d(t,{Jx:()=>b,be:()=>m,e3:()=>g});var r=n(6540),a=n(4164),o=n(5260),i=n(6803),l=n(6025),s=n(4563),u=n(4848);function c({title:e}){let t=(0,s.s$)().format(e);return(0,u.jsxs)(o.A,{children:[(0,u.jsx)("title",{children:t}),(0,u.jsx)("meta",{property:"og:title",content:t})]})}function d({description:e}){return(0,u.jsxs)(o.A,{children:[(0,u.jsx)("meta",{name:"description",content:e}),(0,u.jsx)("meta",{property:"og:description",content:e})]})}function f({image:e}){let{withBaseUrl:t}=(0,l.hH)(),n=t(e,{absolute:!0});return(0,u.jsxs)(o.A,{children:[(0,u.jsx)("meta",{property:"og:image",content:n}),(0,u.jsx)("meta",{name:"twitter:image",content:n})]})}function p({keywords:e}){return(0,u.jsx)(o.A,{children:(0,u.jsx)("meta",{name:"keywords",content:Array.isArray(e)?e.join(","):e})})}function m({title:e,description:t,keywords:n,image:r,children:a}){return(0,u.jsxs)(u.Fragment,{children:[e&&(0,u.jsx)(c,{title:e}),t&&(0,u.jsx)(d,{description:t}),n&&(0,u.jsx)(p,{keywords:n}),r&&(0,u.jsx)(f,{image:r}),a&&(0,u.jsx)(o.A,{children:a})]})}let h=r.createContext(void 0);function g({className:e,children:t}){let n=r.useContext(h),i=(0,a.A)(n,e);return(0,u.jsxs)(h.Provider,{value:i,children:[(0,u.jsx)(o.A,{children:(0,u.jsx)("html",{className:i})}),t]})}function b({children:e}){var t;let n=(0,i.A)(),r=(t=n.plugin.name,`plugin-${t.replace(/docusaurus-(?:plugin|theme)-(?:content-)?/gi,"")}`),o=`plugin-id-${n.plugin.id}`;return(0,u.jsx)(g,{className:(0,a.A)(r,o),children:e})}},9532(e,t,n){"use strict";n.d(t,{Be:()=>u,ZC:()=>l,_q:()=>i,dV:()=>s,fM:()=>c});var r=n(6540),a=n(205),o=n(4848);function i(e){let t=(0,r.useRef)(e);return(0,a.A)(()=>{t.current=e},[e]),(0,r.useCallback)((...e)=>t.current(...e),[])}function l(e){let t=(0,r.useRef)();return(0,a.A)(()=>{t.current=e}),t.current}class s extends Error{constructor(e,t){super(),this.name="ReactContextError",this.message=`Hook ${this.stack?.split("\n")[1]?.match(/at (?:\w+\.)?(?\w+)/)?.groups.name??""} is called outside the <${e}>. ${t??""}`}}function u(e){let t=Object.entries(e);return t.sort((e,t)=>e[0].localeCompare(t[0])),(0,r.useMemo)(()=>e,t.flat())}function c(e){return({children:t})=>(0,o.jsx)(o.Fragment,{children:e.reduceRight((e,t)=>(0,o.jsx)(t,{children:e}),t)})}},9169(e,t,n){"use strict";n.d(t,{Dt:()=>l,ys:()=>i});var r=n(6540),a=n(8708),o=n(4586);function i(e,t){let n=e=>(!e||e.endsWith("/")?e:`${e}/`)?.toLowerCase();return n(e)===n(t)}function l(){let{baseUrl:e}=(0,o.default)().siteConfig;return(0,r.useMemo)(()=>(function({baseUrl:e,routes:t}){function n(t){return t.path===e&&!0===t.exact}function r(t){return t.path===e&&!t.exact}return function e(t){if(0===t.length)return;let a=t.find(n);return a||e(t.filter(r).flatMap(e=>e.routes??[]))}(t)})({routes:a.A,baseUrl:e}),[e])}},3104(e,t,n){"use strict";n.d(t,{Mq:()=>p,Tv:()=>c,a_:()=>m,gk:()=>h});var r=n(6540),a=n(8193),o=n(2303),i=n(205),l=n(9532),s=n(4848);let u=r.createContext(void 0);function c({children:e}){let t,n=(t=(0,r.useRef)(!0),(0,r.useMemo)(()=>({scrollEventsEnabledRef:t,enableScrollEvents:()=>{t.current=!0},disableScrollEvents:()=>{t.current=!1}}),[]));return(0,s.jsx)(u.Provider,{value:n,children:e})}function d(){let e=(0,r.useContext)(u);if(null==e)throw new l.dV("ScrollControllerProvider");return e}let f=()=>a.A.canUseDOM?{scrollX:window.pageXOffset,scrollY:window.pageYOffset}:null;function p(e,t=[]){let{scrollEventsEnabledRef:n}=d(),a=(0,r.useRef)(f()),o=(0,l._q)(e);(0,r.useEffect)(()=>{let e=()=>{if(!n.current)return;let e=f();o(e,a.current),a.current=e},t={passive:!0};return e(),window.addEventListener("scroll",e,t),()=>window.removeEventListener("scroll",e,t)},[o,n,...t])}function m(){let e,t,n,a=d(),o=(e=(0,r.useRef)({elem:null,top:0}),t=(0,r.useCallback)(t=>{e.current={elem:t,top:t.getBoundingClientRect().top}},[]),n=(0,r.useCallback)(()=>{let{current:{elem:t,top:n}}=e;if(!t)return{restored:!1};let r=t.getBoundingClientRect().top-n;return r&&window.scrollBy({left:0,top:r}),e.current={elem:null,top:0},{restored:0!==r}},[]),(0,r.useMemo)(()=>({save:t,restore:n}),[n,t])),l=(0,r.useRef)(void 0),s=(0,r.useCallback)(e=>{o.save(e),a.disableScrollEvents(),l.current=()=>{let{restored:e}=o.restore();if(l.current=void 0,e){let e=()=>{a.enableScrollEvents(),window.removeEventListener("scroll",e)};window.addEventListener("scroll",e)}else a.enableScrollEvents()}},[a,o]);return(0,i.A)(()=>{queueMicrotask(()=>l.current?.())}),{blockElementScrollPositionUntilNextRender:s}}function h(){let e=(0,r.useRef)(null),t=(0,o.A)()&&"smooth"===getComputedStyle(document.documentElement).scrollBehavior;return{startScroll:n=>{let r,a;e.current=t?(window.scrollTo({top:n,behavior:"smooth"}),()=>{}):(r=null,a=document.documentElement.scrollTop>n,!function e(){let t=document.documentElement.scrollTop;(a&&t>n||!a&&tr&&cancelAnimationFrame(r))},cancelScroll:()=>e.current?.()}}},6447(e,t,n){"use strict";n.d(t,{Wf:()=>u,Dv:()=>c});var r=n(6540),a=JSON.parse('{"N":"localStorage","M":""}');let o=a.N;function i({key:e,oldValue:t,newValue:n,storage:r}){if(t===n)return;let a=document.createEvent("StorageEvent");a.initStorageEvent("storage",!1,!1,e,t,n,window.location.href,r),window.dispatchEvent(a)}let l=!1,s={get:()=>null,set:()=>{},del:()=>{},listen:()=>()=>{}};function u(e,t){let n=`${e}${a.M}`;if("u"{try{return u.getItem(n)}catch(e){return console.error(`Docusaurus storage error, can't get key=${n}`,e),null}},set:e=>{try{let t=u.getItem(n);u.setItem(n,e),i({key:n,oldValue:t,newValue:e,storage:u})}catch(t){console.error(`Docusaurus storage error, can't set ${n}=${e}`,t)}},del:()=>{try{let e=u.getItem(n);u.removeItem(n),i({key:n,oldValue:e,newValue:null,storage:u})}catch(e){console.error(`Docusaurus storage error, can't delete key=${n}`,e)}},listen:e=>{try{let t=t=>{t.storageArea===u&&t.key===n&&e(t)};return window.addEventListener("storage",t),()=>window.removeEventListener("storage",t)}catch(e){return console.error(`Docusaurus storage error, can't listen for changes of key=${n}`,e),()=>{}}}}}function c(e,t){let[n]=(0,r.useState)(()=>null===e?s:u(e,t)),a=(0,r.useCallback)(e=>"u"{}:n.listen(e),[n]);return[(0,r.useSyncExternalStore)(a,()=>n.get(),()=>null),n]}},4563(e,t,n){"use strict";n.d(t,{AL:()=>c,s$:()=>d});var r=n(6540),a=n(4586),o=n(6803),i=n(9532),l=n(4848);let s=({title:e,siteTitle:t,titleDelimiter:n})=>{let r=e?.trim();return r&&r!==t?`${r} ${n} ${t}`:t},u=(0,r.createContext)(null);function c({formatter:e,children:t}){return(0,l.jsx)(u.Provider,{value:e,children:t})}function d(){let e=function(){let e=(0,r.useContext)(u);if(null===e)throw new i.dV("TitleFormatterProvider");return e}(),{siteConfig:t}=(0,a.default)(),{title:n,titleDelimiter:l}=t,{plugin:c}=(0,o.A)();return{format:t=>e({title:t,siteTitle:n,titleDelimiter:l,plugin:c,defaultFormatter:s})}}},2131(e,t,n){"use strict";n.d(t,{o:()=>i});var r=n(4586),a=n(6347),o=n(440);function i(){let{siteConfig:{baseUrl:e,trailingSlash:t},i18n:{localeConfigs:n}}=(0,r.default)(),{pathname:i}=(0,a.zy)(),l=(0,o.applyTrailingSlash)(i,{trailingSlash:t,baseUrl:e}).replace(e,"");return{createUrl:function({locale:e,fullyQualified:t}){let r=function(e){let t=n[e];if(!t)throw Error(`Unexpected Docusaurus bug, no locale config found for locale=${e}`);return t}(e),a=`${t?r.url:""}`,o=r.baseUrl;return`${a}${o}${l}`}}}},5062(e,t,n){"use strict";n.d(t,{$:()=>i});var r=n(6540),a=n(6347),o=n(9532);function i(e){let t=(0,a.zy)(),n=(0,o.ZC)(t),i=(0,o._q)(e);(0,r.useEffect)(()=>{n&&t!==n&&i({location:t,previousLocation:n})},[i,t,n])}},6342(e,t,n){"use strict";n.d(t,{p:()=>a});var r=n(4586);function a(){return(0,r.default)().siteConfig.themeConfig}},2983(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.addTrailingSlash=a,t.default=function(e,t){let{trailingSlash:n,baseUrl:r}=t;if(e.startsWith("#")||void 0===n)return e;let[i]=e.split(/[#?]/),l="/"===i||i===r?i:n?a(i):o(i);return e.replace(i,l)},t.addLeadingSlash=function(e){return(0,r.addPrefix)(e,"/")},t.removeTrailingSlash=o;let r=n(2566);function a(e){return e.endsWith("/")?e:`${e}/`}function o(e){return(0,r.removeSuffix)(e,"/")}},253(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getErrorCausalChain=function e(t){return t.cause?[t,...e(t.cause)]:[t]}},440(e,t,n){"use strict";t.getErrorCausalChain=t.applyTrailingSlash=void 0;let r=n(1635);var a=n(2983);Object.defineProperty(t,"applyTrailingSlash",{enumerable:!0,get:function(){return r.__importDefault(a).default}}),n(2566);var o=n(253);Object.defineProperty(t,"getErrorCausalChain",{enumerable:!0,get:function(){return o.getErrorCausalChain}})},2566(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.addPrefix=function(e,t){return e.startsWith(t)?e:`${t}${e}`},t.removeSuffix=function(e,t){return""===t?e:e.endsWith(t)?e.slice(0,-t.length):e},t.addSuffix=function(e,t){return e.endsWith(t)?e:`${e}${t}`},t.removePrefix=function(e,t){return e.startsWith(t)?e.slice(t.length):e}},7426(e,t,n){"use strict";n.d(t,{A:()=>o}),n(6540);var r=n(4164),a=n(4848);function o({className:e}){return(0,a.jsxs)("div",{className:(0,r.A)("loadingRing_RJI3",e),children:[(0,a.jsx)("div",{}),(0,a.jsx)("div",{}),(0,a.jsx)("div",{}),(0,a.jsx)("div",{})]})}},5148(e,t,n){"use strict";let r;n.d(t,{w:()=>E,k:()=>S});let a=Symbol("Comlink.proxy"),o=Symbol("Comlink.endpoint"),i=Symbol("Comlink.releaseProxy"),l=Symbol("Comlink.finalizer"),s=Symbol("Comlink.thrown"),u=e=>"object"==typeof e&&null!==e||"function"==typeof e,c=new Map([["proxy",{canHandle:e=>u(e)&&e[a],serialize(e){let{port1:t,port2:n}=new MessageChannel;return function e(t,n=globalThis,r=["*"]){n.addEventListener("message",function o(i){let u;if(!i||!i.data)return;if(!function(e,t){for(let n of e)if(t===n||"*"===n||n instanceof RegExp&&n.test(t))return!0;return!1}(r,i.origin))return void console.warn(`Invalid origin '${i.origin}' for comlink proxy`);let{id:c,type:f,path:p}=Object.assign({path:[]},i.data),m=(i.data.argumentList||[]).map(k);try{var h,g,b;let n=p.slice(0,-1).reduce((e,t)=>e[t],t),r=p.reduce((e,t)=>e[t],t);switch(f){case"GET":u=r;break;case"SET":n[p.slice(-1)[0]]=k(i.data.value),u=!0;break;case"APPLY":u=r.apply(n,m);break;case"CONSTRUCT":h=new r(...m),u=Object.assign(h,{[a]:!0});break;case"ENDPOINT":{let{port1:n,port2:r}=new MessageChannel;e(t,r),g=n,b=[n],y.set(g,b),u=g}break;case"RELEASE":u=void 0;break;default:return}}catch(e){u={value:e,[s]:0}}Promise.resolve(u).catch(e=>({value:e,[s]:0})).then(e=>{let[r,a]=v(e);n.postMessage(Object.assign(Object.assign({},r),{id:c}),a),"RELEASE"===f&&(n.removeEventListener("message",o),d(n),l in t&&"function"==typeof t[l]&&t[l]())}).catch(e=>{let[t,r]=v({value:TypeError("Unserializable return value"),[s]:0});n.postMessage(Object.assign(Object.assign({},t),{id:c}),r)})}),n.start&&n.start()}(e,t),[n,[n]]},deserialize:e=>(e.start(),f(e))}],["throw",{canHandle:e=>u(e)&&s in e,serialize:({value:e})=>[e instanceof Error?{isError:!0,value:{message:e.message,name:e.name,stack:e.stack}}:{isError:!1,value:e},[]],deserialize(e){if(e.isError)throw Object.assign(Error(e.value.message),e.value);throw e.value}}]]);function d(e){"MessagePort"===e.constructor.name&&e.close()}function f(e,t){let n=new Map;return e.addEventListener("message",function(e){let{data:t}=e;if(!t||!t.id)return;let r=n.get(t.id);if(r)try{r(t)}finally{n.delete(t.id)}}),function e(t,n,r=[],a=function(){}){let l,s=!1,u=new Proxy(a,{get(a,o){if(p(s),o===i)return()=>{g&&g.unregister(u),m(t),n.clear(),s=!0};if("then"===o){if(0===r.length)return{then:()=>u};let e=w(t,n,{type:"GET",path:r.map(e=>e.toString())}).then(k);return e.then.bind(e)}return e(t,n,[...r,o])},set(e,a,o){p(s);let[i,l]=v(o);return w(t,n,{type:"SET",path:[...r,a].map(e=>e.toString()),value:i},l).then(k)},apply(a,i,l){p(s);let u=r[r.length-1];if(u===o)return w(t,n,{type:"ENDPOINT"}).then(k);if("bind"===u)return e(t,n,r.slice(0,-1));let[c,d]=b(l);return w(t,n,{type:"APPLY",path:r.map(e=>e.toString()),argumentList:c},d).then(k)},construct(e,a){p(s);let[o,i]=b(a);return w(t,n,{type:"CONSTRUCT",path:r.map(e=>e.toString()),argumentList:o},i).then(k)}});return l=(h.get(t)||0)+1,h.set(t,l),g&&g.register(u,t,u),u}(e,n,[],t)}function p(e){if(e)throw Error("Proxy has been released and is not useable")}function m(e){return w(e,new Map,{type:"RELEASE"}).then(()=>{d(e)})}let h=new WeakMap,g="FinalizationRegistry"in globalThis&&new FinalizationRegistry(e=>{let t=(h.get(e)||0)-1;h.set(e,t),0===t&&m(e)});function b(e){var t;let n=e.map(v);return[n.map(e=>e[0]),(t=n.map(e=>e[1]),Array.prototype.concat.apply([],t))]}let y=new WeakMap;function v(e){for(let[t,n]of c)if(n.canHandle(e)){let[r,a]=n.serialize(e);return[{type:"HANDLER",name:t,value:r},a]}return[{type:"RAW",value:e},y.get(e)||[]]}function k(e){switch(e.type){case"HANDLER":return c.get(e.name).deserialize(e.value);case"RAW":return e.value}}function w(e,t,n,r){return new Promise(a=>{let o=[,,,,].fill(0).map(()=>Math.floor(Math.random()*Number.MAX_SAFE_INTEGER).toString(16)).join("-");t.set(o,a),e.start&&e.start(),e.postMessage(Object.assign({id:o},n),r)})}function x(){return r||(r=(async()=>{let e=f(new Worker(new URL(n.p+n.u("9618"),n.b)));return await new e})()),r}async function S(e,t){{let n=await x();await n.fetchIndexes(e,t)}}async function E(e,t,n,r){return(await x()).search(e,t,n,r)}},4471(e,t,n){"use strict";function r(e){return e.join(" \u203A ")}n.d(t,{$:()=>r})},3103(e,t,n){"use strict";function r(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}n.d(t,{Z:()=>r})},7674(e,t,n){"use strict";function r(e,t){let n=[];for(let r of Object.values(e))r[t]&&n.push(...r[t].position);return n.sort((e,t)=>e[0]-t[0]||t[1]-e[1])}n.d(t,{g:()=>r})},6841(e,t,n){"use strict";n.d(t,{Z:()=>function e(t,n,a){let o=[];for(let a of n){let i=t.toLowerCase().indexOf(a);if(i>=0){i>0&&o.push(e(t.substr(0,i),n)),o.push(`${(0,r.Z)(t.substr(i,a.length))}`);let l=i+a.length;l${(0,r.Z)(t)}`:(0,r.Z)(t):o.join("")}});var r=n(3103)},8677(e,t,n){"use strict";n.d(t,{C:()=>l});var r=n(3103),a=n(6841);let o=/\w+|\p{Unified_Ideograph}/u;function i(e){let t=[],n=0,r=e;for(;r.length>0;){let a=r.match(o);if(!a){t.push(r);break}a.index>0&&t.push(r.substring(0,a.index)),t.push(a[0]),n+=a.index+a[0].length,r=e.substring(n)}return t}function l(e,t,n,o=50){let{chunkIndex:s,chunks:u}=function(e,t,n){let o=[],l=0,s=0,u=-1;for(;ls)for(let t of i(e.substring(s,c)).map(e=>({html:(0,r.Z)(e),textLength:e.length})))o.push(t);-1===u&&(u=o.length),s=c+d,o.push({html:(0,a.Z)(e.substring(c,s),n,!0),textLength:d})}}if(s({html:(0,r.Z)(e),textLength:e.length})))o.push(t);return{chunkIndex:u,chunks:o}}(e,t,n),c=u.slice(0,s),d=u[s],f=[d.html],p=u.slice(s+1),m=d.textLength,h=0,g=0,b=!1,y=!1;for(;m0){let e=c.pop();m+e.textLength<=o?(f.unshift(e.html),h+=e.textLength,m+=e.textLength):(b=!0,c.length=0)}else if(p.length>0){let e=p.shift();m+e.textLength<=o?(f.push(e.html),g+=e.textLength,m+=e.textLength):(y=!0,p.length=0)}else break;return(b||c.length>0)&&f.unshift("\u2026"),(y||p.length>0)&&f.push("\u2026"),f.join("")}},3385(e,t,n){"use strict";function r(e,t){if("string"==typeof e)return{label:e,path:e};{let{label:n,path:r}=e;return"string"==typeof n?{label:n,path:r}:Object.prototype.hasOwnProperty.call(n,t)?{label:n[t],path:r}:{label:r,path:r}}}n.d(t,{p:()=>r})},9913(e,t,n){"use strict";var r,a;n.d(t,{i:()=>r}),(a=r||(r={}))[a.Title=0]="Title",a[a.Heading=1]="Heading",a[a.Description=2]="Description",a[a.Keywords=3]="Keywords",a[a.Content=4]="Content"},3328(e,t,n){"use strict";n.d(t,{AO:()=>f,yJ:()=>p,sC:()=>T,TM:()=>A,zR:()=>k});var r=n(8168);function a(e){return"/"===e.charAt(0)}function o(e,t){for(var n=t,r=n+1,a=e.length;r=0;f--){var p=i[f];"."===p?o(i,f):".."===p?(o(i,f),d++):d&&(o(i,f),d--)}if(!u)for(;d--;)i.unshift("..");!u||""===i[0]||i[0]&&a(i[0])||i.unshift("");var m=i.join("/");return n&&"/"!==m.substr(-1)&&(m+="/"),m};var l=n(1561);function s(e){return"/"===e.charAt(0)?e:"/"+e}function u(e){return"/"===e.charAt(0)?e.substr(1):e}function c(e,t){return 0===e.toLowerCase().indexOf(t.toLowerCase())&&-1!=="/?#".indexOf(e.charAt(t.length))?e.substr(t.length):e}function d(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e}function f(e){var t=e.pathname,n=e.search,r=e.hash,a=t||"/";return n&&"?"!==n&&(a+="?"===n.charAt(0)?n:"?"+n),r&&"#"!==r&&(a+="#"===r.charAt(0)?r:"#"+r),a}function p(e,t,n,a){var o,l,s,u,c,d;"string"==typeof e?(s="",u="",-1!==(c=(l=e||"/").indexOf("#"))&&(u=l.substr(c),l=l.substr(0,c)),-1!==(d=l.indexOf("?"))&&(s=l.substr(d),l=l.substr(0,d)),(o={pathname:l,search:"?"===s?"":s,hash:"#"===u?"":u}).state=t):(void 0===(o=(0,r.A)({},e)).pathname&&(o.pathname=""),o.search?"?"!==o.search.charAt(0)&&(o.search="?"+o.search):o.search="",o.hash?"#"!==o.hash.charAt(0)&&(o.hash="#"+o.hash):o.hash="",void 0!==t&&void 0===o.state&&(o.state=t));try{o.pathname=decodeURI(o.pathname)}catch(e){if(e instanceof URIError)throw URIError('Pathname "'+o.pathname+'" could not be decoded. This is likely caused by an invalid percent-encoding.');throw e}return n&&(o.key=n),a?o.pathname?"/"!==o.pathname.charAt(0)&&(o.pathname=i(o.pathname,a.pathname)):o.pathname=a.pathname:o.pathname||(o.pathname="/"),o}function m(){var e=null,t=[];return{setPrompt:function(t){return e=t,function(){e===t&&(e=null)}},confirmTransitionTo:function(t,n,r,a){if(null!=e){var o="function"==typeof e?e(t,n):e;"string"==typeof o?"function"==typeof r?r(o,a):a(!0):a(!1!==o)}else a(!0)},appendListener:function(e){var n=!0;function r(){n&&e.apply(void 0,arguments)}return t.push(r),function(){n=!1,t=t.filter(function(e){return e!==r})}},notifyListeners:function(){for(var e=arguments.length,n=Array(e),r=0;rtypeof window&&window.document&&window.document.createElement);function g(e,t){t(window.confirm(e))}var b="popstate",y="hashchange";function v(){try{return window.history.state||{}}catch(e){return{}}}function k(e){void 0===e&&(e={}),h||(0,l.A)(!1);var t,n=window.history,a=(-1===(t=window.navigator.userAgent).indexOf("Android 2.")&&-1===t.indexOf("Android 4.0")||-1===t.indexOf("Mobile Safari")||-1!==t.indexOf("Chrome")||-1!==t.indexOf("Windows Phone"))&&window.history&&"pushState"in window.history,o=-1!==window.navigator.userAgent.indexOf("Trident"),i=e,u=i.forceRefresh,k=void 0!==u&&u,w=i.getUserConfirmation,x=void 0===w?g:w,S=i.keyLength,E=void 0===S?6:S,_=e.basename?d(s(e.basename)):"";function A(e){var t=e||{},n=t.key,r=t.state,a=window.location,o=a.pathname+a.search+a.hash;return _&&(o=c(o,_)),p(o,r,n)}function C(){return Math.random().toString(36).substr(2,E)}var T=m();function P(e){(0,r.A)(B,e),B.length=n.length,T.notifyListeners(B.location,B.action)}function O(e){(void 0!==e.state||-1!==navigator.userAgent.indexOf("CriOS"))&&N(A(e.state))}function L(){N(A(v()))}var j=!1;function N(e){j?(j=!1,P()):T.confirmTransitionTo(e,"POP",x,function(t){var n,r,a,o,i;t?P({action:"POP",location:e}):(n=e,r=B.location,-1===(a=I.indexOf(r.key))&&(a=0),-1===(o=I.indexOf(n.key))&&(o=0),(i=a-o)&&(j=!0,D(i)))})}var R=A(v()),I=[R.key];function F(e){return _+f(e)}function D(e){n.go(e)}var z=0;function M(e){1===(z+=e)&&1===e?(window.addEventListener(b,O),o&&window.addEventListener(y,L)):0===z&&(window.removeEventListener(b,O),o&&window.removeEventListener(y,L))}var $=!1,B={length:n.length,action:"POP",location:R,createHref:F,push:function(e,t){var r="PUSH",o=p(e,t,C(),B.location);T.confirmTransitionTo(o,r,x,function(e){if(e){var t=F(o),i=o.key,l=o.state;if(a)if(n.pushState({key:i,state:l},null,t),k)window.location.href=t;else{var s=I.indexOf(B.location.key),u=I.slice(0,s+1);u.push(o.key),I=u,P({action:r,location:o})}else window.location.href=t}})},replace:function(e,t){var r="REPLACE",o=p(e,t,C(),B.location);T.confirmTransitionTo(o,r,x,function(e){if(e){var t=F(o),i=o.key,l=o.state;if(a)if(n.replaceState({key:i,state:l},null,t),k)window.location.replace(t);else{var s=I.indexOf(B.location.key);-1!==s&&(I[s]=o.key),P({action:r,location:o})}else window.location.replace(t)}})},go:D,goBack:function(){D(-1)},goForward:function(){D(1)},block:function(e){void 0===e&&(e=!1);var t=T.setPrompt(e);return $||(M(1),$=!0),function(){return $&&($=!1,M(-1)),t()}},listen:function(e){var t=T.appendListener(e);return M(1),function(){M(-1),t()}}};return B}var w="hashchange",x={hashbang:{encodePath:function(e){return"!"===e.charAt(0)?e:"!/"+u(e)},decodePath:function(e){return"!"===e.charAt(0)?e.substr(1):e}},noslash:{encodePath:u,decodePath:s},slash:{encodePath:s,decodePath:s}};function S(e){var t=e.indexOf("#");return -1===t?e:e.slice(0,t)}function E(){var e=window.location.href,t=e.indexOf("#");return -1===t?"":e.substring(t+1)}function _(e){window.location.replace(S(window.location.href)+"#"+e)}function A(e){void 0===e&&(e={}),h||(0,l.A)(!1);var t=window.history;window.navigator.userAgent.indexOf("Firefox");var n=e,a=n.getUserConfirmation,o=void 0===a?g:a,i=n.hashType,u=e.basename?d(s(e.basename)):"",b=x[void 0===i?"slash":i],y=b.encodePath,v=b.decodePath;function k(){var e=v(E());return u&&(e=c(e,u)),p(e)}var A=m();function C(e){(0,r.A)(M,e),M.length=t.length,A.notifyListeners(M.location,M.action)}var T=!1,P=null;function O(){var e=E(),t=y(e);if(e!==t)_(t);else{var n,r=k(),a=M.location;if(!T&&a.pathname===r.pathname&&a.search===r.search&&a.hash===r.hash||P===f(r))return;P=null,n=r,T?(T=!1,C()):A.confirmTransitionTo(n,"POP",o,function(e){var t,r,a,o,i;e?C({action:"POP",location:n}):(t=n,r=M.location,-1===(a=R.lastIndexOf(f(r)))&&(a=0),-1===(o=R.lastIndexOf(f(t)))&&(o=0),(i=a-o)&&(T=!0,I(i)))})}}var L=E(),j=y(L);L!==j&&_(j);var N=k(),R=[f(N)];function I(e){t.go(e)}var F=0;function D(e){1===(F+=e)&&1===e?window.addEventListener(w,O):0===F&&window.removeEventListener(w,O)}var z=!1,M={length:t.length,action:"POP",location:N,createHref:function(e){var t=document.querySelector("base"),n="";return t&&t.getAttribute("href")&&(n=S(window.location.href)),n+"#"+y(u+f(e))},push:function(e,t){var n="PUSH",r=p(e,void 0,void 0,M.location);A.confirmTransitionTo(r,n,o,function(e){if(e){var t=f(r),a=y(u+t);if(E()!==a){P=t,window.location.hash=a;var o=R.lastIndexOf(f(M.location)),i=R.slice(0,o+1);i.push(t),R=i,C({action:n,location:r})}else C()}})},replace:function(e,t){var n="REPLACE",r=p(e,void 0,void 0,M.location);A.confirmTransitionTo(r,n,o,function(e){if(e){var t=f(r),a=y(u+t);E()!==a&&(P=t,_(a));var o=R.indexOf(f(M.location));-1!==o&&(R[o]=t),C({action:n,location:r})}})},go:I,goBack:function(){I(-1)},goForward:function(){I(1)},block:function(e){void 0===e&&(e=!1);var t=A.setPrompt(e);return z||(D(1),z=!0),function(){return z&&(z=!1,D(-1)),t()}},listen:function(e){var t=A.appendListener(e);return D(1),function(){D(-1),t()}}};return M}function C(e,t,n){return Math.min(Math.max(e,t),n)}function T(e){void 0===e&&(e={});var t=e,n=t.getUserConfirmation,a=t.initialEntries,o=void 0===a?["/"]:a,i=t.initialIndex,l=t.keyLength,s=void 0===l?6:l,u=m();function c(e){(0,r.A)(y,e),y.length=y.entries.length,u.notifyListeners(y.location,y.action)}function d(){return Math.random().toString(36).substr(2,s)}var h=C(void 0===i?0:i,0,o.length-1),g=o.map(function(e){return"string"==typeof e?p(e,void 0,d()):p(e,void 0,e.key||d())});function b(e){var t=C(y.index+e,0,y.entries.length-1),r=y.entries[t];u.confirmTransitionTo(r,"POP",n,function(e){e?c({action:"POP",location:r,index:t}):c()})}var y={length:g.length,action:"POP",location:g[h],index:h,entries:g,createHref:f,push:function(e,t){var r="PUSH",a=p(e,t,d(),y.location);u.confirmTransitionTo(a,r,n,function(e){if(e){var t=y.index+1,n=y.entries.slice(0);n.length>t?n.splice(t,n.length-t,a):n.push(a),c({action:r,location:a,index:t,entries:n})}})},replace:function(e,t){var r="REPLACE",a=p(e,t,d(),y.location);u.confirmTransitionTo(a,r,n,function(e){e&&(y.entries[y.index]=a,c({action:r,location:a}))})},go:b,goBack:function(){b(-1)},goForward:function(){b(1)},canGo:function(e){var t=y.index+e;return t>=0&&t0){var u=L.utils.clone(t)||{};u.position=[i,s],u.index=a.length,a.push(new L.Token(n.slice(i,o),u))}i=o+1}}return a},L.tokenizer.separator=/[\s\-]+/,L.Pipeline=function(){this._stack=[]},L.Pipeline.registeredFunctions=Object.create(null),L.Pipeline.registerFunction=function(e,t){t in this.registeredFunctions&&L.utils.warn("Overwriting existing registered function: "+t),e.label=t,L.Pipeline.registeredFunctions[e.label]=e},L.Pipeline.warnIfFunctionNotRegistered=function(e){e.label&&e.label in this.registeredFunctions||L.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},L.Pipeline.load=function(e){var t=new L.Pipeline;return e.forEach(function(e){var n=L.Pipeline.registeredFunctions[e];if(n)t.add(n);else throw Error("Cannot load unregistered function: "+e)}),t},L.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){L.Pipeline.warnIfFunctionNotRegistered(e),this._stack.push(e)},this)},L.Pipeline.prototype.after=function(e,t){L.Pipeline.warnIfFunctionNotRegistered(t);var n=this._stack.indexOf(e);if(-1==n)throw Error("Cannot find existingFn");n+=1,this._stack.splice(n,0,t)},L.Pipeline.prototype.before=function(e,t){L.Pipeline.warnIfFunctionNotRegistered(t);var n=this._stack.indexOf(e);if(-1==n)throw Error("Cannot find existingFn");this._stack.splice(n,0,t)},L.Pipeline.prototype.remove=function(e){var t=this._stack.indexOf(e);-1!=t&&this._stack.splice(t,1)},L.Pipeline.prototype.run=function(e){for(var t=this._stack.length,n=0;n1&&(oe&&(n=a),o!=e);)r=n-t,a=t+Math.floor(r/2),o=this.elements[2*a];return o==e||o>e?2*a:ol?u+=2:i==l&&(t+=n[s+1]*r[u+1],s+=2,u+=2);return t},L.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},L.Vector.prototype.toArray=function(){for(var e=Array(this.elements.length/2),t=1,n=0;t0){var o,i=a.str.charAt(0);i in a.node.edges?o=a.node.edges[i]:(o=new L.TokenSet,a.node.edges[i]=o),1==a.str.length&&(o.final=!0),r.push({node:o,editsRemaining:a.editsRemaining,str:a.str.slice(1)})}if(0!=a.editsRemaining){if("*"in a.node.edges)var l=a.node.edges["*"];else{var l=new L.TokenSet;a.node.edges["*"]=l}if(0==a.str.length&&(l.final=!0),r.push({node:l,editsRemaining:a.editsRemaining-1,str:a.str}),a.str.length>1&&r.push({node:a.node,editsRemaining:a.editsRemaining-1,str:a.str.slice(1)}),1==a.str.length&&(a.node.final=!0),a.str.length>=1){if("*"in a.node.edges)var s=a.node.edges["*"];else{var s=new L.TokenSet;a.node.edges["*"]=s}1==a.str.length&&(s.final=!0),r.push({node:s,editsRemaining:a.editsRemaining-1,str:a.str.slice(1)})}if(a.str.length>1){var u,c=a.str.charAt(0),d=a.str.charAt(1);d in a.node.edges?u=a.node.edges[d]:(u=new L.TokenSet,a.node.edges[d]=u),1==a.str.length&&(u.final=!0),r.push({node:u,editsRemaining:a.editsRemaining-1,str:c+a.str.slice(2)})}}}return n},L.TokenSet.fromString=function(e){for(var t=new L.TokenSet,n=t,r=0,a=e.length;r=e;t--){var n=this.uncheckedNodes[t],r=n.child.toString();r in this.minimizedNodes?n.parent.edges[n.char]=this.minimizedNodes[r]:(n.child._str=r,this.minimizedNodes[r]=n.child),this.uncheckedNodes.pop()}},L.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},L.Index.prototype.search=function(e){return this.query(function(t){new L.QueryParser(e,t).parse()})},L.Index.prototype.query=function(e){for(var t=new L.Query(this.fields),n=Object.create(null),r=Object.create(null),a=Object.create(null),o=Object.create(null),i=Object.create(null),l=0;l1?this._b=1:this._b=e},L.Builder.prototype.k1=function(e){this._k1=e},L.Builder.prototype.add=function(e,t){var n=e[this._ref],r=Object.keys(this._fields);this._documents[n]=t||{},this.documentCount+=1;for(var a=0;a=this.length)return L.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},L.QueryLexer.prototype.width=function(){return this.pos-this.start},L.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},L.QueryLexer.prototype.backup=function(){this.pos-=1},L.QueryLexer.prototype.acceptDigitRun=function(){var e,t;do t=(e=this.next()).charCodeAt(0);while(t>47&&t<58)e!=L.QueryLexer.EOS&&this.backup()},L.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(L.QueryLexer.TERM)),e.ignore(),e.more())return L.QueryLexer.lexText},L.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(L.QueryLexer.EDIT_DISTANCE),L.QueryLexer.lexText},L.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(L.QueryLexer.BOOST),L.QueryLexer.lexText},L.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(L.QueryLexer.TERM)},L.QueryLexer.termSeparator=L.tokenizer.separator,L.QueryLexer.lexText=function(e){for(;;){var t=e.next();if(t==L.QueryLexer.EOS)return L.QueryLexer.lexEOS;if(92==t.charCodeAt(0)){e.escapeCharacter();continue}if(":"==t)return L.QueryLexer.lexField;if("~"==t)return e.backup(),e.width()>0&&e.emit(L.QueryLexer.TERM),L.QueryLexer.lexEditDistance;if("^"==t)return e.backup(),e.width()>0&&e.emit(L.QueryLexer.TERM),L.QueryLexer.lexBoost;if("+"==t&&1===e.width()||"-"==t&&1===e.width())return e.emit(L.QueryLexer.PRESENCE),L.QueryLexer.lexText;if(t.match(L.QueryLexer.termSeparator))return L.QueryLexer.lexTerm}},L.QueryParser=function(e,t){this.lexer=new L.QueryLexer(e),this.query=t,this.currentClause={},this.lexemeIdx=0},L.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=L.QueryParser.parseClause;e;)e=e(this);return this.query},L.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},L.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},L.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},L.QueryParser.parseClause=function(e){var t=e.peekLexeme();if(void 0!=t)switch(t.type){case L.QueryLexer.PRESENCE:return L.QueryParser.parsePresence;case L.QueryLexer.FIELD:return L.QueryParser.parseField;case L.QueryLexer.TERM:return L.QueryParser.parseTerm;default:var n="expected either a field or a term, found "+t.type;throw t.str.length>=1&&(n+=" with value '"+t.str+"'"),new L.QueryParseError(n,t.start,t.end)}},L.QueryParser.parsePresence=function(e){var t=e.consumeLexeme();if(void 0!=t){switch(t.str){case"-":e.currentClause.presence=L.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=L.Query.presence.REQUIRED;break;default:var n="unrecognised presence operator'"+t.str+"'";throw new L.QueryParseError(n,t.start,t.end)}var r=e.peekLexeme();if(void 0==r){var n="expecting term or field, found nothing";throw new L.QueryParseError(n,t.start,t.end)}switch(r.type){case L.QueryLexer.FIELD:return L.QueryParser.parseField;case L.QueryLexer.TERM:return L.QueryParser.parseTerm;default:var n="expecting term or field, found '"+r.type+"'";throw new L.QueryParseError(n,r.start,r.end)}}},L.QueryParser.parseField=function(e){var t=e.consumeLexeme();if(void 0!=t){if(-1==e.query.allFields.indexOf(t.str)){var n=e.query.allFields.map(function(e){return"'"+e+"'"}).join(", "),r="unrecognised field '"+t.str+"', possible fields: "+n;throw new L.QueryParseError(r,t.start,t.end)}e.currentClause.fields=[t.str];var a=e.peekLexeme();if(void 0==a){var r="expecting term, found nothing";throw new L.QueryParseError(r,t.start,t.end)}if(a.type===L.QueryLexer.TERM)return L.QueryParser.parseTerm;var r="expecting term, found '"+a.type+"'";throw new L.QueryParseError(r,a.start,a.end)}},L.QueryParser.parseTerm=function(e){var t=e.consumeLexeme();if(void 0!=t){e.currentClause.term=t.str.toLowerCase(),-1!=t.str.indexOf("*")&&(e.currentClause.usePipeline=!1);var n=e.peekLexeme();if(void 0==n)return void e.nextClause();switch(n.type){case L.QueryLexer.TERM:return e.nextClause(),L.QueryParser.parseTerm;case L.QueryLexer.FIELD:return e.nextClause(),L.QueryParser.parseField;case L.QueryLexer.EDIT_DISTANCE:return L.QueryParser.parseEditDistance;case L.QueryLexer.BOOST:return L.QueryParser.parseBoost;case L.QueryLexer.PRESENCE:return e.nextClause(),L.QueryParser.parsePresence;default:var r="Unexpected lexeme type '"+n.type+"'";throw new L.QueryParseError(r,n.start,n.end)}}},L.QueryParser.parseEditDistance=function(e){var t=e.consumeLexeme();if(void 0!=t){var n=parseInt(t.str,10);if(isNaN(n)){var r="edit distance must be numeric";throw new L.QueryParseError(r,t.start,t.end)}e.currentClause.editDistance=n;var a=e.peekLexeme();if(void 0==a)return void e.nextClause();switch(a.type){case L.QueryLexer.TERM:return e.nextClause(),L.QueryParser.parseTerm;case L.QueryLexer.FIELD:return e.nextClause(),L.QueryParser.parseField;case L.QueryLexer.EDIT_DISTANCE:return L.QueryParser.parseEditDistance;case L.QueryLexer.BOOST:return L.QueryParser.parseBoost;case L.QueryLexer.PRESENCE:return e.nextClause(),L.QueryParser.parsePresence;default:var r="Unexpected lexeme type '"+a.type+"'";throw new L.QueryParseError(r,a.start,a.end)}}},L.QueryParser.parseBoost=function(e){var t=e.consumeLexeme();if(void 0!=t){var n=parseInt(t.str,10);if(isNaN(n)){var r="boost must be numeric";throw new L.QueryParseError(r,t.start,t.end)}e.currentClause.boost=n;var a=e.peekLexeme();if(void 0==a)return void e.nextClause();switch(a.type){case L.QueryLexer.TERM:return e.nextClause(),L.QueryParser.parseTerm;case L.QueryLexer.FIELD:return e.nextClause(),L.QueryParser.parseField;case L.QueryLexer.EDIT_DISTANCE:return L.QueryParser.parseEditDistance;case L.QueryLexer.BOOST:return L.QueryParser.parseBoost;case L.QueryLexer.PRESENCE:return e.nextClause(),L.QueryParser.parsePresence;default:var r="Unexpected lexeme type '"+a.type+"'";throw new L.QueryParseError(r,a.start,a.end)}}},O=function(){return L},"function"==typeof define&&define.amd?define(O):e.exports=O()}()},5947(e){var t;t=function(){var e,t,n,r={};r.version="0.2.0";var a=r.settings={minimum:.08,easing:"ease",positionUsing:"",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,showSpinner:!0,barSelector:'[role="bar"]',spinnerSelector:'[role="spinner"]',parent:"body",template:''};function o(e,t,n){return en?n:e}r.configure=function(e){var t,n;for(t in e)void 0!==(n=e[t])&&e.hasOwnProperty(t)&&(a[t]=n);return this},r.status=null,r.set=function(e){var t=r.isStarted();r.status=1===(e=o(e,a.minimum,1))?null:e;var n=r.render(!t),s=n.querySelector(a.barSelector),u=a.speed,c=a.easing;return n.offsetWidth,i(function(t){var o,i,d,f;""===a.positionUsing&&(a.positionUsing=r.getPositioningCSS()),l(s,(o=e,i=u,d=c,(f="translate3d"===a.positionUsing?{transform:"translate3d("+(-1+o)*100+"%,0,0)"}:"translate"===a.positionUsing?{transform:"translate("+(-1+o)*100+"%,0)"}:{"margin-left":(-1+o)*100+"%"}).transition="all "+i+"ms "+d,f)),1===e?(l(n,{transition:"none",opacity:1}),n.offsetWidth,setTimeout(function(){l(n,{transition:"all "+u+"ms linear",opacity:0}),setTimeout(function(){r.remove(),t()},u)},u)):setTimeout(t,u)}),this},r.isStarted=function(){return"number"==typeof r.status},r.start=function(){r.status||r.set(0);var e=function(){setTimeout(function(){r.status&&(r.trickle(),e())},a.trickleSpeed)};return a.trickle&&e(),this},r.done=function(e){return e||r.status?r.inc(.3+.5*Math.random()).set(1):this},r.inc=function(e){var t=r.status;return t?("number"!=typeof e&&(e=(1-t)*o(Math.random()*t,.1,.95)),t=o(t+e,0,.994),r.set(t)):r.start()},r.trickle=function(){return r.inc(Math.random()*a.trickleRate)},e=0,t=0,r.promise=function(n){return n&&"resolved"!==n.state()&&(0===t&&r.start(),e++,t++,n.always(function(){0==--t?(e=0,r.done()):r.set((e-t)/e)})),this},r.render=function(e){if(r.isRendered())return document.getElementById("nprogress");u(document.documentElement,"nprogress-busy");var t=document.createElement("div");t.id="nprogress",t.innerHTML=a.template;var n,o=t.querySelector(a.barSelector),i=e?"-100":(-1+(r.status||0))*100,s=document.querySelector(a.parent);return l(o,{transition:"all 0 linear",transform:"translate3d("+i+"%,0,0)"}),!a.showSpinner&&(n=t.querySelector(a.spinnerSelector))&&f(n),s!=document.body&&u(s,"nprogress-custom-parent"),s.appendChild(t),t},r.remove=function(){c(document.documentElement,"nprogress-busy"),c(document.querySelector(a.parent),"nprogress-custom-parent");var e=document.getElementById("nprogress");e&&f(e)},r.isRendered=function(){return!!document.getElementById("nprogress")},r.getPositioningCSS=function(){var e=document.body.style,t="WebkitTransform"in e?"Webkit":"MozTransform"in e?"Moz":"msTransform"in e?"ms":"OTransform"in e?"O":"";return t+"Perspective"in e?"translate3d":t+"Transform"in e?"translate":"margin"};var i=(n=[],function(e){n.push(e),1==n.length&&function e(){var t=n.shift();t&&t(e)}()}),l=function(){var e=["Webkit","O","Moz","ms"],t={};function n(n,r,a){var o;r=t[o=(o=r).replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(e,t){return t.toUpperCase()})]||(t[o]=function(t){var n=document.body.style;if(t in n)return t;for(var r,a=e.length,o=t.charAt(0).toUpperCase()+t.slice(1);a--;)if((r=e[a]+o)in n)return r;return t}(o)),n.style[r]=a}return function(e,t){var r,a,o=arguments;if(2==o.length)for(r in t)void 0!==(a=t[r])&&t.hasOwnProperty(r)&&n(e,r,a);else n(e,o[1],o[2])}}();function s(e,t){return("string"==typeof e?e:d(e)).indexOf(" "+t+" ")>=0}function u(e,t){var n=d(e),r=n+t;s(n,t)||(e.className=r.substring(1))}function c(e,t){var n,r=d(e);s(e,t)&&(e.className=(n=r.replace(" "+t+" "," ")).substring(1,n.length-1))}function d(e){return(" "+(e.className||"")+" ").replace(/\s+/gi," ")}function f(e){e&&e.parentNode&&e.parentNode.removeChild(e)}return r},"function"==typeof define&&define.amd?define(t):e.exports=t()},5302(e,t,n){var r=n(4634);e.exports=function e(t,n,a){if(r(n)||(a=n||a,n=[]),a=a||{},t instanceof RegExp){var i,l,s=n,d=t.source.match(/\((?!\?)/g);if(d)for(var f=0;f-1?"[^"+s(c)+"]+?":s(d)+"|(?:(?!"+s(d)+")[^"+s(c)+"])+?")})}return i>=?|&&|\|\||[=!+\-*/%<>^&|]=?|[?~:]/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)[\w-]+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b[\w-]+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?:\.\w+)*(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},parameter:{pattern:/(^|\s)-{1,2}(?:\w+:[+-]?)?\w+(?:\.\w+)*(?=[=\s]|$)/,alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+)\s[\s\S]*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:r},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s[\s\S]*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0,inside:{bash:n}},{pattern:/(^|[^\\](?:\\\\)*)"(?:\\[\s\S]|\$\([^)]+\)|\$(?!\()|`[^`]+`|[^"\\`$])*"/,lookbehind:!0,greedy:!0,inside:r},{pattern:/(^|[^$\\])'[^']*'/,lookbehind:!0,greedy:!0},{pattern:/\$'(?:[^'\\]|\\[\s\S])*'/,greedy:!0,inside:{entity:r.entity}}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:r.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:false|true)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|=[=~]?|!=?|<<[<-]?|[&\d]?>>|\d[<>]&?|[<>][&=]?|&[>&]?|\|[&|]?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}},n.inside=e.languages.bash;for(var a=["comment","function-name","for-or-select","assign-left","parameter","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],o=r.variable[1].inside,i=0;i",unchanged:" ",diff:"!"}).forEach(function(n){var r=t[n],a=[];/^\w+$/.test(n)||a.push(/\w+/.exec(n)[0]),"diff"===n&&a.push("bold"),e.languages.diff[n]={pattern:RegExp("^(?:["+r+"].*(?:\r\n?|\n|(?![\\s\\S])))+","m"),alias:a,inside:{line:{pattern:/(.)(?=[\s\S]).*(?:\r\n?|\n)?/,lookbehind:!0},prefix:{pattern:/[\s\S]/,alias:/\w+/.exec(n)[0]}}}}),Object.defineProperty(e.languages.diff,"PREFIXES",{value:t})},568(){!function(e){var t=/\\[\r\n](?:\s|\\[\r\n]|#.*(?!.))*(?![\s#]|\\[\r\n])/.source,n=/(?:[ \t]+(?![ \t])(?:)?|)/.source.replace(//g,function(){return t}),r=/"(?:[^"\\\r\n]|\\(?:\r\n|[\s\S]))*"|'(?:[^'\\\r\n]|\\(?:\r\n|[\s\S]))*'/.source,a=/--[\w-]+=(?:|(?!["'])(?:[^\s\\]|\\.)+)/.source.replace(//g,function(){return r}),o={pattern:RegExp(r),greedy:!0},i={pattern:/(^[ \t]*)#.*/m,lookbehind:!0,greedy:!0};function l(e,t){return RegExp(e=e.replace(//g,function(){return a}).replace(//g,function(){return n}),t)}e.languages.docker={instruction:{pattern:/(^[ \t]*)(?:ADD|ARG|CMD|COPY|ENTRYPOINT|ENV|EXPOSE|FROM|HEALTHCHECK|LABEL|MAINTAINER|ONBUILD|RUN|SHELL|STOPSIGNAL|USER|VOLUME|WORKDIR)(?=\s)(?:\\.|[^\r\n\\])*(?:\\$(?:\s|#.*$)*(?![\s#])(?:\\.|[^\r\n\\])*)*/im,lookbehind:!0,greedy:!0,inside:{options:{pattern:l(/(^(?:ONBUILD)?\w+)(?:)*/.source,"i"),lookbehind:!0,greedy:!0,inside:{property:{pattern:/(^|\s)--[\w-]+/,lookbehind:!0},string:[o,{pattern:/(=)(?!["'])(?:[^\s\\]|\\.)+/,lookbehind:!0}],operator:/\\$/m,punctuation:/=/}},keyword:[{pattern:l(/(^(?:ONBUILD)?HEALTHCHECK(?:)*)(?:CMD|NONE)\b/.source,"i"),lookbehind:!0,greedy:!0},{pattern:l(/(^(?:ONBUILD)?FROM(?:)*(?!--)[^ \t\\]+)AS/.source,"i"),lookbehind:!0,greedy:!0},{pattern:l(/(^ONBUILD)\w+/.source,"i"),lookbehind:!0,greedy:!0},{pattern:/^\w+/,greedy:!0}],comment:i,string:o,variable:/\$(?:\w+|\{[^{}"'\\]*\})/,operator:/\\$/m}},comment:i},e.languages.dockerfile=e.languages.docker}(Prism)},824(){Prism.languages.ini={comment:{pattern:/(^[ \f\t\v]*)[#;][^\n\r]*/m,lookbehind:!0},section:{pattern:/(^[ \f\t\v]*)\[[^\n\r\]]*\]?/m,lookbehind:!0,inside:{"section-name":{pattern:/(^\[[ \f\t\v]*)[^ \f\t\v\]]+(?:[ \f\t\v]+[^ \f\t\v\]]+)*/,lookbehind:!0,alias:"selector"},punctuation:/\[|\]/}},key:{pattern:/(^[ \f\t\v]*)[^ \f\n\r\t\v=]+(?:[ \f\t\v]+[^ \f\n\r\t\v=]+)*(?=[ \f\t\v]*=)/m,lookbehind:!0,alias:"attr-name"},value:{pattern:/(=[ \f\t\v]*)[^ \f\n\r\t\v]+(?:[ \f\t\v]+[^ \f\n\r\t\v]+)*/,lookbehind:!0,alias:"attr-value",inside:{"inner-value":{pattern:/^("|').+(?=\1$)/,lookbehind:!0}}},punctuation:/=/}},2514(){Prism.languages.json={property:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},Prism.languages.webmanifest=Prism.languages.json},267(){var e,t;t=/("|')(?:\\(?:\r\n?|\n|.)|(?!\1)[^\\\r\n])*\1/,(e=Prism).languages.json5=e.languages.extend("json",{property:[{pattern:RegExp(t.source+"(?=\\s*:)"),greedy:!0},{pattern:/(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/,alias:"unquoted"}],string:{pattern:t,greedy:!0},number:/[+-]?\b(?:NaN|Infinity|0x[a-fA-F\d]+)\b|[+-]?(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[eE][+-]?\d+\b)?/})},9700(){var e=Prism;function t(e,t){return"___"+e.toUpperCase()+t+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(n,r,a,o){if(n.language===r){var i=n.tokenStack=[];n.code=n.code.replace(a,function(e){if("function"==typeof o&&!o(e))return e;for(var a,l=i.length;-1!==n.code.indexOf(a=t(r,l));)++l;return i[l]=e,a}),n.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(n,r){if(n.language===r&&n.tokenStack){n.grammar=e.languages[r];var a=0,o=Object.keys(n.tokenStack);!function i(l){for(var s=0;s=o.length);s++){var u=l[s];if("string"==typeof u||u.content&&"string"==typeof u.content){var c=o[a],d=n.tokenStack[c],f="string"==typeof u?u:u.content,p=t(r,c),m=f.indexOf(p);if(m>-1){++a;var h=f.substring(0,m),g=new e.Token(r,e.tokenize(d,n.grammar),"language-"+r,d),b=f.substring(m+p.length),y=[];h&&y.push.apply(y,i([h])),y.push(g),b&&y.push.apply(y,i([b])),"string"==typeof u?l.splice.apply(l,[s,1].concat(y)):u.content=y}}else u.content&&i(u.content)}return l}(n.tokens)}}}})},9587(){var e,t;(e=Prism).languages.typescript=e.languages.extend("javascript",{"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|type)\s+)(?!keyof\b)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?:\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/,lookbehind:!0,greedy:!0,inside:null},builtin:/\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\b/}),e.languages.typescript.keyword.push(/\b(?:abstract|declare|is|keyof|readonly|require)\b/,/\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/,/\btype\b(?=\s*(?:[\{*]|$))/),delete e.languages.typescript.parameter,delete e.languages.typescript["literal-property"],t=e.languages.extend("typescript",{}),delete t["class-name"],e.languages.typescript["class-name"].inside=t,e.languages.insertBefore("typescript","function",{decorator:{pattern:/@[$\w\xA0-\uFFFF]+/,inside:{at:{pattern:/^@/,alias:"operator"},function:/^[\s\S]+/}},"generic-function":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\s*\()/,greedy:!0,inside:{function:/^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:t}}}}),e.languages.ts=e.languages.typescript},4230(e,t,n){var r={"./prism-bash":"7022","./prism-diff":"7839","./prism-docker":"568","./prism-ini":"824","./prism-json":"2514","./prism-json5":"267","./prism-typescript":"9587"};function a(e){return n(o(e))}function o(e){if(!n.o(r,e)){var t=Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return r[e]}a.keys=function(){return Object.keys(r)},a.resolve=o,e.exports=a,a.id=4230},2694(e,t,n){"use strict";var r=n(6925);function a(){}function o(){}o.resetWarningCache=a,e.exports=function(){function e(e,t,n,a,o,i){if(i!==r){var l=Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw l.name="Invariant Violation",l}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:o,resetWarningCache:a};return n.PropTypes=n,n}},5556(e,t,n){e.exports=n(2694)()},6925(e){"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},1247(e,t,n){"use strict";var r,a=n(9982),o=n(6540),i=n(961);function l(e){var t="https://react.dev/errors/"+e;if(1D||(e.current=F[D],F[D]=null,D--)}function $(e,t){F[++D]=e.current,e.current=t}var B=z(null),U=z(null),H=z(null),Q=z(null);function V(e,t){switch($(H,t),$(U,e),$(B,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?ui(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)e=ul(t=ui(t),e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}M(B),$(B,e)}function W(){M(B),M(U),M(H)}function q(e){null!==e.memoizedState&&$(Q,e);var t=B.current,n=ul(t,e.type);t!==n&&($(U,e),$(B,n))}function G(e){U.current===e&&(M(B),M(U)),Q.current===e&&(M(Q),uK._currentValue=I)}var Y=Object.prototype.hasOwnProperty,K=a.unstable_scheduleCallback,Z=a.unstable_cancelCallback,X=a.unstable_shouldYield,J=a.unstable_requestPaint,ee=a.unstable_now,et=a.unstable_getCurrentPriorityLevel,en=a.unstable_ImmediatePriority,er=a.unstable_UserBlockingPriority,ea=a.unstable_NormalPriority,eo=a.unstable_LowPriority,ei=a.unstable_IdlePriority,el=a.log,es=a.unstable_setDisableYieldValue,eu=null,ec=null;function ed(e){if("function"==typeof el&&es(e),ec&&"function"==typeof ec.setStrictMode)try{ec.setStrictMode(eu,e)}catch(e){}}var ef=Math.clz32?Math.clz32:function(e){return 0==(e>>>=0)?32:31-(ep(e)/em|0)|0},ep=Math.log,em=Math.LN2,eh=256,eg=4194304;function eb(e){var t=42&e;if(0!==t)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return 4194048&e;case 4194304:case 8388608:case 0x1000000:case 0x2000000:return 0x3c00000&e;case 0x4000000:return 0x4000000;case 0x8000000:return 0x8000000;case 0x10000000:return 0x10000000;case 0x20000000:return 0x20000000;case 0x40000000:return 0;default:return e}}function ey(e,t,n){var r=e.pendingLanes;if(0===r)return 0;var a=0,o=e.suspendedLanes,i=e.pingedLanes;e=e.warmLanes;var l=0x7ffffff&r;return 0!==l?0!=(r=l&~o)?a=eb(r):0!=(i&=l)?a=eb(i):n||0!=(n=l&~e)&&(a=eb(n)):0!=(l=r&~o)?a=eb(l):0!==i?a=eb(i):n||0!=(n=r&~e)&&(a=eb(n)),0===a?0:0!==t&&t!==a&&0==(t&o)&&((o=a&-a)>=(n=t&-t)||32===o&&0!=(4194048&n))?t:a}function ev(e,t){return 0==(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)}function ek(){var e=eh;return 0==(4194048&(eh<<=1))&&(eh=256),e}function ew(){var e=eg;return 0==(0x3c00000&(eg<<=1))&&(eg=4194304),e}function ex(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function eS(e,t){e.pendingLanes|=t,0x10000000!==t&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function eE(e,t,n){e.pendingLanes|=t,e.suspendedLanes&=~t;var r=31-ef(t);e.entangledLanes|=t,e.entanglements[r]=0x40000000|e.entanglements[r]|4194090&n}function e_(e,t){var n=e.entangledLanes|=t;for(e=e.entanglements;n;){var r=31-ef(n),a=1<)":-1a||s[r]!==u[a]){var c="\n"+s[r].replace(" at new "," at ");return e.displayName&&c.includes("")&&(c=c.replace("",e.displayName)),c}while(1<=r&&0<=a)break}}}finally{e1=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:"")?e0(n):""}function e4(e){try{var t="";do t+=function(e){switch(e.tag){case 26:case 27:case 5:return e0(e.type);case 16:return e0("Lazy");case 13:return e0("Suspense");case 19:return e0("SuspenseList");case 0:case 15:return e2(e.type,!1);case 11:return e2(e.type.render,!1);case 1:return e2(e.type,!0);case 31:return e0("Activity");default:return""}}(e),e=e.return;while(e)return t}catch(e){return"\nError generating stack: "+e.message+"\n"+e.stack}}function e3(e){switch(typeof e){case"bigint":case"boolean":case"number":case"string":case"undefined":case"object":return e;default:return""}}function e5(e){var t=e.type;return(e=e.nodeName)&&"input"===e.toLowerCase()&&("checkbox"===t||"radio"===t)}function e6(e){e._valueTracker||(e._valueTracker=function(e){var t=e5(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&void 0!==n&&"function"==typeof n.get&&"function"==typeof n.set){var a=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return a.call(this)},set:function(e){r=""+e,o.call(this,e)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(e){r=""+e},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}(e))}function e8(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=e5(e)?e.checked?"true":"false":e.value),(e=r)!==n&&(t.setValue(e),!0)}function e9(e){if(void 0===(e=e||("u">typeof document?document:void 0)))return null;try{return e.activeElement||e.body}catch(t){return e.body}}var e7=/[\n"\\]/g;function te(e){return e.replace(e7,function(e){return"\\"+e.charCodeAt(0).toString(16)+" "})}function tt(e,t,n,r,a,o,i,l){e.name="",null!=i&&"function"!=typeof i&&"symbol"!=typeof i&&"boolean"!=typeof i?e.type=i:e.removeAttribute("type"),null!=t?"number"===i?(0===t&&""===e.value||e.value!=t)&&(e.value=""+e3(t)):e.value!==""+e3(t)&&(e.value=""+e3(t)):"submit"!==i&&"reset"!==i||e.removeAttribute("value"),null!=t?tr(e,i,e3(t)):null!=n?tr(e,i,e3(n)):null!=r&&e.removeAttribute("value"),null==a&&null!=o&&(e.defaultChecked=!!o),null!=a&&(e.checked=a&&"function"!=typeof a&&"symbol"!=typeof a),null!=l&&"function"!=typeof l&&"symbol"!=typeof l&&"boolean"!=typeof l?e.name=""+e3(l):e.removeAttribute("name")}function tn(e,t,n,r,a,o,i,l){if(null!=o&&"function"!=typeof o&&"symbol"!=typeof o&&"boolean"!=typeof o&&(e.type=o),null!=t||null!=n){if(("submit"===o||"reset"===o)&&null==t)return;n=null!=n?""+e3(n):"",t=null!=t?""+e3(t):n,l||t===e.value||(e.value=t),e.defaultValue=t}r="function"!=typeof(r=null!=r?r:a)&&"symbol"!=typeof r&&!!r,e.checked=l?e.checked:!!r,e.defaultChecked=!!r,null!=i&&"function"!=typeof i&&"symbol"!=typeof i&&"boolean"!=typeof i&&(e.name=i)}function tr(e,t,n){"number"===t&&e9(e.ownerDocument)===e||e.defaultValue===""+n||(e.defaultValue=""+n)}function ta(e,t,n,r){if(e=e.options,t){t={};for(var a=0;atypeof window&&void 0!==window.document&&void 0!==window.document.createElement,tE=!1;if(tS)try{var t_={};Object.defineProperty(t_,"passive",{get:function(){tE=!0}}),window.addEventListener("test",t_,t_),window.removeEventListener("test",t_,t_)}catch(e){tE=!1}var tA=null,tC=null,tT=null;function tP(){if(tT)return tT;var e,t,n=tC,r=n.length,a="value"in tA?tA.value:tA.textContent,o=a.length;for(e=0;e=t7),nn=!1;function nr(e,t){switch(e){case"keyup":return -1!==t8.indexOf(t.keyCode);case"keydown":return 229!==t.keyCode;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function na(e){return"object"==typeof(e=e.detail)&&"data"in e?e.data:null}var no=!1,ni={color:!0,date:!0,datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0};function nl(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return"input"===t?!!ni[e.type]:"textarea"===t}function ns(e,t,n,r){tb?ty?ty.push(r):ty=[r]:tb=r,0<(t=s2(t,"onChange")).length&&(n=new t$("onChange","change",null,n,r),e.push({event:n,listeners:t}))}var nu=null,nc=null;function nd(e){sG(e,0)}function nf(e){if(e8(eB(e)))return e}function np(e,t){if("change"===e)return t}var nm=!1;if(tS){if(tS){var nh="oninput"in document;if(!nh){var ng=document.createElement("div");ng.setAttribute("oninput","return;"),nh="function"==typeof ng.oninput}r=nh}else r=!1;nm=r&&(!document.documentMode||9=t)return{node:r,offset:t-e};e=n}e:{for(;r;){if(r.nextSibling){r=r.nextSibling;break e}r=r.parentNode}r=void 0}r=n_(r)}}function nC(e){e=null!=e&&null!=e.ownerDocument&&null!=e.ownerDocument.defaultView?e.ownerDocument.defaultView:window;for(var t=e9(e.document);t instanceof e.HTMLIFrameElement;){try{var n="string"==typeof t.contentWindow.location.href}catch(e){n=!1}if(n)e=t.contentWindow;else break;t=e9(e.document)}return t}function nT(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&("input"===t&&("text"===e.type||"search"===e.type||"tel"===e.type||"url"===e.type||"password"===e.type)||"textarea"===t||"true"===e.contentEditable)}var nP=tS&&"documentMode"in document&&11>=document.documentMode,nO=null,nL=null,nj=null,nN=!1;function nR(e,t,n){var r=n.window===n?n.document:9===n.nodeType?n:n.ownerDocument;nN||null==nO||nO!==e9(r)||(r="selectionStart"in(r=nO)&&nT(r)?{start:r.selectionStart,end:r.selectionEnd}:{anchorNode:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection()).anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset},nj&&nE(nj,r)||(nj=r,0<(r=s2(nL,"onSelect")).length&&(t=new t$("onSelect","select",null,t,n),e.push({event:t,listeners:r}),t.target=nO)))}function nI(e,t){var n={};return n[e.toLowerCase()]=t.toLowerCase(),n["Webkit"+e]="webkit"+t,n["Moz"+e]="moz"+t,n}var nF={animationend:nI("Animation","AnimationEnd"),animationiteration:nI("Animation","AnimationIteration"),animationstart:nI("Animation","AnimationStart"),transitionrun:nI("Transition","TransitionRun"),transitionstart:nI("Transition","TransitionStart"),transitioncancel:nI("Transition","TransitionCancel"),transitionend:nI("Transition","TransitionEnd")},nD={},nz={};function nM(e){if(nD[e])return nD[e];if(!nF[e])return e;var t,n=nF[e];for(t in n)if(n.hasOwnProperty(t)&&t in nz)return nD[e]=n[t];return e}tS&&(nz=document.createElement("div").style,"AnimationEvent"in window||(delete nF.animationend.animation,delete nF.animationiteration.animation,delete nF.animationstart.animation),"TransitionEvent"in window||delete nF.transitionend.transition);var n$=nM("animationend"),nB=nM("animationiteration"),nU=nM("animationstart"),nH=nM("transitionrun"),nQ=nM("transitionstart"),nV=nM("transitioncancel"),nW=nM("transitionend"),nq=new Map,nG="abort auxClick beforeToggle cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel".split(" ");function nY(e,t){nq.set(e,t),eW(t,[e])}nG.push("scrollEnd");var nK=new WeakMap;function nZ(e,t){if("object"==typeof e&&null!==e){var n=nK.get(e);return void 0!==n?n:(t={value:e,source:t,stack:e4(t)},nK.set(e,t),t)}return{value:e,source:t,stack:e4(t)}}var nX=[],nJ=0,n0=0;function n1(){for(var e=nJ,t=n0=nJ=0;t>=i,a-=i,rm=1<<32-ef(t)+a|n<typeof AbortController?AbortController:function(){var e=[],t=this.signal={aborted:!1,addEventListener:function(t,n){e.push(n)}};this.abort=function(){t.aborted=!0,e.forEach(function(e){return e()})}},rq=a.unstable_scheduleCallback,rG=a.unstable_NormalPriority,rY={$$typeof:w,Consumer:null,Provider:null,_currentValue:null,_currentValue2:null,_threadCount:0};function rK(){return{controller:new rW,data:new Map,refCount:0}}function rZ(e){e.refCount--,0===e.refCount&&rq(rG,function(){e.controller.abort()})}var rX=null,rJ=0,r0=0,r1=null;function r2(){if(0==--rJ&&null!==rX){null!==r1&&(r1.status="fulfilled");var e=rX;rX=null,r0=0,r1=null;for(var t=0;to?o:8;var i=N.T,l={};N.T=l,oM(e,!1,t,n);try{var s=a(),u=N.S;if(null!==u&&u(l,s),null!==s&&"object"==typeof s&&"function"==typeof s.then){var c,d,f=(c=[],d={status:"pending",value:null,reason:null,then:function(e){c.push(e)}},s.then(function(){d.status="fulfilled",d.value=r;for(var e=0;em?(h=d,d=null):h=d.sibling;var g=p(a,d,l[m],s);if(null===g){null===d&&(d=h);break}e&&d&&null===g.alternate&&t(a,d),i=o(g,i,m),null===c?u=g:c.sibling=g,c=g,d=h}if(m===l.length)return n(a,d),rx&&rg(a,m),u;if(null===d){for(;mh?(g=m,m=null):g=m.sibling;var v=p(a,m,y.value,u);if(null===v){null===m&&(m=g);break}e&&m&&null===v.alternate&&t(a,m),i=o(v,i,h),null===d?c=v:d.sibling=v,d=v,m=g}if(y.done)return n(a,m),rx&&rg(a,h),c;if(null===m){for(;!y.done;h++,y=s.next())null!==(y=f(a,y.value,u))&&(i=o(y,i,h),null===d?c=y:d.sibling=y,d=y);return rx&&rg(a,h),c}for(m=r(m);!y.done;h++,y=s.next())null!==(y=b(m,a,h,y.value,u))&&(e&&null!==y.alternate&&m.delete(null===y.key?h:y.key),i=o(y,i,h),null===d?c=y:d.sibling=y,d=y);return e&&m.forEach(function(e){return t(a,e)}),rx&&rg(a,h),c}(u,c,d=v.call(d),y)}if("function"==typeof d.then)return s(u,c,oY(d),y);if(d.$$typeof===w)return s(u,c,rQ(u,d),y);oZ(u,d)}return"string"==typeof d&&""!==d||"number"==typeof d||"bigint"==typeof d?(d=""+d,null!==c&&6===c.tag?(n(u,c.sibling),(y=a(c,d)).return=u):(n(u,c),(y=ro(d,u.mode,y)).return=u),i(u=y)):n(u,c)}(s,u,c,d);return oq=null,y}catch(e){if(e===r9||e===ae)throw e;var v=n7(29,e,null,s.mode);return v.lanes=d,v.return=s,v}finally{}}}var o0=oJ(!0),o1=oJ(!1),o2=z(null),o4=null;function o3(e){var t=e.alternate;$(o9,1&o9.current),$(o2,e),null===o4&&(null===t||null!==ak.current?o4=e:null!==t.memoizedState&&(o4=e))}function o5(e){if(22===e.tag){if($(o9,o9.current),$(o2,e),null===o4){var t=e.alternate;null!==t&&null!==t.memoizedState&&(o4=e)}}else o6(e)}function o6(){$(o9,o9.current),$(o2,o2.current)}function o8(e){M(o2),o4===e&&(o4=null),M(o9)}var o9=z(0);function o7(e){for(var t=e;null!==t;){if(13===t.tag){var n=t.memoizedState;if(null!==n&&(null===(n=n.dehydrated)||"$?"===n.data||uy(n)))return t}else if(19===t.tag&&void 0!==t.memoizedProps.revealOrder){if(0!=(128&t.flags))return t}else if(null!==t.child){t.child.return=t,t=t.child;continue}if(t===e)break;for(;null===t.sibling;){if(null===t.return||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}function ie(e,t,n,r){n=null==(n=n(r,t=e.memoizedState))?t:f({},t,n),e.memoizedState=n,0===e.lanes&&(e.updateQueue.baseState=n)}var it={enqueueSetState:function(e,t,n){e=e._reactInternals;var r=l5(),a=ad(r);a.payload=t,null!=n&&(a.callback=n),null!==(t=af(e,a,r))&&(l8(t,e,r),ap(t,e,r))},enqueueReplaceState:function(e,t,n){e=e._reactInternals;var r=l5(),a=ad(r);a.tag=1,a.payload=t,null!=n&&(a.callback=n),null!==(t=af(e,a,r))&&(l8(t,e,r),ap(t,e,r))},enqueueForceUpdate:function(e,t){e=e._reactInternals;var n=l5(),r=ad(n);r.tag=2,null!=t&&(r.callback=t),null!==(t=af(e,r,n))&&(l8(t,e,n),ap(t,e,n))}};function ir(e,t,n,r,a,o,i){return"function"==typeof(e=e.stateNode).shouldComponentUpdate?e.shouldComponentUpdate(r,o,i):!t.prototype||!t.prototype.isPureReactComponent||!nE(n,r)||!nE(a,o)}function ia(e,t,n,r){e=t.state,"function"==typeof t.componentWillReceiveProps&&t.componentWillReceiveProps(n,r),"function"==typeof t.UNSAFE_componentWillReceiveProps&&t.UNSAFE_componentWillReceiveProps(n,r),t.state!==e&&it.enqueueReplaceState(t,t.state,null)}function io(e,t){var n=t;if("ref"in t)for(var r in n={},t)"ref"!==r&&(n[r]=t[r]);if(e=e.defaultProps)for(var a in n===t&&(n=f({},n)),e)void 0===n[a]&&(n[a]=e[a]);return n}var ii="function"==typeof reportError?reportError:function(e){if("object"==typeof window&&"function"==typeof window.ErrorEvent){var t=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:"object"==typeof e&&null!==e&&"string"==typeof e.message?String(e.message):String(e),error:e});if(!window.dispatchEvent(t))return}else if("object"==typeof process&&"function"==typeof process.emit)return void process.emit("uncaughtException",e);console.error(e)};function il(e){ii(e)}function is(e){console.error(e)}function iu(e){ii(e)}function ic(e,t){try{(0,e.onUncaughtError)(t.value,{componentStack:t.stack})}catch(e){setTimeout(function(){throw e})}}function id(e,t,n){try{(0,e.onCaughtError)(n.value,{componentStack:n.stack,errorBoundary:1===t.tag?t.stateNode:null})}catch(e){setTimeout(function(){throw e})}}function ip(e,t,n){return(n=ad(n)).tag=3,n.payload={element:null},n.callback=function(){ic(e,t)},n}function im(e){return(e=ad(e)).tag=3,e}function ih(e,t,n,r){var a=n.type.getDerivedStateFromError;if("function"==typeof a){var o=r.value;e.payload=function(){return a(o)},e.callback=function(){id(t,n,r)}}var i=n.stateNode;null!==i&&"function"==typeof i.componentDidCatch&&(e.callback=function(){id(t,n,r),"function"!=typeof a&&(null===lY?lY=new Set([this]):lY.add(this));var e=r.stack;this.componentDidCatch(r.value,{componentStack:null!==e?e:""})})}var ig=Error(l(461)),ib=!1;function iy(e,t,n,r){t.child=null===e?o1(t,null,n,r):o0(t,e.child,n,r)}function iv(e,t,n,r,a){n=n.render;var o=t.ref;if("ref"in r){var i={};for(var l in r)"ref"!==l&&(i[l]=r[l])}else i=r;return(rU(t),r=az(e,t,n,i,o,a),l=aU(),null===e||ib)?(rx&&l&&ry(t),t.flags|=1,iy(e,t,r,a),t.child):(aH(e,t,a),iM(e,t,a))}function ik(e,t,n,r,a){if(null===e){var o=n.type;return"function"!=typeof o||re(o)||void 0!==o.defaultProps||null!==n.compare?((e=rr(n.type,null,r,t,t.mode,a)).ref=t.ref,e.return=t,t.child=e):(t.tag=15,t.type=o,iw(e,t,o,r,a))}if(o=e.child,!i$(e,a)){var i=o.memoizedProps;if((n=null!==(n=n.compare)?n:nE)(i,r)&&e.ref===t.ref)return iM(e,t,a)}return t.flags|=1,(e=rt(o,r)).ref=t.ref,e.return=t,t.child=e}function iw(e,t,n,r,a){if(null!==e){var o=e.memoizedProps;if(nE(o,r)&&e.ref===t.ref)if(ib=!1,t.pendingProps=r=o,!i$(e,a))return t.lanes=e.lanes,iM(e,t,a);else 0!=(131072&e.flags)&&(ib=!0)}return i_(e,t,n,r,a)}function ix(e,t,n){var r=t.pendingProps,a=r.children,o=null!==e?e.memoizedState:null;if("hidden"===r.mode){if(0!=(128&t.flags)){if(r=null!==o?o.baseLanes|n:n,null!==e){for(o=0,a=t.child=e.child;null!==a;)o=o|a.lanes|a.childLanes,a=a.sibling;t.childLanes=o&~r}else t.childLanes=0,t.child=null;return iS(e,t,r,n)}if(0==(0x20000000&n))return t.lanes=t.childLanes=0x20000000,iS(e,t,null!==o?o.baseLanes|n:n,n);t.memoizedState={baseLanes:0,cachePool:null},null!==e&&r6(t,null!==o?o.cachePool:null),null!==o?ax(t,o):aS(),o5(t)}else null!==o?(r6(t,o.cachePool),ax(t,o),o6(t),t.memoizedState=null):(null!==e&&r6(t,null),aS(),o6(t));return iy(e,t,a,n),t.child}function iS(e,t,n,r){var a=r5();return t.memoizedState={baseLanes:n,cachePool:a=null===a?null:{parent:rY._currentValue,pool:a}},null!==e&&r6(t,null),aS(),o5(t),null!==e&&r$(e,t,r,!0),null}function iE(e,t){var n=t.ref;if(null===n)null!==e&&null!==e.ref&&(t.flags|=4194816);else{if("function"!=typeof n&&"object"!=typeof n)throw Error(l(284));(null===e||e.ref!==n)&&(t.flags|=4194816)}}function i_(e,t,n,r,a){return(rU(t),n=az(e,t,n,r,void 0,a),r=aU(),null===e||ib)?(rx&&r&&ry(t),t.flags|=1,iy(e,t,n,a),t.child):(aH(e,t,a),iM(e,t,a))}function iA(e,t,n,r,a,o){return(rU(t),t.updateQueue=null,n=a$(t,r,n,a),aM(e),r=aU(),null===e||ib)?(rx&&r&&ry(t),t.flags|=1,iy(e,t,n,o),t.child):(aH(e,t,o),iM(e,t,o))}function iC(e,t,n,r,a){if(rU(t),null===t.stateNode){var o=n8,i=n.contextType;"object"==typeof i&&null!==i&&(o=rH(i)),t.memoizedState=null!==(o=new n(r,o)).state&&void 0!==o.state?o.state:null,o.updater=it,t.stateNode=o,o._reactInternals=t,(o=t.stateNode).props=r,o.state=t.memoizedState,o.refs={},au(t),i=n.contextType,o.context="object"==typeof i&&null!==i?rH(i):n8,o.state=t.memoizedState,"function"==typeof(i=n.getDerivedStateFromProps)&&(ie(t,n,i,r),o.state=t.memoizedState),"function"==typeof n.getDerivedStateFromProps||"function"==typeof o.getSnapshotBeforeUpdate||"function"!=typeof o.UNSAFE_componentWillMount&&"function"!=typeof o.componentWillMount||(i=o.state,"function"==typeof o.componentWillMount&&o.componentWillMount(),"function"==typeof o.UNSAFE_componentWillMount&&o.UNSAFE_componentWillMount(),i!==o.state&&it.enqueueReplaceState(o,o.state,null),ab(t,r,o,a),ag(),o.state=t.memoizedState),"function"==typeof o.componentDidMount&&(t.flags|=4194308),r=!0}else if(null===e){o=t.stateNode;var l=t.memoizedProps,s=io(n,l);o.props=s;var u=o.context,c=n.contextType;i=n8,"object"==typeof c&&null!==c&&(i=rH(c));var d=n.getDerivedStateFromProps;c="function"==typeof d||"function"==typeof o.getSnapshotBeforeUpdate,l=t.pendingProps!==l,c||"function"!=typeof o.UNSAFE_componentWillReceiveProps&&"function"!=typeof o.componentWillReceiveProps||(l||u!==i)&&ia(t,o,r,i),as=!1;var f=t.memoizedState;o.state=f,ab(t,r,o,a),ag(),u=t.memoizedState,l||f!==u||as?("function"==typeof d&&(ie(t,n,d,r),u=t.memoizedState),(s=as||ir(t,n,s,r,f,u,i))?(c||"function"!=typeof o.UNSAFE_componentWillMount&&"function"!=typeof o.componentWillMount||("function"==typeof o.componentWillMount&&o.componentWillMount(),"function"==typeof o.UNSAFE_componentWillMount&&o.UNSAFE_componentWillMount()),"function"==typeof o.componentDidMount&&(t.flags|=4194308)):("function"==typeof o.componentDidMount&&(t.flags|=4194308),t.memoizedProps=r,t.memoizedState=u),o.props=r,o.state=u,o.context=i,r=s):("function"==typeof o.componentDidMount&&(t.flags|=4194308),r=!1)}else{o=t.stateNode,ac(e,t),c=io(n,i=t.memoizedProps),o.props=c,d=t.pendingProps,f=o.context,u=n.contextType,s=n8,"object"==typeof u&&null!==u&&(s=rH(u)),(u="function"==typeof(l=n.getDerivedStateFromProps)||"function"==typeof o.getSnapshotBeforeUpdate)||"function"!=typeof o.UNSAFE_componentWillReceiveProps&&"function"!=typeof o.componentWillReceiveProps||(i!==d||f!==s)&&ia(t,o,r,s),as=!1,f=t.memoizedState,o.state=f,ab(t,r,o,a),ag();var p=t.memoizedState;i!==d||f!==p||as||null!==e&&null!==e.dependencies&&rB(e.dependencies)?("function"==typeof l&&(ie(t,n,l,r),p=t.memoizedState),(c=as||ir(t,n,c,r,f,p,s)||null!==e&&null!==e.dependencies&&rB(e.dependencies))?(u||"function"!=typeof o.UNSAFE_componentWillUpdate&&"function"!=typeof o.componentWillUpdate||("function"==typeof o.componentWillUpdate&&o.componentWillUpdate(r,p,s),"function"==typeof o.UNSAFE_componentWillUpdate&&o.UNSAFE_componentWillUpdate(r,p,s)),"function"==typeof o.componentDidUpdate&&(t.flags|=4),"function"==typeof o.getSnapshotBeforeUpdate&&(t.flags|=1024)):("function"!=typeof o.componentDidUpdate||i===e.memoizedProps&&f===e.memoizedState||(t.flags|=4),"function"!=typeof o.getSnapshotBeforeUpdate||i===e.memoizedProps&&f===e.memoizedState||(t.flags|=1024),t.memoizedProps=r,t.memoizedState=p),o.props=r,o.state=p,o.context=s,r=c):("function"!=typeof o.componentDidUpdate||i===e.memoizedProps&&f===e.memoizedState||(t.flags|=4),"function"!=typeof o.getSnapshotBeforeUpdate||i===e.memoizedProps&&f===e.memoizedState||(t.flags|=1024),r=!1)}return o=r,iE(e,t),r=0!=(128&t.flags),o||r?(o=t.stateNode,n=r&&"function"!=typeof n.getDerivedStateFromError?null:o.render(),t.flags|=1,null!==e&&r?(t.child=o0(t,e.child,null,a),t.child=o0(t,null,n,a)):iy(e,t,n,a),t.memoizedState=o.state,e=t.child):e=iM(e,t,a),e}function iT(e,t,n,r){return rO(),t.flags|=256,iy(e,t,n,r),t.child}var iP={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function iO(e){return{baseLanes:e,cachePool:r8()}}function iL(e,t,n){return e=null!==e?e.childLanes&~n:0,t&&(e|=lB),e}function ij(e,t,n){var r,a=t.pendingProps,o=!1,i=0!=(128&t.flags);if((r=i)||(r=(null===e||null!==e.memoizedState)&&0!=(2&o9.current)),r&&(o=!0,t.flags&=-129),r=0!=(32&t.flags),t.flags&=-33,null===e){if(rx){if(o?o3(t):o6(t),rx){var s,u=rw;if(s=u){n:{for(s=u,u=rE;8!==s.nodeType;)if(!u||null===(s=uv(s.nextSibling))){u=null;break n}u=s}null!==u?(t.memoizedState={dehydrated:u,treeContext:null!==rp?{id:rm,overflow:rh}:null,retryLane:0x20000000,hydrationErrors:null},(s=n7(18,null,null,0)).stateNode=u,s.return=t,t.child=s,rk=t,rw=null,s=!0):s=!1}s||rA(t)}if(null!==(u=t.memoizedState)&&null!==(u=u.dehydrated))return uy(u)?t.lanes=32:t.lanes=0x20000000,null;o8(t)}return(u=a.children,a=a.fallback,o)?(o6(t),u=iR({mode:"hidden",children:u},o=t.mode),a=ra(a,o,n,null),u.return=t,a.return=t,u.sibling=a,t.child=u,(o=t.child).memoizedState=iO(n),o.childLanes=iL(e,r,n),t.memoizedState=iP,a):(o3(t),iN(t,u))}if(null!==(s=e.memoizedState)&&null!==(u=s.dehydrated)){if(i)256&t.flags?(o3(t),t.flags&=-257,t=iI(e,t,n)):null!==t.memoizedState?(o6(t),t.child=e.child,t.flags|=128,t=null):(o6(t),o=a.fallback,u=t.mode,a=iR({mode:"visible",children:a.children},u),o=ra(o,u,n,null),o.flags|=2,a.return=t,o.return=t,a.sibling=o,t.child=a,o0(t,e.child,null,n),(a=t.child).memoizedState=iO(n),a.childLanes=iL(e,r,n),t.memoizedState=iP,t=o);else if(o3(t),uy(u)){if(r=u.nextSibling&&u.nextSibling.dataset)var c=r.dgst;r=c,(a=Error(l(419))).stack="",a.digest=r,rj({value:a,source:null,stack:null}),t=iI(e,t,n)}else if(ib||r$(e,t,n,!1),r=0!=(n&e.childLanes),ib||r){if(null!==(r=lT)&&0!==(a=0!=((a=0!=(42&(a=n&-n))?1:eA(a))&(r.suspendedLanes|n))?0:a)&&a!==s.retryLane)throw s.retryLane=a,n3(e,a),l8(r,e,a),ig;"$?"===u.data||sl(),t=iI(e,t,n)}else"$?"===u.data?(t.flags|=192,t.child=e.child,t=null):(e=s.treeContext,rw=uv(u.nextSibling),rk=t,rx=!0,rS=null,rE=!1,null!==e&&(rd[rf++]=rm,rd[rf++]=rh,rd[rf++]=rp,rm=e.id,rh=e.overflow,rp=t),t=iN(t,a.children),t.flags|=4096);return t}return o?(o6(t),o=a.fallback,u=t.mode,c=(s=e.child).sibling,(a=rt(s,{mode:"hidden",children:a.children})).subtreeFlags=0x3e00000&s.subtreeFlags,null!==c?o=rt(c,o):(o=ra(o,u,n,null),o.flags|=2),o.return=t,a.return=t,a.sibling=o,t.child=a,a=o,o=t.child,null===(u=e.child.memoizedState)?u=iO(n):(null!==(s=u.cachePool)?(c=rY._currentValue,s=s.parent!==c?{parent:c,pool:c}:s):s=r8(),u={baseLanes:u.baseLanes|n,cachePool:s}),o.memoizedState=u,o.childLanes=iL(e,r,n),t.memoizedState=iP,a):(o3(t),e=(n=e.child).sibling,(n=rt(n,{mode:"visible",children:a.children})).return=t,n.sibling=null,null!==e&&(null===(r=t.deletions)?(t.deletions=[e],t.flags|=16):r.push(e)),t.child=n,t.memoizedState=null,n)}function iN(e,t){return(t=iR({mode:"visible",children:t},e.mode)).return=e,e.child=t}function iR(e,t){return(e=n7(22,e,null,t)).lanes=0,e.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null},e}function iI(e,t,n){return o0(t,e.child,null,n),e=iN(t,t.pendingProps.children),e.flags|=2,t.memoizedState=null,e}function iF(e,t,n){e.lanes|=t;var r=e.alternate;null!==r&&(r.lanes|=t),rz(e.return,t,n)}function iD(e,t,n,r,a){var o=e.memoizedState;null===o?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:r,tail:n,tailMode:a}:(o.isBackwards=t,o.rendering=null,o.renderingStartTime=0,o.last=r,o.tail=n,o.tailMode=a)}function iz(e,t,n){var r=t.pendingProps,a=r.revealOrder,o=r.tail;if(iy(e,t,r.children,n),0!=(2&(r=o9.current)))r=1&r|2,t.flags|=128;else{if(null!==e&&0!=(128&e.flags))e:for(e=t.child;null!==e;){if(13===e.tag)null!==e.memoizedState&&iF(e,n,t);else if(19===e.tag)iF(e,n,t);else if(null!==e.child){e.child.return=e,e=e.child;continue}if(e===t)break;for(;null===e.sibling;){if(null===e.return||e.return===t)break e;e=e.return}e.sibling.return=e.return,e=e.sibling}r&=1}switch($(o9,r),a){case"forwards":for(a=null,n=t.child;null!==n;)null!==(e=n.alternate)&&null===o7(e)&&(a=n),n=n.sibling;null===(n=a)?(a=t.child,t.child=null):(a=n.sibling,n.sibling=null),iD(t,!1,a,n,o);break;case"backwards":for(n=null,a=t.child,t.child=null;null!==a;){if(null!==(e=a.alternate)&&null===o7(e)){t.child=a;break}e=a.sibling,a.sibling=n,n=a,a=e}iD(t,!0,n,null,o);break;case"together":iD(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function iM(e,t,n){if(null!==e&&(t.dependencies=e.dependencies),lz|=t.lanes,0==(n&t.childLanes)){if(null===e)return null;else if(r$(e,t,n,!1),0==(n&t.childLanes))return null}if(null!==e&&t.child!==e.child)throw Error(l(153));if(null!==t.child){for(n=rt(e=t.child,e.pendingProps),t.child=n,n.return=t;null!==e.sibling;)e=e.sibling,(n=n.sibling=rt(e,e.pendingProps)).return=t;n.sibling=null}return t.child}function i$(e,t){return 0!=(e.lanes&t)||!!(null!==(e=e.dependencies)&&rB(e))}function iB(e,t,n){if(null!==e)if(e.memoizedProps!==t.pendingProps)ib=!0;else{if(!i$(e,n)&&0==(128&t.flags))return ib=!1,function(e,t,n){switch(t.tag){case 3:V(t,t.stateNode.containerInfo),rF(t,rY,e.memoizedState.cache),rO();break;case 27:case 5:q(t);break;case 4:V(t,t.stateNode.containerInfo);break;case 10:rF(t,t.type,t.memoizedProps.value);break;case 13:var r=t.memoizedState;if(null!==r){if(null!==r.dehydrated)return o3(t),t.flags|=128,null;if(0!=(n&t.child.childLanes))return ij(e,t,n);return o3(t),null!==(e=iM(e,t,n))?e.sibling:null}o3(t);break;case 19:var a=0!=(128&e.flags);if((r=0!=(n&t.childLanes))||(r$(e,t,n,!1),r=0!=(n&t.childLanes)),a){if(r)return iz(e,t,n);t.flags|=128}if(null!==(a=t.memoizedState)&&(a.rendering=null,a.tail=null,a.lastEffect=null),$(o9,o9.current),!r)return null;break;case 22:case 23:return t.lanes=0,ix(e,t,n);case 24:rF(t,rY,e.memoizedState.cache)}return iM(e,t,n)}(e,t,n);ib=0!=(131072&e.flags)}else ib=!1,rx&&0!=(1048576&t.flags)&&rb(t,rc,t.index);switch(t.lanes=0,t.tag){case 16:e:{e=t.pendingProps;var r=t.elementType,a=r._init;if(r=a(r._payload),t.type=r,"function"==typeof r)re(r)?(e=io(r,e),t.tag=1,t=iC(null,t,r,e,n)):(t.tag=0,t=i_(null,t,r,e,n));else{if(null!=r){if((a=r.$$typeof)===x){t.tag=11,t=iv(null,t,r,e,n);break e}else if(a===_){t.tag=14,t=ik(null,t,r,e,n);break e}}throw Error(l(306,t=function e(t){if(null==t)return null;if("function"==typeof t)return t.$$typeof===L?null:t.displayName||t.name||null;if("string"==typeof t)return t;switch(t){case g:return"Fragment";case y:return"Profiler";case b:return"StrictMode";case S:return"Suspense";case E:return"SuspenseList";case C:return"Activity"}if("object"==typeof t)switch(t.$$typeof){case h:return"Portal";case w:return(t.displayName||"Context")+".Provider";case k:return(t._context.displayName||"Context")+".Consumer";case x:var n=t.render;return(t=t.displayName)||(t=""!==(t=n.displayName||n.name||"")?"ForwardRef("+t+")":"ForwardRef"),t;case _:return null!==(n=t.displayName||null)?n:e(t.type)||"Memo";case A:n=t._payload,t=t._init;try{return e(t(n))}catch(e){}}return null}(r)||r,""))}}return t;case 0:return i_(e,t,t.type,t.pendingProps,n);case 1:return a=io(r=t.type,t.pendingProps),iC(e,t,r,a,n);case 3:e:{if(V(t,t.stateNode.containerInfo),null===e)throw Error(l(387));r=t.pendingProps;var o=t.memoizedState;a=o.element,ac(e,t),ab(t,r,null,n);var i=t.memoizedState;if(rF(t,rY,r=i.cache),r!==o.cache&&rM(t,[rY],n,!0),ag(),r=i.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:i.cache},t.updateQueue.baseState=o,t.memoizedState=o,256&t.flags){t=iT(e,t,r,n);break e}else if(r!==a){rj(a=nZ(Error(l(424)),t)),t=iT(e,t,r,n);break e}else for(rw=uv((e=9===(e=t.stateNode.containerInfo).nodeType?e.body:"HTML"===e.nodeName?e.ownerDocument.body:e).firstChild),rk=t,rx=!0,rS=null,rE=!0,n=o1(t,null,r,n),t.child=n;n;)n.flags=-3&n.flags|4096,n=n.sibling;else{if(rO(),r===a){t=iM(e,t,n);break e}iy(e,t,r,n)}t=t.child}return t;case 26:return iE(e,t),null===e?(n=uO(t.type,null,t.pendingProps,null))?t.memoizedState=n:rx||(n=t.type,e=t.pendingProps,(r=uo(H.current).createElement(n))[eO]=t,r[eL]=e,un(r,n,e),eH(r),t.stateNode=r):t.memoizedState=uO(t.type,e.memoizedProps,t.pendingProps,e.memoizedState),null;case 27:return q(t),null===e&&rx&&(r=t.stateNode=ux(t.type,t.pendingProps,H.current),rk=t,rE=!0,a=rw,uh(t.type)?(uk=a,rw=uv(r.firstChild)):rw=a),iy(e,t,t.pendingProps.children,n),iE(e,t),null===e&&(t.flags|=4194304),t.child;case 5:return null===e&&rx&&((a=r=rw)&&(null!==(r=function(e,t,n,r){for(;1===e.nodeType;){if(e.nodeName.toLowerCase()!==t.toLowerCase()){if(!r&&("INPUT"!==e.nodeName||"hidden"!==e.type))break}else if(r){if(!e[eD])switch(t){case"meta":if(!e.hasAttribute("itemprop"))break;return e;case"link":if("stylesheet"===(a=e.getAttribute("rel"))&&e.hasAttribute("data-precedence")||a!==n.rel||e.getAttribute("href")!==(null==n.href||""===n.href?null:n.href)||e.getAttribute("crossorigin")!==(null==n.crossOrigin?null:n.crossOrigin)||e.getAttribute("title")!==(null==n.title?null:n.title))break;return e;case"style":if(e.hasAttribute("data-precedence"))break;return e;case"script":if(((a=e.getAttribute("src"))!==(null==n.src?null:n.src)||e.getAttribute("type")!==(null==n.type?null:n.type)||e.getAttribute("crossorigin")!==(null==n.crossOrigin?null:n.crossOrigin))&&a&&e.hasAttribute("async")&&!e.hasAttribute("itemprop"))break;return e;default:return e}}else{if("input"!==t||"hidden"!==e.type)return e;var a=null==n.name?null:""+n.name;if("hidden"===n.type&&e.getAttribute("name")===a)return e}if(null===(e=uv(e.nextSibling)))break}return null}(r,t.type,t.pendingProps,rE))?(t.stateNode=r,rk=t,rw=uv(r.firstChild),rE=!1,a=!0):a=!1),a||rA(t)),q(t),a=t.type,o=t.pendingProps,i=null!==e?e.memoizedProps:null,r=o.children,us(a,o)?r=null:null!==i&&us(a,i)&&(t.flags|=32),null!==t.memoizedState&&(uK._currentValue=a=az(e,t,aB,null,null,n)),iE(e,t),iy(e,t,r,n),t.child;case 6:return null===e&&rx&&((e=n=rw)&&(null!==(n=function(e,t,n){if(""===t)return null;for(;3!==e.nodeType;)if((1!==e.nodeType||"INPUT"!==e.nodeName||"hidden"!==e.type)&&!n||null===(e=uv(e.nextSibling)))return null;return e}(n,t.pendingProps,rE))?(t.stateNode=n,rk=t,rw=null,e=!0):e=!1),e||rA(t)),null;case 13:return ij(e,t,n);case 4:return V(t,t.stateNode.containerInfo),r=t.pendingProps,null===e?t.child=o0(t,null,r,n):iy(e,t,r,n),t.child;case 11:return iv(e,t,t.type,t.pendingProps,n);case 7:return iy(e,t,t.pendingProps,n),t.child;case 8:case 12:return iy(e,t,t.pendingProps.children,n),t.child;case 10:return r=t.pendingProps,rF(t,t.type,r.value),iy(e,t,r.children,n),t.child;case 9:return a=t.type._context,r=t.pendingProps.children,rU(t),r=r(a=rH(a)),t.flags|=1,iy(e,t,r,n),t.child;case 14:return ik(e,t,t.type,t.pendingProps,n);case 15:return iw(e,t,t.type,t.pendingProps,n);case 19:return iz(e,t,n);case 31:return r=t.pendingProps,n=t.mode,r={mode:r.mode,children:r.children},null===e?(n=iR(r,n)).ref=t.ref:(n=rt(e.child,r)).ref=t.ref,t.child=n,n.return=t,t=n;case 22:return ix(e,t,n);case 24:return rU(t),r=rH(rY),null===e?(null===(a=r5())&&(a=lT,o=rK(),a.pooledCache=o,o.refCount++,null!==o&&(a.pooledCacheLanes|=n),a=o),t.memoizedState={parent:r,cache:a},au(t),rF(t,rY,a)):(0!=(e.lanes&n)&&(ac(e,t),ab(t,null,null,n),ag()),a=e.memoizedState,o=t.memoizedState,a.parent!==r?(a={parent:r,cache:r},t.memoizedState=a,0===t.lanes&&(t.memoizedState=t.updateQueue.baseState=a),rF(t,rY,r)):(rF(t,rY,r=o.cache),r!==a.cache&&rM(t,[rY],n,!0))),iy(e,t,t.pendingProps.children,n),t.child;case 29:throw t.pendingProps}throw Error(l(156,t.tag))}function iU(e){e.flags|=4}function iH(e,t){if("stylesheet"!==t.type||0!=(4&t.state.loading))e.flags&=-0x1000001;else if(e.flags|=0x1000000,!uH(t)){if(null!==(t=o2.current)&&((4194048&lO)===lO?null!==o4:(0x3c00000&lO)!==lO&&0==(0x20000000&lO)||t!==o4))throw ao=at,r7;e.flags|=8192}}function iQ(e,t){null!==t&&(e.flags|=4),16384&e.flags&&(t=22!==e.tag?ew():0x20000000,e.lanes|=t,lU|=t)}function iV(e,t){if(!rx)switch(e.tailMode){case"hidden":t=e.tail;for(var n=null;null!==t;)null!==t.alternate&&(n=t),t=t.sibling;null===n?e.tail=null:n.sibling=null;break;case"collapsed":n=e.tail;for(var r=null;null!==n;)null!==n.alternate&&(r=n),n=n.sibling;null===r?t||null===e.tail?e.tail=null:e.tail.sibling=null:r.sibling=null}}function iW(e){var t=null!==e.alternate&&e.alternate.child===e.child,n=0,r=0;if(t)for(var a=e.child;null!==a;)n|=a.lanes|a.childLanes,r|=0x3e00000&a.subtreeFlags,r|=0x3e00000&a.flags,a.return=e,a=a.sibling;else for(a=e.child;null!==a;)n|=a.lanes|a.childLanes,r|=a.subtreeFlags,r|=a.flags,a.return=e,a=a.sibling;return e.subtreeFlags|=r,e.childLanes=n,t}function iq(e,t){switch(rv(t),t.tag){case 3:rD(rY),W();break;case 26:case 27:case 5:G(t);break;case 4:W();break;case 13:o8(t);break;case 19:M(o9);break;case 10:rD(t.type);break;case 22:case 23:o8(t),aE(),null!==e&&M(r3);break;case 24:rD(rY)}}function iG(e,t){try{var n=t.updateQueue,r=null!==n?n.lastEffect:null;if(null!==r){var a=r.next;n=a;do{if((n.tag&e)===e){r=void 0;var o=n.create;n.inst.destroy=r=o()}n=n.next}while(n!==a)}}catch(e){sx(t,t.return,e)}}function iY(e,t,n){try{var r=t.updateQueue,a=null!==r?r.lastEffect:null;if(null!==a){var o=a.next;r=o;do{if((r.tag&e)===e){var i=r.inst,l=i.destroy;if(void 0!==l){i.destroy=void 0,a=t;try{l()}catch(e){sx(a,n,e)}}}r=r.next}while(r!==o)}}catch(e){sx(t,t.return,e)}}function iK(e){var t=e.updateQueue;if(null!==t){var n=e.stateNode;try{av(t,n)}catch(t){sx(e,e.return,t)}}}function iZ(e,t,n){n.props=io(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){sx(e,t,n)}}function iX(e,t){try{var n=e.ref;if(null!==n){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;default:r=e.stateNode}"function"==typeof n?e.refCleanup=n(r):n.current=r}}catch(n){sx(e,t,n)}}function iJ(e,t){var n=e.ref,r=e.refCleanup;if(null!==n)if("function"==typeof r)try{r()}catch(n){sx(e,t,n)}finally{e.refCleanup=null,null!=(e=e.alternate)&&(e.refCleanup=null)}else if("function"==typeof n)try{n(null)}catch(n){sx(e,t,n)}else n.current=null}function i0(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{switch(t){case"button":case"input":case"select":case"textarea":n.autoFocus&&r.focus();break;case"img":n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){sx(e,e.return,t)}}function i1(e,t,n){try{var r=e.stateNode;(function(e,t,n,r){switch(t){case"div":case"span":case"svg":case"path":case"a":case"g":case"p":case"li":break;case"input":var a=null,o=null,i=null,s=null,u=null,c=null,d=null;for(m in n){var f=n[m];if(n.hasOwnProperty(m)&&null!=f)switch(m){case"checked":case"value":break;case"defaultValue":u=f;default:r.hasOwnProperty(m)||ue(e,t,m,null,r,f)}}for(var p in r){var m=r[p];if(f=n[p],r.hasOwnProperty(p)&&(null!=m||null!=f))switch(p){case"type":o=m;break;case"name":a=m;break;case"checked":c=m;break;case"defaultChecked":d=m;break;case"value":i=m;break;case"defaultValue":s=m;break;case"children":case"dangerouslySetInnerHTML":if(null!=m)throw Error(l(137,t));break;default:m!==f&&ue(e,t,p,m,r,f)}}tt(e,i,s,u,c,d,o,a);return;case"select":for(o in m=i=s=p=null,n)if(u=n[o],n.hasOwnProperty(o)&&null!=u)switch(o){case"value":break;case"multiple":m=u;default:r.hasOwnProperty(o)||ue(e,t,o,null,r,u)}for(a in r)if(o=r[a],u=n[a],r.hasOwnProperty(a)&&(null!=o||null!=u))switch(a){case"value":p=o;break;case"defaultValue":s=o;break;case"multiple":i=o;default:o!==u&&ue(e,t,a,o,r,u)}t=s,n=i,r=m,null!=p?ta(e,!!n,p,!1):!!r!=!!n&&(null!=t?ta(e,!!n,t,!0):ta(e,!!n,n?[]:"",!1));return;case"textarea":for(s in m=p=null,n)if(a=n[s],n.hasOwnProperty(s)&&null!=a&&!r.hasOwnProperty(s))switch(s){case"value":case"children":break;default:ue(e,t,s,null,r,a)}for(i in r)if(a=r[i],o=n[i],r.hasOwnProperty(i)&&(null!=a||null!=o))switch(i){case"value":p=a;break;case"defaultValue":m=a;break;case"children":break;case"dangerouslySetInnerHTML":if(null!=a)throw Error(l(91));break;default:a!==o&&ue(e,t,i,a,r,o)}to(e,p,m);return;case"option":for(var h in n)p=n[h],n.hasOwnProperty(h)&&null!=p&&!r.hasOwnProperty(h)&&("selected"===h?e.selected=!1:ue(e,t,h,null,r,p));for(u in r)p=r[u],m=n[u],r.hasOwnProperty(u)&&p!==m&&(null!=p||null!=m)&&("selected"===u?e.selected=p&&"function"!=typeof p&&"symbol"!=typeof p:ue(e,t,u,p,r,m));return;case"img":case"link":case"area":case"base":case"br":case"col":case"embed":case"hr":case"keygen":case"meta":case"param":case"source":case"track":case"wbr":case"menuitem":for(var g in n)p=n[g],n.hasOwnProperty(g)&&null!=p&&!r.hasOwnProperty(g)&&ue(e,t,g,null,r,p);for(c in r)if(p=r[c],m=n[c],r.hasOwnProperty(c)&&p!==m&&(null!=p||null!=m))switch(c){case"children":case"dangerouslySetInnerHTML":if(null!=p)throw Error(l(137,t));break;default:ue(e,t,c,p,r,m)}return;default:if(td(t)){for(var b in n)p=n[b],n.hasOwnProperty(b)&&void 0!==p&&!r.hasOwnProperty(b)&&ut(e,t,b,void 0,r,p);for(d in r)p=r[d],m=n[d],r.hasOwnProperty(d)&&p!==m&&(void 0!==p||void 0!==m)&&ut(e,t,d,p,r,m);return}}for(var y in n)p=n[y],n.hasOwnProperty(y)&&null!=p&&!r.hasOwnProperty(y)&&ue(e,t,y,null,r,p);for(f in r)p=r[f],m=n[f],r.hasOwnProperty(f)&&p!==m&&(null!=p||null!=m)&&ue(e,t,f,p,r,m)})(r,e.type,n,t),r[eL]=t}catch(t){sx(e,e.return,t)}}function i2(e){return 5===e.tag||3===e.tag||26===e.tag||27===e.tag&&uh(e.type)||4===e.tag}function i4(e){e:for(;;){for(;null===e.sibling;){if(null===e.return||i2(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;5!==e.tag&&6!==e.tag&&18!==e.tag;){if(27===e.tag&&uh(e.type)||2&e.flags||null===e.child||4===e.tag)continue e;e.child.return=e,e=e.child}if(!(2&e.flags))return e.stateNode}}function i3(e,t,n){var r=e.tag;if(5===r||6===r)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(4!==r&&(27===r&&uh(e.type)&&(n=e.stateNode),null!==(e=e.child)))for(i3(e,t,n),e=e.sibling;null!==e;)i3(e,t,n),e=e.sibling}function i5(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,a=t.attributes;a.length;)t.removeAttributeNode(a[0]);un(t,r,n),t[eO]=e,t[eL]=n}catch(t){sx(e,e.return,t)}}var i6=!1,i8=!1,i9=!1,i7="function"==typeof WeakSet?WeakSet:Set,le=null;function lt(e,t,n){var r=n.flags;switch(n.tag){case 0:case 11:case 15:lf(e,n),4&r&&iG(5,n);break;case 1:if(lf(e,n),4&r)if(e=n.stateNode,null===t)try{e.componentDidMount()}catch(e){sx(n,n.return,e)}else{var a=io(n.type,t.memoizedProps);t=t.memoizedState;try{e.componentDidUpdate(a,t,e.__reactInternalSnapshotBeforeUpdate)}catch(e){sx(n,n.return,e)}}64&r&&iK(n),512&r&&iX(n,n.return);break;case 3:if(lf(e,n),64&r&&null!==(e=n.updateQueue)){if(t=null,null!==n.child)switch(n.child.tag){case 27:case 5:case 1:t=n.child.stateNode}try{av(e,t)}catch(e){sx(n,n.return,e)}}break;case 27:null===t&&4&r&&i5(n);case 26:case 5:lf(e,n),null===t&&4&r&&i0(n),512&r&&iX(n,n.return);break;case 12:default:lf(e,n);break;case 13:lf(e,n),4&r&&li(e,n),64&r&&null!==(e=n.memoizedState)&&null!==(e=e.dehydrated)&&function(e,t){var n=e.ownerDocument;if("$?"!==e.data||"complete"===n.readyState)t();else{var r=function(){t(),n.removeEventListener("DOMContentLoaded",r)};n.addEventListener("DOMContentLoaded",r),e._reactRetry=r}}(e,n=sA.bind(null,n));break;case 22:if(!(r=null!==n.memoizedState||i6)){t=null!==t&&null!==t.memoizedState||i8,a=i6;var o=i8;i6=r,(i8=t)&&!o?function e(t,n,r){for(r=r&&0!=(8772&n.subtreeFlags),n=n.child;null!==n;){var a=n.alternate,o=t,i=n,l=i.flags;switch(i.tag){case 0:case 11:case 15:e(o,i,r),iG(4,i);break;case 1:if(e(o,i,r),"function"==typeof(o=(a=i).stateNode).componentDidMount)try{o.componentDidMount()}catch(e){sx(a,a.return,e)}if(null!==(o=(a=i).updateQueue)){var s=a.stateNode;try{var u=o.shared.hiddenCallbacks;if(null!==u)for(o.shared.hiddenCallbacks=null,o=0;o title"))),un(o,r,n),o[eO]=e,eH(o),r=o;break e;case"link":var i=uB("link","href",a).get(r+(n.href||""));if(i){for(var s=0;s<\/script>",e=e.removeChild(e.firstChild);break;case"select":e="string"==typeof r.is?a.createElement("select",{is:r.is}):a.createElement("select"),r.multiple?e.multiple=!0:r.size&&(e.size=r.size);break;default:e="string"==typeof r.is?a.createElement(n,{is:r.is}):a.createElement(n)}}e[eO]=t,e[eL]=r;e:for(a=t.child;null!==a;){if(5===a.tag||6===a.tag)e.appendChild(a.stateNode);else if(4!==a.tag&&27!==a.tag&&null!==a.child){a.child.return=a,a=a.child;continue}if(a===t)break;for(;null===a.sibling;){if(null===a.return||a.return===t)break e;a=a.return}a.sibling.return=a.return,a=a.sibling}switch(t.stateNode=e,un(e,n,r),n){case"button":case"input":case"select":case"textarea":e=!!r.autoFocus;break;case"img":e=!0;break;default:e=!1}e&&iU(t)}}return iW(t),t.flags&=-0x1000001,null;case 6:if(e&&null!=t.stateNode)e.memoizedProps!==r&&iU(t);else{if("string"!=typeof r&&null===t.stateNode)throw Error(l(166));if(e=H.current,rP(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,null!==(a=rk))switch(a.tag){case 27:case 5:r=a.memoizedProps}e[eO]=t,(e=!!(e.nodeValue===n||null!==r&&!0===r.suppressHydrationWarning||s9(e.nodeValue,n)))||rA(t)}else(e=uo(e).createTextNode(r))[eO]=t,t.stateNode=e}return iW(t),null;case 13:if(r=t.memoizedState,null===e||null!==e.memoizedState&&null!==e.memoizedState.dehydrated){if(a=rP(t),null!==r&&null!==r.dehydrated){if(null===e){if(!a)throw Error(l(318));if(!(a=null!==(a=t.memoizedState)?a.dehydrated:null))throw Error(l(317));a[eO]=t}else rO(),0==(128&t.flags)&&(t.memoizedState=null),t.flags|=4;iW(t),a=!1}else a=rL(),null!==e&&null!==e.memoizedState&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a){if(256&t.flags)return o8(t),t;return o8(t),null}}if(o8(t),0!=(128&t.flags))return t.lanes=n,t;if(n=null!==r,e=null!==e&&null!==e.memoizedState,n){r=t.child,a=null,null!==r.alternate&&null!==r.alternate.memoizedState&&null!==r.alternate.memoizedState.cachePool&&(a=r.alternate.memoizedState.cachePool.pool);var o=null;null!==r.memoizedState&&null!==r.memoizedState.cachePool&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)}return n!==e&&n&&(t.child.flags|=8192),iQ(t,t.updateQueue),iW(t),null;case 4:return W(),null===e&&sX(t.stateNode.containerInfo),iW(t),null;case 10:return rD(t.type),iW(t),null;case 19:if(M(o9),null===(a=t.memoizedState))return iW(t),null;if(r=0!=(128&t.flags),null===(o=a.rendering))if(r)iV(a,!1);else{if(0!==lD||null!==e&&0!=(128&e.flags))for(e=t.child;null!==e;){if(null!==(o=o7(e))){for(t.flags|=128,iV(a,!1),e=o.updateQueue,t.updateQueue=e,iQ(t,e),t.subtreeFlags=0,e=n,n=t.child;null!==n;)rn(n,e),n=n.sibling;return $(o9,1&o9.current|2),t.child}e=e.sibling}null!==a.tail&&ee()>lq&&(t.flags|=128,r=!0,iV(a,!1),t.lanes=4194304)}else{if(!r)if(null!==(e=o7(o))){if(t.flags|=128,r=!0,e=e.updateQueue,t.updateQueue=e,iQ(t,e),iV(a,!0),null===a.tail&&"hidden"===a.tailMode&&!o.alternate&&!rx)return iW(t),null}else 2*ee()-a.renderingStartTime>lq&&0x20000000!==n&&(t.flags|=128,r=!0,iV(a,!1),t.lanes=4194304);a.isBackwards?(o.sibling=t.child,t.child=o):(null!==(e=a.last)?e.sibling=o:t.child=o,a.last=o)}if(null!==a.tail)return t=a.tail,a.rendering=t,a.tail=t.sibling,a.renderingStartTime=ee(),t.sibling=null,e=o9.current,$(o9,r?1&e|2:1&e),t;return iW(t),null;case 22:case 23:return o8(t),aE(),r=null!==t.memoizedState,null!==e?null!==e.memoizedState!==r&&(t.flags|=8192):r&&(t.flags|=8192),r?0!=(0x20000000&n)&&0==(128&t.flags)&&(iW(t),6&t.subtreeFlags&&(t.flags|=8192)):iW(t),null!==(n=t.updateQueue)&&iQ(t,n.retryQueue),n=null,null!==e&&null!==e.memoizedState&&null!==e.memoizedState.cachePool&&(n=e.memoizedState.cachePool.pool),r=null,null!==t.memoizedState&&null!==t.memoizedState.cachePool&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),null!==e&&M(r3),null;case 24:return n=null,null!==e&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),rD(rY),iW(t),null;case 25:case 30:return null}throw Error(l(156,t.tag))}(t.alternate,t,lF);if(null!==n){lP=n;return}if(null!==(t=t.sibling)){lP=t;return}lP=t=e}while(null!==t)0===lD&&(lD=5)}function sp(e,t){do{var n=function(e,t){switch(rv(t),t.tag){case 1:return 65536&(e=t.flags)?(t.flags=-65537&e|128,t):null;case 3:return rD(rY),W(),0!=(65536&(e=t.flags))&&0==(128&e)?(t.flags=-65537&e|128,t):null;case 26:case 27:case 5:return G(t),null;case 13:if(o8(t),null!==(e=t.memoizedState)&&null!==e.dehydrated){if(null===t.alternate)throw Error(l(340));rO()}return 65536&(e=t.flags)?(t.flags=-65537&e|128,t):null;case 19:return M(o9),null;case 4:return W(),null;case 10:return rD(t.type),null;case 22:case 23:return o8(t),aE(),null!==e&&M(r3),65536&(e=t.flags)?(t.flags=-65537&e|128,t):null;case 24:return rD(rY),null;default:return null}}(e.alternate,e);if(null!==n){n.flags&=32767,lP=n;return}if(null!==(n=e.return)&&(n.flags|=32768,n.subtreeFlags=0,n.deletions=null),!t&&null!==(e=e.sibling)){lP=e;return}lP=e=n}while(null!==e)lD=6,lP=null}function sm(e,t,n,r,a,o,i,s,u){e.cancelPendingCommit=null;do sv();while(0!==lK)if(0!=(6&lC))throw Error(l(327));if(null!==t){if(t===e.current)throw Error(l(177));if(!function(e,t,n,r,a,o){var i=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var l=e.entanglements,s=e.expirationTimes,u=e.hiddenUpdates;for(n=i&~n;0g&&(i=g,g=h,h=i);var b=nA(l,h),y=nA(l,g);if(b&&y&&(1!==p.rangeCount||p.anchorNode!==b.node||p.anchorOffset!==b.offset||p.focusNode!==y.node||p.focusOffset!==y.offset)){var v=d.createRange();v.setStart(b.node,b.offset),p.removeAllRanges(),h>g?(p.addRange(v),p.extend(y.node,y.offset)):(v.setEnd(y.node,y.offset),p.addRange(v))}}}}for(d=[],p=l;p=p.parentNode;)1===p.nodeType&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for("function"==typeof l.focus&&l.focus(),l=0;ln?32:n,N.T=null,n=l1,l1=null;var o=lZ,i=lJ;if(lK=0,lX=lZ=null,lJ=0,0!=(6&lC))throw Error(l(331));var s=lC;if(lC|=4,lS(o.current),lg(o,o.current,i,n),lC=s,sI(0,!1),ec&&"function"==typeof ec.onPostCommitFiberRoot)try{ec.onPostCommitFiberRoot(eu,o)}catch(e){}return!0}finally{R.p=a,N.T=r,sy(e,t)}}function sw(e,t,n){t=nZ(n,t),t=ip(e.stateNode,t,2),null!==(e=af(e,t,2))&&(eS(e,2),sR(e))}function sx(e,t,n){if(3===e.tag)sw(e,e,n);else for(;null!==t;){if(3===t.tag){sw(t,e,n);break}if(1===t.tag){var r=t.stateNode;if("function"==typeof t.type.getDerivedStateFromError||"function"==typeof r.componentDidCatch&&(null===lY||!lY.has(r))){e=nZ(n,e),null!==(r=af(t,n=im(2),2))&&(ih(n,r,t,e),eS(r,2),sR(r));break}}t=t.return}}function sS(e,t,n){var r=e.pingCache;if(null===r){r=e.pingCache=new lA;var a=new Set;r.set(t,a)}else void 0===(a=r.get(t))&&(a=new Set,r.set(t,a));a.has(n)||(lI=!0,a.add(n),e=sE.bind(null,e,t,n),t.then(e,e))}function sE(e,t,n){var r=e.pingCache;null!==r&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,lT===e&&(lO&n)===n&&(4===lD||3===lD&&(0x3c00000&lO)===lO&&300>ee()-lW?0==(2&lC)&&sr(e,0):l$|=n,lU===lO&&(lU=0)),sR(e)}function s_(e,t){0===t&&(t=ew()),null!==(e=n3(e,t))&&(eS(e,t),sR(e))}function sA(e){var t=e.memoizedState,n=0;null!==t&&(n=t.retryLane),s_(e,n)}function sC(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,a=e.memoizedState;null!==a&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(l(314))}null!==r&&r.delete(t),s_(e,n)}var sT=null,sP=null,sO=!1,sL=!1,sj=!1,sN=0;function sR(e){e!==sP&&null===e.next&&(null===sP?sT=sP=e:sP=sP.next=e),sL=!0,sO||(sO=!0,up(function(){0!=(6&lC)?K(en,sF):sD()}))}function sI(e,t){if(!sj&&sL){sj=!0;do for(var n=!1,r=sT;null!==r;){if(!t)if(0!==e){var a=r.pendingLanes;if(0===a)var o=0;else{var i=r.suspendedLanes,l=r.pingedLanes;o=0xc000095&(o=(1<<31-ef(42|e)+1)-1&(a&~(i&~l)))?0xc000095&o|1:o?2|o:0}0!==o&&(n=!0,s$(r,o))}else o=lO,0==(3&(o=ey(r,r===lT?o:0,null!==r.cancelPendingCommit||-1!==r.timeoutHandle)))||ev(r,o)||(n=!0,s$(r,o));r=r.next}while(n)sj=!1}}function sF(){sD()}function sD(){sL=sO=!1;var e,t=0;0!==sN&&(((e=window.event)&&"popstate"===e.type?e===uu||(uu=e,0):(uu=null,1))||(t=sN),sN=0);for(var n=ee(),r=null,a=sT;null!==a;){var o=a.next,i=sz(a,n);0===i?(a.next=null,null===r?sT=o:r.next=o,null===o&&(sP=r)):(r=a,(0!==t||0!=(3&i))&&(sL=!0)),a=o}sI(t,!1)}function sz(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,a=e.expirationTimes,o=-0x3c00001&e.pendingLanes;0r){n=r;var i=e.ownerDocument;if(1&n&&uS(i.documentElement),2&n&&uS(i.body),4&n)for(uS(n=i.head),i=n.firstChild;i;){var l=i.nextSibling,s=i.nodeName;i[eD]||"SCRIPT"===s||"STYLE"===s||"LINK"===s&&"stylesheet"===i.rel.toLowerCase()||n.removeChild(i),i=l}}if(0===a){e.removeChild(o),cy(t);return}a--}else"$"===n||"$?"===n||"$!"===n?a++:r=n.charCodeAt(0)-48;else r=0;n=o}while(n)cy(t)}function ub(e){var t=e.firstChild;for(t&&10===t.nodeType&&(t=t.nextSibling);t;){var n=t;switch(t=t.nextSibling,n.nodeName){case"HTML":case"HEAD":case"BODY":ub(n),ez(n);continue;case"SCRIPT":case"STYLE":continue;case"LINK":if("stylesheet"===n.rel.toLowerCase())continue}e.removeChild(n)}}function uy(e){return"$!"===e.data||"$?"===e.data&&"complete"===e.ownerDocument.readyState}function uv(e){for(;null!=e;e=e.nextSibling){var t=e.nodeType;if(1===t||3===t)break;if(8===t){if("$"===(t=e.data)||"$!"===t||"$?"===t||"F!"===t||"F"===t)break;if("/$"===t)return null}}return e}var uk=null;function uw(e){e=e.previousSibling;for(var t=0;e;){if(8===e.nodeType){var n=e.data;if("$"===n||"$!"===n||"$?"===n){if(0===t)return e;t--}else"/$"===n&&t++}e=e.previousSibling}return null}function ux(e,t,n){switch(t=uo(n),e){case"html":if(!(e=t.documentElement))throw Error(l(452));return e;case"head":if(!(e=t.head))throw Error(l(453));return e;case"body":if(!(e=t.body))throw Error(l(454));return e;default:throw Error(l(451))}}function uS(e){for(var t=e.attributes;t.length;)e.removeAttributeNode(t[0]);ez(e)}var uE=new Map,u_=new Set;function uA(e){return"function"==typeof e.getRootNode?e.getRootNode():9===e.nodeType?e:e.ownerDocument}var uC=R.d;R.d={f:function(){var e=uC.f(),t=st();return e||t},r:function(e){var t=e$(e);null!==t&&5===t.tag&&"form"===t.type?oL(t):uC.r(e)},D:function(e){uC.D(e),uP("dns-prefetch",e,null)},C:function(e,t){uC.C(e,t),uP("preconnect",e,t)},L:function(e,t,n){if(uC.L(e,t,n),uT&&e&&t){var r='link[rel="preload"][as="'+te(t)+'"]';"image"===t&&n&&n.imageSrcSet?(r+='[imagesrcset="'+te(n.imageSrcSet)+'"]',"string"==typeof n.imageSizes&&(r+='[imagesizes="'+te(n.imageSizes)+'"]')):r+='[href="'+te(e)+'"]';var a=r;switch(t){case"style":a=uL(e);break;case"script":a=uR(e)}uE.has(a)||(e=f({rel:"preload",href:"image"===t&&n&&n.imageSrcSet?void 0:e,as:t},n),uE.set(a,e),null!==uT.querySelector(r)||"style"===t&&uT.querySelector(uj(a))||"script"===t&&uT.querySelector(uI(a))||(un(t=uT.createElement("link"),"link",e),eH(t),uT.head.appendChild(t)))}},m:function(e,t){if(uC.m(e,t),uT&&e){var n=t&&"string"==typeof t.as?t.as:"script",r='link[rel="modulepreload"][as="'+te(n)+'"][href="'+te(e)+'"]',a=r;switch(n){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":a=uR(e)}if(!uE.has(a)&&(e=f({rel:"modulepreload",href:e},t),uE.set(a,e),null===uT.querySelector(r))){switch(n){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(uT.querySelector(uI(a)))return}un(n=uT.createElement("link"),"link",e),eH(n),uT.head.appendChild(n)}}},X:function(e,t){if(uC.X(e,t),uT&&e){var n=eU(uT).hoistableScripts,r=uR(e),a=n.get(r);a||((a=uT.querySelector(uI(r)))||(e=f({src:e,async:!0},t),(t=uE.get(r))&&uM(e,t),eH(a=uT.createElement("script")),un(a,"link",e),uT.head.appendChild(a)),a={type:"script",instance:a,count:1,state:null},n.set(r,a))}},S:function(e,t,n){if(uC.S(e,t,n),uT&&e){var r=eU(uT).hoistableStyles,a=uL(e);t=t||"default";var o=r.get(a);if(!o){var i={loading:0,preload:null};if(o=uT.querySelector(uj(a)))i.loading=5;else{e=f({rel:"stylesheet",href:e,"data-precedence":t},n),(n=uE.get(a))&&uz(e,n);var l=o=uT.createElement("link");eH(l),un(l,"link",e),l._p=new Promise(function(e,t){l.onload=e,l.onerror=t}),l.addEventListener("load",function(){i.loading|=1}),l.addEventListener("error",function(){i.loading|=2}),i.loading|=4,uD(o,t,uT)}o={type:"stylesheet",instance:o,count:1,state:i},r.set(a,o)}}},M:function(e,t){if(uC.M(e,t),uT&&e){var n=eU(uT).hoistableScripts,r=uR(e),a=n.get(r);a||((a=uT.querySelector(uI(r)))||(e=f({src:e,async:!0,type:"module"},t),(t=uE.get(r))&&uM(e,t),eH(a=uT.createElement("script")),un(a,"link",e),uT.head.appendChild(a)),a={type:"script",instance:a,count:1,state:null},n.set(r,a))}}};var uT="u" title"):null)}function uH(e){return"stylesheet"!==e.type||0!=(3&e.state.loading)}var uQ=null;function uV(){}function uW(){if(this.count--,0===this.count){if(this.stylesheets)uG(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var uq=null;function uG(e,t){e.stylesheets=null,null!==e.unsuspend&&(e.count++,uq=new Map,t.forEach(uY,e),uq=null,uW.call(e))}function uY(e,t){if(!(4&t.state.loading)){var n=uq.get(e);if(n)var r=n.get(null);else{n=new Map,uq.set(e,n);for(var a=e.querySelectorAll("link[data-precedence],style[data-precedence]"),o=0;otypeof __REACT_DEVTOOLS_GLOBAL_HOOK__){var cx=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(!cx.isDisabled&&cx.supportsFiber)try{eu=cx.inject({bundleType:0,version:"19.1.0",rendererPackageName:"react-dom",currentDispatcherRef:N,reconcilerVersion:"19.1.0"}),ec=cx}catch(e){}}t.createRoot=function(e,t){if(!s(e))throw Error(l(299));var n=!1,r="",a=il,o=is,i=iu,u=null;return null!=t&&(!0===t.unstable_strictMode&&(n=!0),void 0!==t.identifierPrefix&&(r=t.identifierPrefix),void 0!==t.onUncaughtError&&(a=t.onUncaughtError),void 0!==t.onCaughtError&&(o=t.onCaughtError),void 0!==t.onRecoverableError&&(i=t.onRecoverableError),void 0!==t.unstable_transitionCallbacks&&(u=t.unstable_transitionCallbacks)),t=uX(e,1,!1,null,null,n,r,a,o,i,u,null),e[ej]=t.current,sX(e),new cv(t)},t.hydrateRoot=function(e,t,n){if(!s(e))throw Error(l(299));var r=!1,a="",o=il,i=is,u=iu,c=null,d=null;return null!=n&&(!0===n.unstable_strictMode&&(r=!0),void 0!==n.identifierPrefix&&(a=n.identifierPrefix),void 0!==n.onUncaughtError&&(o=n.onUncaughtError),void 0!==n.onCaughtError&&(i=n.onCaughtError),void 0!==n.onRecoverableError&&(u=n.onRecoverableError),void 0!==n.unstable_transitionCallbacks&&(c=n.unstable_transitionCallbacks),void 0!==n.formState&&(d=n.formState)),(t=uX(e,1,!0,t,null!=n?n:null,r,a,o,i,u,c,d)).context=n8,n=t.current,(a=ad(r=eA(r=l5()))).callback=null,af(n,a,r),n=r,t.current.lanes=n,eS(t,n),sR(t),e[ej]=t.current,sX(e),new ck(t)},t.version="19.1.0"},6221(e,t,n){"use strict";var r=n(6540);function a(e){var t="https://react.dev/errors/"+e;if(1typeof __REACT_DEVTOOLS_GLOBAL_HOOK__&&"function"==typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE)try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(e){console.error(e)}}(),e.exports=n(1247)},961(e,t,n){"use strict";!function e(){if("u">typeof __REACT_DEVTOOLS_GLOBAL_HOOK__&&"function"==typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE)try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(e){console.error(e)}}(),e.exports=n(6221)},115(e){var t="u">typeof Element,n="function"==typeof Map,r="function"==typeof Set,a="function"==typeof ArrayBuffer&&!!ArrayBuffer.isView;e.exports=function(e,o){try{return function e(o,i){if(o===i)return!0;if(o&&i&&"object"==typeof o&&"object"==typeof i){var l,s,u,c;if(o.constructor!==i.constructor)return!1;if(Array.isArray(o)){if((l=o.length)!=i.length)return!1;for(s=l;0!=s--;)if(!e(o[s],i[s]))return!1;return!0}if(n&&o instanceof Map&&i instanceof Map){if(o.size!==i.size)return!1;for(c=o.entries();!(s=c.next()).done;)if(!i.has(s.value[0]))return!1;for(c=o.entries();!(s=c.next()).done;)if(!e(s.value[1],i.get(s.value[0])))return!1;return!0}if(r&&o instanceof Set&&i instanceof Set){if(o.size!==i.size)return!1;for(c=o.entries();!(s=c.next()).done;)if(!i.has(s.value[0]))return!1;return!0}if(a&&ArrayBuffer.isView(o)&&ArrayBuffer.isView(i)){if((l=o.length)!=i.length)return!1;for(s=l;0!=s--;)if(o[s]!==i[s])return!1;return!0}if(o.constructor===RegExp)return o.source===i.source&&o.flags===i.flags;if(o.valueOf!==Object.prototype.valueOf&&"function"==typeof o.valueOf&&"function"==typeof i.valueOf)return o.valueOf()===i.valueOf();if(o.toString!==Object.prototype.toString&&"function"==typeof o.toString&&"function"==typeof i.toString)return o.toString()===i.toString();if((l=(u=Object.keys(o)).length)!==Object.keys(i).length)return!1;for(s=l;0!=s--;)if(!Object.prototype.hasOwnProperty.call(i,u[s]))return!1;if(t&&o instanceof Element)return!1;for(s=l;0!=s--;)if(("_owner"!==u[s]&&"__v"!==u[s]&&"__o"!==u[s]||!o.$$typeof)&&!e(o[u[s]],i[u[s]]))return!1;return!0}return o!=o&&i!=i}(e,o)}catch(e){if((e.message||"").match(/stack|recursion/i))return console.warn("react-fast-compare cannot handle circular refs"),!1;throw e}}},545(e,t,n){"use strict";n.d(t,{mg:()=>Z,vd:()=>H});var r=n(6540),a=n(5556),o=n.n(a),i=n(115),l=n.n(i),s=n(311),u=n.n(s),c=n(2833),d=n.n(c);function f(){return(f=Object.assign||function(e){for(var t=1;t=0||(a[n]=e[n]);return a}var g={BASE:"base",BODY:"body",HEAD:"head",HTML:"html",LINK:"link",META:"meta",NOSCRIPT:"noscript",SCRIPT:"script",STYLE:"style",TITLE:"title",FRAGMENT:"Symbol(react.fragment)"},b={rel:["amphtml","canonical","alternate"]},y={type:["application/ld+json"]},v={charset:"",name:["robots","description"],property:["og:type","og:title","og:url","og:image","og:image:alt","og:description","twitter:url","twitter:title","twitter:description","twitter:image","twitter:image:alt","twitter:card","twitter:site"]},k=Object.keys(g).map(function(e){return g[e]}),w={accesskey:"accessKey",charset:"charSet",class:"className",contenteditable:"contentEditable",contextmenu:"contextMenu","http-equiv":"httpEquiv",itemprop:"itemProp",tabindex:"tabIndex"},x=Object.keys(w).reduce(function(e,t){return e[w[t]]=t,e},{}),S=function(e,t){for(var n=e.length-1;n>=0;n-=1){var r=e[n];if(Object.prototype.hasOwnProperty.call(r,t))return r[t]}return null},E=function(e){var t=S(e,g.TITLE),n=S(e,"titleTemplate");if(Array.isArray(t)&&(t=t.join("")),n&&t)return n.replace(/%s/g,function(){return t});var r=S(e,"defaultTitle");return t||r||void 0},_=function(e,t){return t.filter(function(t){return void 0!==t[e]}).map(function(t){return t[e]}).reduce(function(e,t){return f({},e,t)},{})},A=function(e,t,n){var r={};return n.filter(function(t){return!!Array.isArray(t[e])||(void 0!==t[e]&&console&&"function"==typeof console.warn&&console.warn("Helmet: "+e+' should be of type "Array". Instead found type "'+typeof t[e]+'"'),!1)}).map(function(t){return t[e]}).reverse().reduce(function(e,n){var a={};n.filter(function(e){for(var n,o=Object.keys(e),i=0;i/g,">").replace(/"/g,""").replace(/'/g,"'")},N=function(e){return Object.keys(e).reduce(function(t,n){var r=void 0!==e[n]?n+'="'+e[n]+'"':""+n;return t?t+" "+r:r},"")},R=function(e,t){return void 0===t&&(t={}),Object.keys(e).reduce(function(t,n){return t[w[n]||n]=e[n],t},t)},I=function(e,t){return t.map(function(t,n){var a,o=((a={key:n})["data-rh"]=!0,a);return Object.keys(t).forEach(function(e){var n=w[e]||e;"innerHTML"===n||"cssText"===n?o.dangerouslySetInnerHTML={__html:t.innerHTML||t.cssText}:o[n]=t[e]}),r.createElement(e,o)})},F=function(e,t,n){switch(e){case g.TITLE:return{toComponent:function(){var e,n,a,o;return n=t.titleAttributes,(a={key:e=t.title})["data-rh"]=!0,o=R(n,a),[r.createElement(g.TITLE,o,e)]},toString:function(){var r,a,o,i;return r=t.title,a=t.titleAttributes,o=N(a),i=T(r),o?"<"+e+' data-rh="true" '+o+">"+j(i,n)+""+e+">":"<"+e+' data-rh="true">'+j(i,n)+""+e+">"}};case"bodyAttributes":case"htmlAttributes":return{toComponent:function(){return R(t)},toString:function(){return N(t)}};default:return{toComponent:function(){return I(e,t)},toString:function(){return t.reduce(function(t,r){var a=Object.keys(r).filter(function(e){return"innerHTML"!==e&&"cssText"!==e}).reduce(function(e,t){var a=void 0===r[t]?t:t+'="'+j(r[t],n)+'"';return e?e+" "+a:a},""),o=r.innerHTML||r.cssText||"",i=-1===L.indexOf(e);return t+"<"+e+' data-rh="true" '+a+(i?"/>":">"+o+""+e+">")},"")}}}},D=function(e){var t=e.baseTag,n=e.bodyAttributes,r=e.encode,a=e.htmlAttributes,o=e.noscriptTags,i=e.styleTags,l=e.title,s=e.titleAttributes,u=e.linkTags,c=e.metaTags,d=e.scriptTags,f={toComponent:function(){},toString:function(){return""}};if(e.prioritizeSeoTags){var p,m,h,k,w,x,S=(p=e.linkTags,m=e.scriptTags,h=e.encode,k=P(e.metaTags,v),w=P(p,b),x=P(m,y),{priorityMethods:{toComponent:function(){return[].concat(I(g.META,k.priority),I(g.LINK,w.priority),I(g.SCRIPT,x.priority))},toString:function(){return F(g.META,k.priority,h)+" "+F(g.LINK,w.priority,h)+" "+F(g.SCRIPT,x.priority,h)}},metaTags:k.default,linkTags:w.default,scriptTags:x.default});f=S.priorityMethods,u=S.linkTags,c=S.metaTags,d=S.scriptTags}return{priority:f,base:F(g.BASE,t,r),bodyAttributes:F("bodyAttributes",n,r),htmlAttributes:F("htmlAttributes",a,r),link:F(g.LINK,u,r),meta:F(g.META,c,r),noscript:F(g.NOSCRIPT,o,r),script:F(g.SCRIPT,d,r),style:F(g.STYLE,i,r),title:F(g.TITLE,{title:void 0===l?"":l,titleAttributes:s},r)}},z=[],M=function(e,t){var n=this;void 0===t&&(t="u">typeof document),this.instances=[],this.value={setHelmet:function(e){n.context.helmet=e},helmetInstances:{get:function(){return n.canUseDOM?z:n.instances},add:function(e){(n.canUseDOM?z:n.instances).push(e)},remove:function(e){var t=(n.canUseDOM?z:n.instances).indexOf(e);(n.canUseDOM?z:n.instances).splice(t,1)}}},this.context=e,this.canUseDOM=t,t||(e.helmet=D({baseTag:[],bodyAttributes:{},encodeSpecialCharacters:!0,htmlAttributes:{},linkTags:[],metaTags:[],noscriptTags:[],scriptTags:[],styleTags:[],title:"",titleAttributes:{}}))},$=r.createContext({}),B=o().shape({setHelmet:o().func,helmetInstances:o().shape({get:o().func,add:o().func,remove:o().func})}),U="u">typeof document,H=function(e){function t(n){var r;return(r=e.call(this,n)||this).helmetData=new M(r.props.context,t.canUseDOM),r}return p(t,e),t.prototype.render=function(){return r.createElement($.Provider,{value:this.helmetData.value},this.props.children)},t}(r.Component);H.canUseDOM=U,H.propTypes={context:o().shape({helmet:o().shape()}),children:o().node.isRequired},H.defaultProps={context:{}},H.displayName="HelmetProvider";var Q=function(e,t){var n,r=document.head||document.querySelector(g.HEAD),a=r.querySelectorAll(e+"[data-rh]"),o=[].slice.call(a),i=[];return t&&t.length&&t.forEach(function(t){var r=document.createElement(e);for(var a in t)Object.prototype.hasOwnProperty.call(t,a)&&("innerHTML"===a?r.innerHTML=t.innerHTML:"cssText"===a?r.styleSheet?r.styleSheet.cssText=t.cssText:r.appendChild(document.createTextNode(t.cssText)):r.setAttribute(a,void 0===t[a]?"":t[a]));r.setAttribute("data-rh","true"),o.some(function(e,t){return n=t,r.isEqualNode(e)})?o.splice(n,1):i.push(r)}),o.forEach(function(e){return e.parentNode.removeChild(e)}),i.forEach(function(e){return r.appendChild(e)}),{oldTags:o,newTags:i}},V=function(e,t){var n=document.getElementsByTagName(e)[0];if(n){for(var r=n.getAttribute("data-rh"),a=r?r.split(","):[],o=[].concat(a),i=Object.keys(t),l=0;l=0;d-=1)n.removeAttribute(o[d]);a.length===o.length?n.removeAttribute("data-rh"):n.getAttribute("data-rh")!==i.join(",")&&n.setAttribute("data-rh",i.join(","))}},W=function(e,t){var n=e.baseTag,r=e.htmlAttributes,a=e.linkTags,o=e.metaTags,i=e.noscriptTags,l=e.onChangeClientState,s=e.scriptTags,u=e.styleTags,c=e.title,d=e.titleAttributes;V(g.BODY,e.bodyAttributes),V(g.HTML,r),void 0!==c&&document.title!==c&&(document.title=T(c)),V(g.TITLE,d);var f={baseTag:Q(g.BASE,n),linkTags:Q(g.LINK,a),metaTags:Q(g.META,o),noscriptTags:Q(g.NOSCRIPT,i),scriptTags:Q(g.SCRIPT,s),styleTags:Q(g.STYLE,u)},p={},m={};Object.keys(f).forEach(function(e){var t=f[e],n=t.newTags,r=t.oldTags;n.length&&(p[e]=n),r.length&&(m[e]=f[e].oldTags)}),t&&t(),l(e,p,m)},q=null,G=function(e){function t(){for(var t,n=arguments.length,r=Array(n),a=0;a elements are self-closing and can not contain children. Refer to our API for more information.")}},n.flattenArrayTypeChildren=function(e){var t,n=e.child,r=e.arrayTypeChildren;return f({},r,((t={})[n.type]=[].concat(r[n.type]||[],[f({},e.newChildProps,this.mapNestedChildrenToProps(n,e.nestedChildren))]),t))},n.mapObjectTypeChildren=function(e){var t,n,r=e.child,a=e.newProps,o=e.newChildProps,i=e.nestedChildren;switch(r.type){case g.TITLE:return f({},a,((t={})[r.type]=i,t.titleAttributes=f({},o),t));case g.BODY:return f({},a,{bodyAttributes:f({},o)});case g.HTML:return f({},a,{htmlAttributes:f({},o)});default:return f({},a,((n={})[r.type]=f({},o),n))}},n.mapArrayTypeChildrenToProps=function(e,t){var n=f({},t);return Object.keys(e).forEach(function(t){var r;n=f({},n,((r={})[t]=e[t],r))}),n},n.warnOnInvalidChildren=function(e,t){return u()(k.some(function(t){return e.type===t}),"function"==typeof e.type?"You may be attempting to nest components within each other, which is not allowed. Refer to our API for more information.":"Only elements types "+k.join(", ")+" are allowed. Helmet does not support rendering <"+e.type+"> elements. Refer to our API for more information."),u()(!t||"string"==typeof t||Array.isArray(t)&&!t.some(function(e){return"string"!=typeof e}),"Helmet expects a string as a child of <"+e.type+">. Did you forget to wrap your children in braces? ( <"+e.type+">{``}"+e.type+"> ) Refer to our API for more information."),!0},n.mapChildrenToProps=function(e,t){var n=this,a={};return r.Children.forEach(e,function(e){if(e&&e.props){var r=e.props,o=r.children,i=h(r,Y),l=Object.keys(i).reduce(function(e,t){return e[x[t]||t]=i[t],e},{}),s=e.type;switch("symbol"==typeof s?s=s.toString():n.warnOnInvalidChildren(e,o),s){case g.FRAGMENT:t=n.mapChildrenToProps(o,t);break;case g.LINK:case g.META:case g.NOSCRIPT:case g.SCRIPT:case g.STYLE:a=n.flattenArrayTypeChildren({child:e,arrayTypeChildren:a,newChildProps:l,nestedChildren:o});break;default:t=n.mapObjectTypeChildren({child:e,newProps:t,newChildProps:l,nestedChildren:o})}}}),this.mapArrayTypeChildrenToProps(a,t)},n.render=function(){var e=this.props,t=e.children,n=h(e,K),a=f({},n),o=n.helmetData;return t&&(a=this.mapChildrenToProps(t,a)),!o||o instanceof M||(o=new M(o.context,o.instances)),o?r.createElement(G,f({},a,{context:o.value,helmetData:void 0})):r.createElement($.Consumer,null,function(e){return r.createElement(G,f({},a,{context:e}))})},t}(r.Component);Z.propTypes={base:o().object,bodyAttributes:o().object,children:o().oneOfType([o().arrayOf(o().node),o().node]),defaultTitle:o().string,defer:o().bool,encodeSpecialCharacters:o().bool,htmlAttributes:o().object,link:o().arrayOf(o().object),meta:o().arrayOf(o().object),noscript:o().arrayOf(o().object),onChangeClientState:o().func,script:o().arrayOf(o().object),style:o().arrayOf(o().object),title:o().string,titleAttributes:o().object,titleTemplate:o().string,prioritizeSeoTags:o().bool,helmetData:o().object},Z.defaultProps={defer:!0,encodeSpecialCharacters:!0,prioritizeSeoTags:!1},Z.displayName="Helmet"},2799(e,t){"use strict";var n="function"==typeof Symbol&&Symbol.for,r=n?Symbol.for("react.element"):60103,a=n?Symbol.for("react.portal"):60106,o=n?Symbol.for("react.fragment"):60107,i=n?Symbol.for("react.strict_mode"):60108,l=n?Symbol.for("react.profiler"):60114,s=n?Symbol.for("react.provider"):60109,u=n?Symbol.for("react.context"):60110,c=n?Symbol.for("react.async_mode"):60111,d=n?Symbol.for("react.concurrent_mode"):60111,f=n?Symbol.for("react.forward_ref"):60112,p=n?Symbol.for("react.suspense"):60113,m=n?Symbol.for("react.suspense_list"):60120,h=n?Symbol.for("react.memo"):60115,g=n?Symbol.for("react.lazy"):60116,b=n?Symbol.for("react.block"):60121,y=n?Symbol.for("react.fundamental"):60117,v=n?Symbol.for("react.responder"):60118,k=n?Symbol.for("react.scope"):60119;function w(e){if("object"==typeof e&&null!==e){var t=e.$$typeof;switch(t){case r:switch(e=e.type){case c:case d:case o:case l:case i:case p:return e;default:switch(e=e&&e.$$typeof){case u:case f:case g:case h:case s:return e;default:return t}}case a:return t}}}function x(e){return w(e)===d}t.AsyncMode=c,t.ConcurrentMode=d,t.ContextConsumer=u,t.ContextProvider=s,t.Element=r,t.ForwardRef=f,t.Fragment=o,t.Lazy=g,t.Memo=h,t.Portal=a,t.Profiler=l,t.StrictMode=i,t.Suspense=p,t.isAsyncMode=function(e){return x(e)||w(e)===c},t.isConcurrentMode=x,t.isContextConsumer=function(e){return w(e)===u},t.isContextProvider=function(e){return w(e)===s},t.isElement=function(e){return"object"==typeof e&&null!==e&&e.$$typeof===r},t.isForwardRef=function(e){return w(e)===f},t.isFragment=function(e){return w(e)===o},t.isLazy=function(e){return w(e)===g},t.isMemo=function(e){return w(e)===h},t.isPortal=function(e){return w(e)===a},t.isProfiler=function(e){return w(e)===l},t.isStrictMode=function(e){return w(e)===i},t.isSuspense=function(e){return w(e)===p},t.isValidElementType=function(e){return"string"==typeof e||"function"==typeof e||e===o||e===d||e===l||e===i||e===p||e===m||"object"==typeof e&&null!==e&&(e.$$typeof===g||e.$$typeof===h||e.$$typeof===s||e.$$typeof===u||e.$$typeof===f||e.$$typeof===y||e.$$typeof===v||e.$$typeof===k||e.$$typeof===b)},t.typeOf=w},4363(e,t,n){"use strict";e.exports=n(2799)},3259(e,t,n){"use strict";function r(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,e.__proto__=t}function a(e){if(void 0===e)throw ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function o(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(){return(i=Object.assign||function(e){for(var t=1;tfunction e(t,n,a){return void 0===a&&(a=[]),t.some(function(t){var o=t.path?(0,r.B6)(n,t):a.length?a[a.length-1].match:r.Ix.computeRootMatch(n);return o&&(a.push({route:t,match:o}),t.routes&&e(t.routes,n,a)),o}),a},v:()=>i});var r=n(6347),a=n(8168),o=n(6540);function i(e,t,n){return void 0===t&&(t={}),void 0===n&&(n={}),e?o.createElement(r.dO,n,e.map(function(e,n){return o.createElement(r.qh,{key:e.key||n,path:e.path,exact:e.exact,strict:e.strict,render:function(n){return e.render?e.render((0,a.A)({},n,{},t,{route:e})):o.createElement(e.component,(0,a.A)({},n,t,{route:e}))}})})):null}},4625(e,t,n){"use strict";n.d(t,{I9:()=>d,Kd:()=>c,N_:()=>b,k2:()=>k});var r=n(6347),a=n(594),o=n(6540),i=n(3328),l=n(8168),s=n(8587),u=n(1561),c=function(e){function t(){for(var t,n=arguments.length,r=Array(n),a=0;av,Ix:()=>g,W6:()=>S,XZ:()=>h,dO:()=>w,qh:()=>k,zy:()=>E});var r=n(594),a=n(6540),o=n(5556),i=n.n(o),l=(n(3328),n(1561)),s=n(8168),u=n(5302),c=n.n(u);n(4363),n(8587),n(4146);var d="u">typeof globalThis?globalThis:"u">typeof window?window:void 0!==n.g?n.g:{},f=a.createContext||function(e,t){var n,o,l="__create-react-context-"+(d.__global_unique_id__=(d.__global_unique_id__||0)+1)+"__",s=function(e){function n(){for(var t,n,r,a=arguments.length,o=Array(a),i=0;i>>1,a=e[r];if(0>>1;ro(s,n))uo(c,s)?(e[r]=c,e[u]=n,r=u):(e[r]=s,e[l]=n,r=l);else if(uo(c,n))e[r]=c,e[u]=n,r=u;else break}}return t}function o(e,t){var n=e.sortIndex-t.sortIndex;return 0!==n?n:e.id-t.id}if(t.unstable_now=void 0,"object"==typeof performance&&"function"==typeof performance.now){var i,l=performance;t.unstable_now=function(){return l.now()}}else{var s=Date,u=s.now();t.unstable_now=function(){return s.now()-u}}var c=[],d=[],f=1,p=null,m=3,h=!1,g=!1,b=!1,y=!1,v="function"==typeof setTimeout?setTimeout:null,k="function"==typeof clearTimeout?clearTimeout:null,w="u">typeof setImmediate?setImmediate:null;function x(e){for(var t=r(d);null!==t;){if(null===t.callback)a(d);else if(t.startTime<=e)a(d),t.sortIndex=t.expirationTime,n(c,t);else break;t=r(d)}}function S(e){if(b=!1,x(e),!g)if(null!==r(c))g=!0,E||(E=!0,i());else{var t=r(d);null!==t&&j(S,t.startTime-e)}}var E=!1,_=-1,A=5,C=-1;function T(){return!!y||!(t.unstable_now()-Ce&&T());){var l=p.callback;if("function"==typeof l){p.callback=null,m=p.priorityLevel;var s=l(p.expirationTime<=e);if(e=t.unstable_now(),"function"==typeof s){p.callback=s,x(e),n=!0;break t}p===r(c)&&a(c),x(e)}else a(c);p=r(c)}if(null!==p)n=!0;else{var u=r(d);null!==u&&j(S,u.startTime-e),n=!1}}break e}finally{p=null,m=o,h=!1}}}finally{n?i():E=!1}}}if("function"==typeof w)i=function(){w(P)};else if("u">typeof MessageChannel){var O=new MessageChannel,L=O.port2;O.port1.onmessage=P,i=function(){L.postMessage(null)}}else i=function(){v(P,0)};function j(e,n){_=v(function(){e(t.unstable_now())},n)}t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(e){e.callback=null},t.unstable_forceFrameRate=function(e){0>e||125l?(e.sortIndex=o,n(d,e),null===r(c)&&e===r(d)&&(b?(k(_),_=-1):b=!0,j(S,o-l))):(e.sortIndex=s,n(c,e),g||h||(g=!0,E||(E=!0,i()))),e},t.unstable_shouldYield=T,t.unstable_wrapCallback=function(e){var t=m;return function(){var n=m;m=t;try{return e.apply(this,arguments)}finally{m=n}}}},9982(e,t,n){"use strict";e.exports=n(4477)},2833(e){e.exports=function(e,t,n,r){var a=n?n.call(r,e,t):void 0;if(void 0!==a)return!!a;if(e===t)return!0;if("object"!=typeof e||!e||"object"!=typeof t||!t)return!1;var o=Object.keys(e),i=Object.keys(t);if(o.length!==i.length)return!1;for(var l=Object.prototype.hasOwnProperty.bind(t),s=0;sr});let r={future:{v4:{removeLegacyPostBuildHeadAttribute:!0,useCssCascadeLayers:!1},experimental_faster:{ssgWorkerThreads:!0,rspackBundler:!0,rspackPersistentCache:!0,swcJsLoader:!1,swcJsMinimizer:!1,swcHtmlMinimizer:!1,lightningCssMinimizer:!1,mdxCrossCompilerCache:!1},experimental_storage:{type:"localStorage",namespace:!1},experimental_router:"browser"},title:"Multi-Scrobbler",tagline:"Scrobble all the things",favicon:"img/favicon.ico",url:"https://foxxmd.github.io",baseUrl:"/multi-scrobbler/",organizationName:"foxxmd",projectName:"multi-scrobbler",trailingSlash:!0,markdown:{hooks:{onBrokenMarkdownLinks:"warn",onBrokenMarkdownImages:"throw"},format:"mdx",mermaid:!1,emoji:!0,mdx1Compat:{comments:!0,admonitions:!0,headingIds:!0},anchors:{maintainCase:!1}},i18n:{defaultLocale:"en",locales:["en"],path:"i18n",localeConfigs:{}},scripts:[{src:"https://ry.foxxmd.dev/api/script.js",defer:!0,"data-site-id":"3"}],presets:[["classic",{docs:{sidebarPath:"./sidebars.ts",routeBasePath:"/",editUrl:"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/"},blog:!1,theme:{customCss:"./src/css/custom.css"}}]],themes:[["@easyops-cn/docusaurus-search-local",{hashed:!0,indexBlog:!1,docsRouteBasePath:"/"}],"docusaurus-theme-github-codeblock"],plugins:["docusaurus-plugin-sass",["@docusaurus/plugin-client-redirects",{redirects:[{to:"/configuration",from:"/docs/configuration"},{to:"/configuration/kitchensink",from:"/docs/configuration/kitchensink"},{to:"/installation",from:"/docs/installation"},{to:"/quickstart",from:"/docs/quickstart"},{to:"/FAQ",from:"/docs/FAQ"},{to:"/",from:"/docs"}]}]],themeConfig:{image:"img/icon.png",navbar:{title:"Multi-Scrobbler",hideOnScroll:!0,logo:{alt:"Logo",src:"img/icon.svg",height:40},items:[{type:"docSidebar",sidebarId:"tutorialSidebar",position:"left",label:"Docs"},{to:"playground",position:"left",label:"Config Playground"},{href:"https://github.com/foxxmd/multi-scrobbler",label:"GitHub",position:"right"},{href:"https://multi-scrobbler.app",label:"Website",position:"right"}]},footer:{links:[{title:"Docs",items:[{label:"Overview",to:"/"},{label:"Installation",to:"/installation"},{label:"Configuration",to:"/configuration"},{label:"Development",to:"/development/dev-common"}]},{title:"More",items:[{label:"GitHub",href:"https://github.com/foxxmd/multi-scrobbler"},{label:"Website",href:"https://multi-scrobbler.app"}]}],copyright:"Copyright \xa9 2026 Multi-Scrobbler. Built with Docusaurus.",style:"light"},prism:{theme:{plain:{color:"#393A34",backgroundColor:"#f6f8fa"},styles:[{types:["comment","prolog","doctype","cdata"],style:{color:"#999988",fontStyle:"italic"}},{types:["namespace"],style:{opacity:.7}},{types:["string","attr-value"],style:{color:"#e3116c"}},{types:["punctuation","operator"],style:{color:"#393A34"}},{types:["entity","url","symbol","number","boolean","variable","constant","property","regex","inserted"],style:{color:"#36acaa"}},{types:["atrule","keyword","attr-name","selector"],style:{color:"#00a4db"}},{types:["function","deleted","tag"],style:{color:"#d73a49"}},{types:["function-variable"],style:{color:"#6f42c1"}},{types:["tag","selector","keyword"],style:{color:"#00009f"}}]},darkTheme:{plain:{color:"#F8F8F2",backgroundColor:"#282A36"},styles:[{types:["prolog","constant","builtin"],style:{color:"rgb(189, 147, 249)"}},{types:["inserted","function"],style:{color:"rgb(80, 250, 123)"}},{types:["deleted"],style:{color:"rgb(255, 85, 85)"}},{types:["changed"],style:{color:"rgb(255, 184, 108)"}},{types:["punctuation","symbol"],style:{color:"rgb(248, 248, 242)"}},{types:["string","char","tag","selector"],style:{color:"rgb(255, 121, 198)"}},{types:["keyword","variable"],style:{color:"rgb(189, 147, 249)",fontStyle:"italic"}},{types:["comment"],style:{color:"rgb(98, 114, 164)"}},{types:["attr-name"],style:{color:"rgb(241, 250, 140)"}}]},additionalLanguages:["json","json5","typescript","docker","bash","ini","diff"],magicComments:[{className:"theme-code-block-highlighted-line",line:"highlight-next-line",block:{start:"highlight-start",end:"highlight-end"}}]},colorMode:{defaultMode:"dark",disableSwitch:!1,respectPrefersColorScheme:!1},codeblock:{showGithubLink:!0,githubLinkLabel:"View on GitHub",showRunmeLink:!1,runmeLinkLabel:"Checkout via Runme"},docs:{versionPersistence:"localStorage",sidebar:{hideable:!1,autoCollapseCategories:!1}},blog:{sidebar:{groupByYear:!0}},metadata:[],tableOfContents:{minHeadingLevel:2,maxHeadingLevel:3}},headTags:[{tagName:"meta",attributes:{name:"google-site-verification",content:"8hqrPhw4yuoTBM6c3S10aC8L4tu4aIBe8Q-oSP3OES0"}}],baseUrlIssueBanner:!0,onBrokenLinks:"throw",onBrokenAnchors:"warn",onDuplicateRoutes:"warn",staticDirectories:["static"],customFields:{},stylesheets:[],clientModules:[],titleDelimiter:"|",noIndex:!1}},8168(e,t,n){"use strict";function r(){return(r=Object.assign?Object.assign.bind():function(e){for(var t=1;tr})},594(e,t,n){"use strict";function r(e,t){return(r=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e})(e,t)}function a(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,r(e,t)}n.d(t,{A:()=>a})},8587(e,t,n){"use strict";function r(e,t){if(null==e)return{};var n={};for(var r in e)if(({}).hasOwnProperty.call(e,r)){if(-1!==t.indexOf(r))continue;n[r]=e[r]}return n}n.d(t,{A:()=>r})},4164(e,t,n){"use strict";function r(){for(var e,t,n=0,r="",a=arguments.length;nr,A:()=>a});let a=r},1765(e,t,n){"use strict";let r,a,o,i,l;n.d(t,{My:()=>B,f4:()=>ey});var s,u,c,d,f,p,m,h,g,b,y,v,k,w,x,S,E,_,A=n(6540),C=n(4164),T=Object.create,P=Object.defineProperty,O=Object.defineProperties,L=Object.getOwnPropertyDescriptor,j=Object.getOwnPropertyDescriptors,N=Object.getOwnPropertyNames,R=Object.getOwnPropertySymbols,I=Object.getPrototypeOf,F=Object.prototype.hasOwnProperty,D=Object.prototype.propertyIsEnumerable,z=(e,t,n)=>t in e?P(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,M=(e,t)=>{for(var n in t||(t={}))F.call(t,n)&&z(e,n,t[n]);if(R)for(var n of R(t))D.call(t,n)&&z(e,n,t[n]);return e},$=(e,t)=>{var n={};for(var r in e)F.call(e,r)&&0>t.indexOf(r)&&(n[r]=e[r]);if(null!=e&&R)for(var r of R(e))0>t.indexOf(r)&&D.call(e,r)&&(n[r]=e[r]);return n},B=(l=null!=(o=(r={"../../node_modules/.pnpm/prismjs@1.29.0_patch_hash=vrxx3pzkik6jpmgpayxfjunetu/node_modules/prismjs/prism.js"(e,t){var n=function(){var e=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,n={},r={util:{encode:function e(t){return t instanceof a?new a(t.type,e(t.content),t.alias):Array.isArray(t)?t.map(e):t.replace(/&/g,"&").replace(/=c.reach));x+=w.value.length,w=w.next){var S,E=w.value;if(n.length>t.length)return;if(!(E instanceof a)){var _=1;if(b){if(!(S=o(k,x,t,g))||S.index>=t.length)break;var A=S.index,C=S.index+S[0].length,T=x;for(T+=w.value.length;A>=T;)T+=(w=w.next).value.length;if(T-=w.value.length,x=T,w.value instanceof a)continue;for(var P=w;P!==n.tail&&(Tc.reach&&(c.reach=N);var R=w.prev;if(L&&(R=l(n,R,L),x+=L.length),function(e,t,n){for(var r=t.next,a=0;a1){var I={cause:d+","+p,reach:N};e(t,n,i,w.prev,x,I),c&&I.reach>c.reach&&(c.reach=I.reach)}}}}}}(e,u,t,u.head,0),function(e){for(var t=[],n=e.head.next;n!==e.tail;)t.push(n.value),n=n.next;return t}(u)},hooks:{all:{},add:function(e,t){var n=r.hooks.all;n[e]=n[e]||[],n[e].push(t)},run:function(e,t){var n=r.hooks.all[e];if(n&&n.length)for(var a,o=0;a=n[o++];)a(t)}},Token:a};function a(e,t,n,r){this.type=e,this.content=t,this.alias=n,this.length=0|(r||"").length}function o(e,t,n,r){e.lastIndex=t;var a=e.exec(n);if(a&&r&&a[1]){var o=a[1].length;a.index+=o,a[0]=a[0].slice(o)}return a}function i(){var e={value:null,prev:null,next:null},t={value:null,prev:e,next:null};e.next=t,this.head=e,this.tail=t,this.length=0}function l(e,t,n){var r=t.next,a={value:n,prev:t,next:r};return t.next=a,r.prev=a,e.length++,a}return a.stringify=function e(t,n){if("string"==typeof t)return t;if(Array.isArray(t)){var a="";return t.forEach(function(t){a+=e(t,n)}),a}var o={type:t.type,content:e(t.content,n),tag:"span",classes:["token",t.type],attributes:{},language:n},i=t.alias;i&&(Array.isArray(i)?Array.prototype.push.apply(o.classes,i):o.classes.push(i)),r.hooks.run("wrap",o);var l="";for(var s in o.attributes)l+=" "+s+'="'+(o.attributes[s]||"").replace(/"/g,""")+'"';return"<"+o.tag+' class="'+o.classes.join(" ")+'"'+l+">"+o.content+""+o.tag+">"},r}();t.exports=n,n.default=n}},function(){return a||(0,r[N(r)[0]])((a={exports:{}}).exports,a),a.exports})())?T(I(o)):{},((e,t,n,r)=>{if(t&&"object"==typeof t||"function"==typeof t)for(let a of N(t))F.call(e,a)||a===n||P(e,a,{get:()=>t[a],enumerable:!(r=L(t,a))||r.enumerable});return e})(!i&&o&&o.__esModule?l:P(l,"default",{value:o,enumerable:!0}),o));B.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/?[\da-f]{1,8};/i]},B.languages.markup.tag.inside["attr-value"].inside.entity=B.languages.markup.entity,B.languages.markup.doctype.inside["internal-subset"].inside=B.languages.markup,B.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),Object.defineProperty(B.languages.markup.tag,"addInlined",{value:function(e,t){var n={},n=(n["language-"+t]={pattern:/(^$)/i,lookbehind:!0,inside:B.languages[t]},n.cdata=/^$/i,{"included-cdata":{pattern://i,inside:n}}),t=(n["language-"+t]={pattern:/[\s\S]+/,inside:B.languages[t]},{});t[e]={pattern:RegExp(/(<__[^>]*>)(?:))*\]\]>|(?!)/.source.replace(/__/g,function(){return e}),"i"),lookbehind:!0,greedy:!0,inside:n},B.languages.insertBefore("markup","cdata",t)}}),Object.defineProperty(B.languages.markup.tag,"addAttribute",{value:function(e,t){B.languages.markup.tag.inside["special-attr"].push({pattern:RegExp(/(^|["'\s])/.source+"(?:"+e+")"+/\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))/.source,"i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[t,"language-"+t],inside:B.languages[t]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),B.languages.html=B.languages.markup,B.languages.mathml=B.languages.markup,B.languages.svg=B.languages.markup,B.languages.xml=B.languages.extend("markup",{}),B.languages.ssml=B.languages.xml,B.languages.atom=B.languages.xml,B.languages.rss=B.languages.xml,s={pattern:/\\[\\(){}[\]^$+*?|.]/,alias:"escape"},c=RegExp((c="(?:[^\\\\-]|"+(u=/\\(?:x[\da-fA-F]{2}|u[\da-fA-F]{4}|u\{[\da-fA-F]+\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)/).source+")")+"-"+c),d={pattern:/(<|')[^<>']+(?=[>']$)/,lookbehind:!0,alias:"variable"},B.languages.regex={"char-class":{pattern:/((?:^|[^\\])(?:\\\\)*)\[(?:[^\\\]]|\\[\s\S])*\]/,lookbehind:!0,inside:{"char-class-negation":{pattern:/(^\[)\^/,lookbehind:!0,alias:"operator"},"char-class-punctuation":{pattern:/^\[|\]$/,alias:"punctuation"},range:{pattern:c,inside:{escape:u,"range-punctuation":{pattern:/-/,alias:"operator"}}},"special-escape":s,"char-set":{pattern:/\\[wsd]|\\p\{[^{}]+\}/i,alias:"class-name"},escape:u}},"special-escape":s,"char-set":{pattern:/\.|\\[wsd]|\\p\{[^{}]+\}/i,alias:"class-name"},backreference:[{pattern:/\\(?![123][0-7]{2})[1-9]/,alias:"keyword"},{pattern:/\\k<[^<>']+>/,alias:"keyword",inside:{"group-name":d}}],anchor:{pattern:/[$^]|\\[ABbGZz]/,alias:"function"},escape:u,group:[{pattern:/\((?:\?(?:<[^<>']+>|'[^<>']+'|[>:]|[=!]|[idmnsuxU]+(?:-[idmnsuxU]+)?:?))?/,alias:"punctuation",inside:{"group-name":d}},{pattern:/\)/,alias:"punctuation"}],quantifier:{pattern:/(?:[+*?]|\{\d+(?:,\d*)?\})[?+]?/,alias:"number"},alternation:{pattern:/\|/,alias:"keyword"}},B.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},B.languages.javascript=B.languages.extend("clike",{"class-name":[B.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp(/(^|[^\w$])/.source+"(?:"+/NaN|Infinity/.source+"|"+/0[bB][01]+(?:_[01]+)*n?/.source+"|"+/0[oO][0-7]+(?:_[0-7]+)*n?/.source+"|"+/0[xX][\dA-Fa-f]+(?:_[\dA-Fa-f]+)*n?/.source+"|"+/\d+(?:_\d+)*n/.source+"|"+/(?:\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\.\d+(?:_\d+)*)(?:[Ee][+-]?\d+(?:_\d+)*)?/.source+")"+/(?![\w$])/.source),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),B.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,B.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp(/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)/.source+/\//.source+"(?:"+/(?:\[(?:[^\]\\\r\n]|\\.)*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}/.source+"|"+/(?:\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.)*\])*\])*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}v[dgimyus]{0,7}/.source+")"+/(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/.source),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:B.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:B.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:B.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:B.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:B.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),B.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:B.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),B.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),B.languages.markup&&(B.languages.markup.tag.addInlined("script","javascript"),B.languages.markup.tag.addAttribute(/on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)/.source,"javascript")),B.languages.js=B.languages.javascript,B.languages.actionscript=B.languages.extend("javascript",{keyword:/\b(?:as|break|case|catch|class|const|default|delete|do|dynamic|each|else|extends|final|finally|for|function|get|if|implements|import|in|include|instanceof|interface|internal|is|namespace|native|new|null|override|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|use|var|void|while|with)\b/,operator:/\+\+|--|(?:[+\-*\/%^]|&&?|\|\|?|<|>>?>?|[!=]=?)=?|[~?@]/}),B.languages.actionscript["class-name"].alias="function",delete B.languages.actionscript.parameter,delete B.languages.actionscript["literal-property"],B.languages.markup&&B.languages.insertBefore("actionscript","string",{xml:{pattern:/(^|[^.])<\/?\w+(?:\s+[^\s>\/=]+=("|')(?:\\[\s\S]|(?!\2)[^\\])*\2)*\s*\/?>/,lookbehind:!0,inside:B.languages.markup}}),f=/#(?!\{).+/,p={pattern:/#\{[^}]+\}/,alias:"variable"},B.languages.coffeescript=B.languages.extend("javascript",{comment:f,string:[{pattern:/'(?:\\[\s\S]|[^\\'])*'/,greedy:!0},{pattern:/"(?:\\[\s\S]|[^\\"])*"/,greedy:!0,inside:{interpolation:p}}],keyword:/\b(?:and|break|by|catch|class|continue|debugger|delete|do|each|else|extend|extends|false|finally|for|if|in|instanceof|is|isnt|let|loop|namespace|new|no|not|null|of|off|on|or|own|return|super|switch|then|this|throw|true|try|typeof|undefined|unless|until|when|while|window|with|yes|yield)\b/,"class-member":{pattern:/@(?!\d)\w+/,alias:"variable"}}),B.languages.insertBefore("coffeescript","comment",{"multiline-comment":{pattern:/###[\s\S]+?###/,alias:"comment"},"block-regex":{pattern:/\/{3}[\s\S]*?\/{3}/,alias:"regex",inside:{comment:f,interpolation:p}}}),B.languages.insertBefore("coffeescript","string",{"inline-javascript":{pattern:/`(?:\\[\s\S]|[^\\`])*`/,inside:{delimiter:{pattern:/^`|`$/,alias:"punctuation"},script:{pattern:/[\s\S]+/,alias:"language-javascript",inside:B.languages.javascript}}},"multiline-string":[{pattern:/'''[\s\S]*?'''/,greedy:!0,alias:"string"},{pattern:/"""[\s\S]*?"""/,greedy:!0,alias:"string",inside:{interpolation:p}}]}),B.languages.insertBefore("coffeescript","keyword",{property:/(?!\d)\w+(?=\s*:(?!:))/}),delete B.languages.coffeescript["template-string"],B.languages.coffee=B.languages.coffeescript,Object.defineProperty(m=B.languages.javadoclike={parameter:{pattern:/(^[\t ]*(?:\/{3}|\*|\/\*\*)\s*@(?:arg|arguments|param)\s+)\w+/m,lookbehind:!0},keyword:{pattern:/(^[\t ]*(?:\/{3}|\*|\/\*\*)\s*|\{)@[a-z][a-zA-Z-]+\b/m,lookbehind:!0},punctuation:/[{}]/},"addSupport",{value:function(e,t){(e="string"==typeof e?[e]:e).forEach(function(e){var n=function(e){e.inside||(e.inside={}),e.inside.rest=t},r="doc-comment";if(a=B.languages[e]){var a,o=a[r];if((o=o||(a=B.languages.insertBefore(e,"comment",{"doc-comment":{pattern:/(^|[^\\])\/\*\*[^/][\s\S]*?(?:\*\/|$)/,lookbehind:!0,alias:"comment"}}))[r])instanceof RegExp&&(o=a[r]={pattern:o}),Array.isArray(o))for(var i=0,l=o.length;i|\+|~|\|\|/,punctuation:/[(),]/}},B.languages.css.atrule.inside["selector-function-argument"].inside=g,B.languages.insertBefore("css","property",{variable:{pattern:/(^|[^-\w\xA0-\uFFFF])--(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*/i,lookbehind:!0}}),g={pattern:/(\b\d+)(?:%|[a-z]+(?![\w-]))/,lookbehind:!0},b={pattern:/(^|[^\w.-])-?(?:\d+(?:\.\d+)?|\.\d+)/,lookbehind:!0},B.languages.insertBefore("css","function",{operator:{pattern:/(\s)[+\-*\/](?=\s)/,lookbehind:!0},hexcode:{pattern:/\B#[\da-f]{3,8}\b/i,alias:"color"},color:[{pattern:/(^|[^\w-])(?:AliceBlue|AntiqueWhite|Aqua|Aquamarine|Azure|Beige|Bisque|Black|BlanchedAlmond|Blue|BlueViolet|Brown|BurlyWood|CadetBlue|Chartreuse|Chocolate|Coral|CornflowerBlue|Cornsilk|Crimson|Cyan|DarkBlue|DarkCyan|DarkGoldenRod|DarkGr[ae]y|DarkGreen|DarkKhaki|DarkMagenta|DarkOliveGreen|DarkOrange|DarkOrchid|DarkRed|DarkSalmon|DarkSeaGreen|DarkSlateBlue|DarkSlateGr[ae]y|DarkTurquoise|DarkViolet|DeepPink|DeepSkyBlue|DimGr[ae]y|DodgerBlue|FireBrick|FloralWhite|ForestGreen|Fuchsia|Gainsboro|GhostWhite|Gold|GoldenRod|Gr[ae]y|Green|GreenYellow|HoneyDew|HotPink|IndianRed|Indigo|Ivory|Khaki|Lavender|LavenderBlush|LawnGreen|LemonChiffon|LightBlue|LightCoral|LightCyan|LightGoldenRodYellow|LightGr[ae]y|LightGreen|LightPink|LightSalmon|LightSeaGreen|LightSkyBlue|LightSlateGr[ae]y|LightSteelBlue|LightYellow|Lime|LimeGreen|Linen|Magenta|Maroon|MediumAquaMarine|MediumBlue|MediumOrchid|MediumPurple|MediumSeaGreen|MediumSlateBlue|MediumSpringGreen|MediumTurquoise|MediumVioletRed|MidnightBlue|MintCream|MistyRose|Moccasin|NavajoWhite|Navy|OldLace|Olive|OliveDrab|Orange|OrangeRed|Orchid|PaleGoldenRod|PaleGreen|PaleTurquoise|PaleVioletRed|PapayaWhip|PeachPuff|Peru|Pink|Plum|PowderBlue|Purple|RebeccaPurple|Red|RosyBrown|RoyalBlue|SaddleBrown|Salmon|SandyBrown|SeaGreen|SeaShell|Sienna|Silver|SkyBlue|SlateBlue|SlateGr[ae]y|Snow|SpringGreen|SteelBlue|Tan|Teal|Thistle|Tomato|Transparent|Turquoise|Violet|Wheat|White|WhiteSmoke|Yellow|YellowGreen)(?![\w-])/i,lookbehind:!0},{pattern:/\b(?:hsl|rgb)\(\s*\d{1,3}\s*,\s*\d{1,3}%?\s*,\s*\d{1,3}%?\s*\)\B|\b(?:hsl|rgb)a\(\s*\d{1,3}\s*,\s*\d{1,3}%?\s*,\s*\d{1,3}%?\s*,\s*(?:0|0?\.\d+|1)\s*\)\B/i,inside:{unit:g,number:b,function:/[\w-]+(?=\()/,punctuation:/[(),]/}}],entity:/\\[\da-f]{1,8}/i,unit:g,number:b}),function(e){var t=/[*&][^\s[\]{},]+/,n=/!(?:<[\w\-%#;/?:@&=+$,.!~*'()[\]]+>|(?:[a-zA-Z\d-]*!)?[\w\-%#;/?:@&=+$.~*'()]+)?/,r="(?:"+n.source+"(?:[ ]+"+t.source+")?|"+t.source+"(?:[ ]+"+n.source+")?)",a=/(?:[^\s\x00-\x08\x0e-\x1f!"#%&'*,\-:>?@[\]`{|}\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]|[?:-])(?:[ \t]*(?:(?![#:])|:))*/.source.replace(//g,function(){return/[^\s\x00-\x08\x0e-\x1f,[\]{}\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]/.source}),o=/"(?:[^"\\\r\n]|\\.)*"|'(?:[^'\\\r\n]|\\.)*'/.source;function i(e,t){return t=(t||"").replace(/m/g,"")+"m",RegExp(/([:\-,[{]\s*(?:\s<>[ \t]+)?)(?:<>)(?=[ \t]*(?:$|,|\]|\}|(?:[\r\n]\s*)?#))/.source.replace(/<>/g,function(){return r}).replace(/<>/g,function(){return e}),t)}e.languages.yaml={scalar:{pattern:RegExp(/([\-:]\s*(?:\s<>[ \t]+)?[|>])[ \t]*(?:((?:\r?\n|\r)[ \t]+)\S[^\r\n]*(?:\2[^\r\n]+)*)/.source.replace(/<>/g,function(){return r})),lookbehind:!0,alias:"string"},comment:/#.*/,key:{pattern:RegExp(/((?:^|[:\-,[{\r\n?])[ \t]*(?:<>[ \t]+)?)<>(?=\s*:\s)/.source.replace(/<>/g,function(){return r}).replace(/<>/g,function(){return"(?:"+a+"|"+o+")"})),lookbehind:!0,greedy:!0,alias:"atrule"},directive:{pattern:/(^[ \t]*)%.+/m,lookbehind:!0,alias:"important"},datetime:{pattern:i(/\d{4}-\d\d?-\d\d?(?:[tT]|[ \t]+)\d\d?:\d{2}:\d{2}(?:\.\d*)?(?:[ \t]*(?:Z|[-+]\d\d?(?::\d{2})?))?|\d{4}-\d{2}-\d{2}|\d\d?:\d{2}(?::\d{2}(?:\.\d*)?)?/.source),lookbehind:!0,alias:"number"},boolean:{pattern:i(/false|true/.source,"i"),lookbehind:!0,alias:"important"},null:{pattern:i(/null|~/.source,"i"),lookbehind:!0,alias:"important"},string:{pattern:i(o),lookbehind:!0,greedy:!0},number:{pattern:i(/[+-]?(?:0x[\da-f]+|0o[0-7]+|(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?|\.inf|\.nan)/.source,"i"),lookbehind:!0},tag:n,important:t,punctuation:/---|[:[\]{}\-,|>?]|\.\.\./},e.languages.yml=e.languages.yaml}(B),function(e){var t=/(?:\\.|[^\\\n\r]|(?:\n|\r\n?)(?![\r\n]))/.source;function n(e){return e=e.replace(//g,function(){return t}),RegExp(/((?:^|[^\\])(?:\\{2})*)/.source+"(?:"+e+")")}var r=/(?:\\.|``(?:[^`\r\n]|`(?!`))+``|`[^`\r\n]+`|[^\\|\r\n`])+/.source,a=/\|?__(?:\|__)+\|?(?:(?:\n|\r\n?)|(?![\s\S]))/.source.replace(/__/g,function(){return r}),o=/\|?[ \t]*:?-{3,}:?[ \t]*(?:\|[ \t]*:?-{3,}:?[ \t]*)+\|?(?:\n|\r\n?)/.source,i=(e.languages.markdown=e.languages.extend("markup",{}),e.languages.insertBefore("markdown","prolog",{"front-matter-block":{pattern:/(^(?:\s*[\r\n])?)---(?!.)[\s\S]*?[\r\n]---(?!.)/,lookbehind:!0,greedy:!0,inside:{punctuation:/^---|---$/,"front-matter":{pattern:/\S+(?:\s+\S+)*/,alias:["yaml","language-yaml"],inside:e.languages.yaml}}},blockquote:{pattern:/^>(?:[\t ]*>)*/m,alias:"punctuation"},table:{pattern:RegExp("^"+a+o+"(?:"+a+")*","m"),inside:{"table-data-rows":{pattern:RegExp("^("+a+o+")(?:"+a+")*$"),lookbehind:!0,inside:{"table-data":{pattern:RegExp(r),inside:e.languages.markdown},punctuation:/\|/}},"table-line":{pattern:RegExp("^("+a+")"+o+"$"),lookbehind:!0,inside:{punctuation:/\||:?-{3,}:?/}},"table-header-row":{pattern:RegExp("^"+a+"$"),inside:{"table-header":{pattern:RegExp(r),alias:"important",inside:e.languages.markdown},punctuation:/\|/}}}},code:[{pattern:/((?:^|\n)[ \t]*\n|(?:^|\r\n?)[ \t]*\r\n?)(?: {4}|\t).+(?:(?:\n|\r\n?)(?: {4}|\t).+)*/,lookbehind:!0,alias:"keyword"},{pattern:/^```[\s\S]*?^```$/m,greedy:!0,inside:{"code-block":{pattern:/^(```.*(?:\n|\r\n?))[\s\S]+?(?=(?:\n|\r\n?)^```$)/m,lookbehind:!0},"code-language":{pattern:/^(```).+/,lookbehind:!0},punctuation:/```/}}],title:[{pattern:/\S.*(?:\n|\r\n?)(?:==+|--+)(?=[ \t]*$)/m,alias:"important",inside:{punctuation:/==+$|--+$/}},{pattern:/(^\s*)#.+/m,lookbehind:!0,alias:"important",inside:{punctuation:/^#+|#+$/}}],hr:{pattern:/(^\s*)([*-])(?:[\t ]*\2){2,}(?=\s*$)/m,lookbehind:!0,alias:"punctuation"},list:{pattern:/(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m,lookbehind:!0,alias:"punctuation"},"url-reference":{pattern:/!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/,inside:{variable:{pattern:/^(!?\[)[^\]]+/,lookbehind:!0},string:/(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/,punctuation:/^[\[\]!:]|[<>]/},alias:"url"},bold:{pattern:n(/\b__(?:(?!_)|_(?:(?!_))+_)+__\b|\*\*(?:(?!\*)|\*(?:(?!\*))+\*)+\*\*/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^..)[\s\S]+(?=..$)/,lookbehind:!0,inside:{}},punctuation:/\*\*|__/}},italic:{pattern:n(/\b_(?:(?!_)|__(?:(?!_))+__)+_\b|\*(?:(?!\*)|\*\*(?:(?!\*))+\*\*)+\*/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^.)[\s\S]+(?=.$)/,lookbehind:!0,inside:{}},punctuation:/[*_]/}},strike:{pattern:n(/(~~?)(?:(?!~))+\2/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^~~?)[\s\S]+(?=\1$)/,lookbehind:!0,inside:{}},punctuation:/~~?/}},"code-snippet":{pattern:/(^|[^\\`])(?:``[^`\r\n]+(?:`[^`\r\n]+)*``(?!`)|`[^`\r\n]+`(?!`))/,lookbehind:!0,greedy:!0,alias:["code","keyword"]},url:{pattern:n(/!?\[(?:(?!\]))+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)|[ \t]?\[(?:(?!\]))+\])/.source),lookbehind:!0,greedy:!0,inside:{operator:/^!/,content:{pattern:/(^\[)[^\]]+(?=\])/,lookbehind:!0,inside:{}},variable:{pattern:/(^\][ \t]?\[)[^\]]+(?=\]$)/,lookbehind:!0},url:{pattern:/(^\]\()[^\s)]+/,lookbehind:!0},string:{pattern:/(^[ \t]+)"(?:\\.|[^"\\])*"(?=\)$)/,lookbehind:!0}}}}),["url","bold","italic","strike"].forEach(function(t){["url","bold","italic","strike","code-snippet"].forEach(function(n){t!==n&&(e.languages.markdown[t].inside.content.inside[n]=e.languages.markdown[n])})}),e.hooks.add("after-tokenize",function(e){"markdown"!==e.language&&"md"!==e.language||function e(t){if(t&&"string"!=typeof t)for(var n=0,r=t.length;n",quot:'"'},s=String.fromCodePoint||String.fromCharCode;e.languages.md=e.languages.markdown}(B),B.languages.graphql={comment:/#.*/,description:{pattern:/(?:"""(?:[^"]|(?!""")")*"""|"(?:\\.|[^\\"\r\n])*")(?=\s*[a-z_])/i,greedy:!0,alias:"string",inside:{"language-markdown":{pattern:/(^"(?:"")?)(?!\1)[\s\S]+(?=\1$)/,lookbehind:!0,inside:B.languages.markdown}}},string:{pattern:/"""(?:[^"]|(?!""")")*"""|"(?:\\.|[^\\"\r\n])*"/,greedy:!0},number:/(?:\B-|\b)\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,boolean:/\b(?:false|true)\b/,variable:/\$[a-z_]\w*/i,directive:{pattern:/@[a-z_]\w*/i,alias:"function"},"attr-name":{pattern:/\b[a-z_]\w*(?=\s*(?:\((?:[^()"]|"(?:\\.|[^\\"\r\n])*")*\))?:)/i,greedy:!0},"atom-input":{pattern:/\b[A-Z]\w*Input\b/,alias:"class-name"},scalar:/\b(?:Boolean|Float|ID|Int|String)\b/,constant:/\b[A-Z][A-Z_\d]*\b/,"class-name":{pattern:/(\b(?:enum|implements|interface|on|scalar|type|union)\s+|&\s*|:\s*|\[)[A-Z_]\w*/,lookbehind:!0},fragment:{pattern:/(\bfragment\s+|\.{3}\s*(?!on\b))[a-zA-Z_]\w*/,lookbehind:!0,alias:"function"},"definition-mutation":{pattern:/(\bmutation\s+)[a-zA-Z_]\w*/,lookbehind:!0,alias:"function"},"definition-query":{pattern:/(\bquery\s+)[a-zA-Z_]\w*/,lookbehind:!0,alias:"function"},keyword:/\b(?:directive|enum|extend|fragment|implements|input|interface|mutation|on|query|repeatable|scalar|schema|subscription|type|union)\b/,operator:/[!=|&]|\.{3}/,"property-query":/\w+(?=\s*\()/,object:/\w+(?=\s*\{)/,punctuation:/[!(){}\[\]:=,]/,property:/\w+/},B.hooks.add("after-tokenize",function(e){if("graphql"===e.language)for(var t=e.tokens.filter(function(e){return"string"!=typeof e&&"comment"!==e.type&&"scalar"!==e.type}),n=0;n?|<|>)?|>[>=]?|\b(?:AND|BETWEEN|DIV|ILIKE|IN|IS|LIKE|NOT|OR|REGEXP|RLIKE|SOUNDS LIKE|XOR)\b/i,punctuation:/[;[\]()`,.]/},function(e){var t=e.languages.javascript["template-string"],n=t.pattern.source,r=t.inside.interpolation,a=r.inside["interpolation-punctuation"],o=r.pattern.source;function i(t,r){if(e.languages[t])return{pattern:RegExp("((?:"+r+")\\s*)"+n),lookbehind:!0,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},"embedded-code":{pattern:/[\s\S]+/,alias:t}}}}function l(t,n,r){return t={code:t,grammar:n,language:r},e.hooks.run("before-tokenize",t),t.tokens=e.tokenize(t.code,t.grammar),e.hooks.run("after-tokenize",t),t.tokens}e.languages.javascript["template-string"]=[i("css",/\b(?:styled(?:\([^)]*\))?(?:\s*\.\s*\w+(?:\([^)]*\))*)*|css(?:\s*\.\s*(?:global|resolve))?|createGlobalStyle|keyframes)/.source),i("html",/\bhtml|\.\s*(?:inner|outer)HTML\s*\+?=/.source),i("svg",/\bsvg/.source),i("markdown",/\b(?:markdown|md)/.source),i("graphql",/\b(?:gql|graphql(?:\s*\.\s*experimental)?)/.source),i("sql",/\bsql/.source),t].filter(Boolean);var s={javascript:!0,js:!0,typescript:!0,ts:!0,jsx:!0,tsx:!0};e.hooks.add("after-tokenize",function(t){t.language in s&&function t(n){for(var i=0,s=n.length;i=d.length)return;var i,s,f,p,m,h,g,b=n[o];"string"==typeof b||"string"==typeof b.content?(i=d[u],-1!==(g=(h="string"==typeof b?b:b.content).indexOf(i))&&(++u,s=h.substring(0,g),m=c[i],f=void 0,(p={})["interpolation-punctuation"]=a,3===(p=e.tokenize(m,p)).length&&((f=[1,1]).push.apply(f,l(p[1],e.languages.javascript,"javascript")),p.splice.apply(p,f)),f=new e.Token("interpolation",p,r.alias,m),p=h.substring(g+i.length),m=[],s&&m.push(s),m.push(f),p&&(t(h=[p]),m.push.apply(m,h)),"string"==typeof b?(n.splice.apply(n,[o,1].concat(m)),o+=m.length-1):b.content=m)):Array.isArray(g=b.content)?t(g):t([g])}}(s),new e.Token(i,s,"language-"+i,t)}(c,d,f))):t(u):"string"!=typeof u&&t([u]))}}(t.tokens)})}(B),B.languages.typescript=B.languages.extend("javascript",{"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|type)\s+)(?!keyof\b)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?:\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/,lookbehind:!0,greedy:!0,inside:null},builtin:/\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\b/}),B.languages.typescript.keyword.push(/\b(?:abstract|declare|is|keyof|readonly|require)\b/,/\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/,/\btype\b(?=\s*(?:[\{*]|$))/),delete B.languages.typescript.parameter,delete B.languages.typescript["literal-property"],y=B.languages.extend("typescript",{}),delete y["class-name"],B.languages.typescript["class-name"].inside=y,B.languages.insertBefore("typescript","function",{decorator:{pattern:/@[$\w\xA0-\uFFFF]+/,inside:{at:{pattern:/^@/,alias:"operator"},function:/^[\s\S]+/}},"generic-function":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\s*\()/,greedy:!0,inside:{function:/^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:y}}}}),B.languages.ts=B.languages.typescript,v=B.languages.javascript,w="(@(?:arg|argument|param|property)\\s+(?:"+(k=/\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})+\}/.source)+"\\s+)?)",B.languages.jsdoc=B.languages.extend("javadoclike",{parameter:{pattern:RegExp(w+/(?:(?!\s)[$\w\xA0-\uFFFF.])+(?=\s|$)/.source),lookbehind:!0,inside:{punctuation:/\./}}}),B.languages.insertBefore("jsdoc","keyword",{"optional-parameter":{pattern:RegExp(w+/\[(?:(?!\s)[$\w\xA0-\uFFFF.])+(?:=[^[\]]+)?\](?=\s|$)/.source),lookbehind:!0,inside:{parameter:{pattern:/(^\[)[$\w\xA0-\uFFFF\.]+/,lookbehind:!0,inside:{punctuation:/\./}},code:{pattern:/(=)[\s\S]*(?=\]$)/,lookbehind:!0,inside:v,alias:"language-javascript"},punctuation:/[=[\]]/}},"class-name":[{pattern:RegExp(/(@(?:augments|class|extends|interface|memberof!?|template|this|typedef)\s+(?:\s+)?)[A-Z]\w*(?:\.[A-Z]\w*)*/.source.replace(//g,function(){return k})),lookbehind:!0,inside:{punctuation:/\./}},{pattern:RegExp("(@[a-z]+\\s+)"+k),lookbehind:!0,inside:{string:v.string,number:v.number,boolean:v.boolean,keyword:B.languages.typescript.keyword,operator:/=>|\.\.\.|[&|?:*]/,punctuation:/[.,;=<>{}()[\]]/}}],example:{pattern:/(@example\s+(?!\s))(?:[^@\s]|\s+(?!\s))+?(?=\s*(?:\*\s*)?(?:@\w|\*\/))/,lookbehind:!0,inside:{code:{pattern:/^([\t ]*(?:\*\s*)?)\S.*$/m,lookbehind:!0,inside:v,alias:"language-javascript"}}}}),B.languages.javadoclike.addSupport("javascript",B.languages.jsdoc),B.languages.flow=B.languages.extend("javascript",{}),B.languages.insertBefore("flow","keyword",{type:[{pattern:/\b(?:[Bb]oolean|Function|[Nn]umber|[Ss]tring|[Ss]ymbol|any|mixed|null|void)\b/,alias:"class-name"}]}),B.languages.flow["function-variable"].pattern=/(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=\s*(?:function\b|(?:\([^()]*\)(?:\s*:\s*\w+)?|(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/i,delete B.languages.flow.parameter,B.languages.insertBefore("flow","operator",{"flow-punctuation":{pattern:/\{\||\|\}/,alias:"punctuation"}}),Array.isArray(B.languages.flow.keyword)||(B.languages.flow.keyword=[B.languages.flow.keyword]),B.languages.flow.keyword.unshift({pattern:/(^|[^$]\b)(?:Class|declare|opaque|type)\b(?!\$)/,lookbehind:!0},{pattern:/(^|[^$]\B)\$(?:Diff|Enum|Exact|Keys|ObjMap|PropertyType|Record|Shape|Subtype|Supertype|await)\b(?!\$)/,lookbehind:!0}),B.languages.n4js=B.languages.extend("javascript",{keyword:/\b(?:Array|any|boolean|break|case|catch|class|const|constructor|continue|debugger|declare|default|delete|do|else|enum|export|extends|false|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|module|new|null|number|package|private|protected|public|return|set|static|string|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)\b/}),B.languages.insertBefore("n4js","constant",{annotation:{pattern:/@+\w+/,alias:"operator"}}),B.languages.n4jsd=B.languages.n4js,function(e){function t(e,t){return RegExp(e.replace(//g,function(){return/(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/.source}),t)}e.languages.insertBefore("javascript","function-variable",{"method-variable":{pattern:RegExp("(\\.\\s*)"+e.languages.javascript["function-variable"].pattern.source),lookbehind:!0,alias:["function-variable","method","function","property-access"]}}),e.languages.insertBefore("javascript","function",{method:{pattern:RegExp("(\\.\\s*)"+e.languages.javascript.function.source),lookbehind:!0,alias:["function","property-access"]}}),e.languages.insertBefore("javascript","constant",{"known-class-name":[{pattern:/\b(?:(?:Float(?:32|64)|(?:Int|Uint)(?:8|16|32)|Uint8Clamped)?Array|ArrayBuffer|BigInt|Boolean|DataView|Date|Error|Function|Intl|JSON|(?:Weak)?(?:Map|Set)|Math|Number|Object|Promise|Proxy|Reflect|RegExp|String|Symbol|WebAssembly)\b/,alias:"class-name"},{pattern:/\b(?:[A-Z]\w*)Error\b/,alias:"class-name"}]}),e.languages.insertBefore("javascript","keyword",{imports:{pattern:t(/(\bimport\b\s*)(?:(?:\s*,\s*(?:\*\s*as\s+|\{[^{}]*\}))?|\*\s*as\s+|\{[^{}]*\})(?=\s*\bfrom\b)/.source),lookbehind:!0,inside:e.languages.javascript},exports:{pattern:t(/(\bexport\b\s*)(?:\*(?:\s*as\s+)?(?=\s*\bfrom\b)|\{[^{}]*\})/.source),lookbehind:!0,inside:e.languages.javascript}}),e.languages.javascript.keyword.unshift({pattern:/\b(?:as|default|export|from|import)\b/,alias:"module"},{pattern:/\b(?:await|break|catch|continue|do|else|finally|for|if|return|switch|throw|try|while|yield)\b/,alias:"control-flow"},{pattern:/\bnull\b/,alias:["null","nil"]},{pattern:/\bundefined\b/,alias:"nil"}),e.languages.insertBefore("javascript","operator",{spread:{pattern:/\.{3}/,alias:"operator"},arrow:{pattern:/=>/,alias:"operator"}}),e.languages.insertBefore("javascript","punctuation",{"property-access":{pattern:t(/(\.\s*)#?/.source),lookbehind:!0},"maybe-class-name":{pattern:/(^|[^$\w\xA0-\uFFFF])[A-Z][$\w\xA0-\uFFFF]+/,lookbehind:!0},dom:{pattern:/\b(?:document|(?:local|session)Storage|location|navigator|performance|window)\b/,alias:"variable"},console:{pattern:/\bconsole(?=\s*\.)/,alias:"class-name"}});for(var n=["function","function-variable","method","method-variable","property-access"],r=0;r*\.{3}(?:[^{}]|)*\})/.source;function o(e,t){return RegExp(e=e.replace(//g,function(){return n}).replace(//g,function(){return r}).replace(//g,function(){return a}),t)}a=o(a).source,e.languages.jsx=e.languages.extend("markup",t),e.languages.jsx.tag.pattern=o(/<\/?(?:[\w.:-]+(?:+(?:[\w.:$-]+(?:=(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s{'"/>=]+|))?|))**\/?)?>/.source),e.languages.jsx.tag.inside.tag.pattern=/^<\/?[^\s>\/]*/,e.languages.jsx.tag.inside["attr-value"].pattern=/=(?!\{)(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s'">]+)/,e.languages.jsx.tag.inside.tag.inside["class-name"]=/^[A-Z]\w*(?:\.[A-Z]\w*)*$/,e.languages.jsx.tag.inside.comment=t.comment,e.languages.insertBefore("inside","attr-name",{spread:{pattern:o(//.source),inside:e.languages.jsx}},e.languages.jsx.tag),e.languages.insertBefore("inside","special-attr",{script:{pattern:o(/=/.source),alias:"language-javascript",inside:{"script-punctuation":{pattern:/^=(?=\{)/,alias:"punctuation"},rest:e.languages.jsx}}},e.languages.jsx.tag);var i=function(e){return e?"string"==typeof e?e:"string"==typeof e.content?e.content:e.content.map(i).join(""):""};e.hooks.add("after-tokenize",function(t){"jsx"!==t.language&&"tsx"!==t.language||function t(n){for(var r=[],a=0;a"!==o.content[o.content.length-1].content&&r.push({tagName:i(o.content[0].content[1]),openedBraces:0}):0]=?/,punctuation:/[(),]/}},literal:{pattern:/#(?:colorLiteral|column|dsohandle|file(?:ID|Literal|Path)?|function|imageLiteral|line)\b/,alias:"constant"},"other-directive":{pattern:/#\w+\b/,alias:"property"},attribute:{pattern:/@\w+/,alias:"atrule"},"function-definition":{pattern:/(\bfunc\s+)\w+/,lookbehind:!0,alias:"function"},label:{pattern:/\b(break|continue)\s+\w+|\b[a-zA-Z_]\w*(?=\s*:\s*(?:for|repeat|while)\b)/,lookbehind:!0,alias:"important"},keyword:/\b(?:Any|Protocol|Self|Type|actor|as|assignment|associatedtype|associativity|async|await|break|case|catch|class|continue|convenience|default|defer|deinit|didSet|do|dynamic|else|enum|extension|fallthrough|fileprivate|final|for|func|get|guard|higherThan|if|import|in|indirect|infix|init|inout|internal|is|isolated|lazy|left|let|lowerThan|mutating|none|nonisolated|nonmutating|open|operator|optional|override|postfix|precedencegroup|prefix|private|protocol|public|repeat|required|rethrows|return|right|safe|self|set|some|static|struct|subscript|super|switch|throw|throws|try|typealias|unowned|unsafe|var|weak|where|while|willSet)\b/,boolean:/\b(?:false|true)\b/,nil:{pattern:/\bnil\b/,alias:"constant"},"short-argument":/\$\d+\b/,omit:{pattern:/\b_\b/,alias:"keyword"},number:/\b(?:[\d_]+(?:\.[\de_]+)?|0x[a-f0-9_]+(?:\.[a-f0-9p_]+)?|0b[01_]+|0o[0-7_]+)\b/i,"class-name":/\b[A-Z](?:[A-Z_\d]*[a-z]\w*)?\b/,function:/\b[a-z_]\w*(?=\s*\()/i,constant:/\b(?:[A-Z_]{2,}|k[A-Z][A-Za-z_]+)\b/,operator:/[-+*/%=!<>&|^~?]+|\.[.\-+*/%=!<>&|^~?]+/,punctuation:/[{}[\]();,.:\\]/},B.languages.swift["string-literal"].forEach(function(e){e.inside.interpolation.inside=B.languages.swift}),B.languages.kotlin=B.languages.extend("clike",{keyword:{pattern:/(^|[^.])\b(?:abstract|actual|annotation|as|break|by|catch|class|companion|const|constructor|continue|crossinline|data|do|dynamic|else|enum|expect|external|final|finally|for|fun|get|if|import|in|infix|init|inline|inner|interface|internal|is|lateinit|noinline|null|object|open|operator|out|override|package|private|protected|public|reified|return|sealed|set|super|suspend|tailrec|this|throw|to|try|typealias|val|var|vararg|when|where|while)\b/,lookbehind:!0},function:[{pattern:/(?:`[^\r\n`]+`|\b\w+)(?=\s*\()/,greedy:!0},{pattern:/(\.)(?:`[^\r\n`]+`|\w+)(?=\s*\{)/,lookbehind:!0,greedy:!0}],number:/\b(?:0[xX][\da-fA-F]+(?:_[\da-fA-F]+)*|0[bB][01]+(?:_[01]+)*|\d+(?:_\d+)*(?:\.\d+(?:_\d+)*)?(?:[eE][+-]?\d+(?:_\d+)*)?[fFL]?)\b/,operator:/\+[+=]?|-[-=>]?|==?=?|!(?:!|==?)?|[\/*%<>]=?|[?:]:?|\.\.|&&|\|\||\b(?:and|inv|or|shl|shr|ushr|xor)\b/}),delete B.languages.kotlin["class-name"],S={"interpolation-punctuation":{pattern:/^\$\{?|\}$/,alias:"punctuation"},expression:{pattern:/[\s\S]+/,inside:B.languages.kotlin}},B.languages.insertBefore("kotlin","string",{"string-literal":[{pattern:/"""(?:[^$]|\$(?:(?!\{)|\{[^{}]*\}))*?"""/,alias:"multiline",inside:{interpolation:{pattern:/\$(?:[a-z_]\w*|\{[^{}]*\})/i,inside:S},string:/[\s\S]+/}},{pattern:/"(?:[^"\\\r\n$]|\\.|\$(?:(?!\{)|\{[^{}]*\}))*"/,alias:"singleline",inside:{interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$(?:[a-z_]\w*|\{[^{}]*\})/i,lookbehind:!0,inside:S},string:/[\s\S]+/}}],char:{pattern:/'(?:[^'\\\r\n]|\\(?:.|u[a-fA-F0-9]{0,4}))'/,greedy:!0}}),delete B.languages.kotlin.string,B.languages.insertBefore("kotlin","keyword",{annotation:{pattern:/\B@(?:\w+:)?(?:[A-Z]\w*|\[[^\]]+\])/,alias:"builtin"}}),B.languages.insertBefore("kotlin","function",{label:{pattern:/\b\w+@|@\w+\b/,alias:"symbol"}}),B.languages.kt=B.languages.kotlin,B.languages.kts=B.languages.kotlin,B.languages.c=B.languages.extend("clike",{comment:{pattern:/\/\/(?:[^\r\n\\]|\\(?:\r\n?|\n|(?![\r\n])))*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},string:{pattern:/"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"/,greedy:!0},"class-name":{pattern:/(\b(?:enum|struct)\s+(?:__attribute__\s*\(\([\s\S]*?\)\)\s*)?)\w+|\b[a-z]\w*_t\b/,lookbehind:!0},keyword:/\b(?:_Alignas|_Alignof|_Atomic|_Bool|_Complex|_Generic|_Imaginary|_Noreturn|_Static_assert|_Thread_local|__attribute__|asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|inline|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|typeof|union|unsigned|void|volatile|while)\b/,function:/\b[a-z_]\w*(?=\s*\()/i,number:/(?:\b0x(?:[\da-f]+(?:\.[\da-f]*)?|\.[\da-f]+)(?:p[+-]?\d+)?|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?)[ful]{0,4}/i,operator:/>>=?|<<=?|->|([-+&|:])\1|[?:~]|[-+*/%&|^!=<>]=?/}),B.languages.insertBefore("c","string",{char:{pattern:/'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n]){0,32}'/,greedy:!0}}),B.languages.insertBefore("c","string",{macro:{pattern:/(^[\t ]*)#\s*[a-z](?:[^\r\n\\/]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|\\(?:\r\n|[\s\S]))*/im,lookbehind:!0,greedy:!0,alias:"property",inside:{string:[{pattern:/^(#\s*include\s*)<[^>]+>/,lookbehind:!0},B.languages.c.string],char:B.languages.c.char,comment:B.languages.c.comment,"macro-name":[{pattern:/(^#\s*define\s+)\w+\b(?!\()/i,lookbehind:!0},{pattern:/(^#\s*define\s+)\w+\b(?=\()/i,lookbehind:!0,alias:"function"}],directive:{pattern:/^(#\s*)[a-z]+/,lookbehind:!0,alias:"keyword"},"directive-hash":/^#/,punctuation:/##|\\(?=[\r\n])/,expression:{pattern:/\S[\s\S]*/,inside:B.languages.c}}}}),B.languages.insertBefore("c","function",{constant:/\b(?:EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|__DATE__|__FILE__|__LINE__|__TIMESTAMP__|__TIME__|__func__|stderr|stdin|stdout)\b/}),delete B.languages.c.boolean,B.languages.objectivec=B.languages.extend("c",{string:{pattern:/@?"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"/,greedy:!0},keyword:/\b(?:asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|in|inline|int|long|register|return|self|short|signed|sizeof|static|struct|super|switch|typedef|typeof|union|unsigned|void|volatile|while)\b|(?:@interface|@end|@implementation|@protocol|@class|@public|@protected|@private|@property|@try|@catch|@finally|@throw|@synthesize|@dynamic|@selector)\b/,operator:/-[->]?|\+\+?|!=?|<=?|>>?=?|==?|&&?|\|\|?|[~^%?*\/@]/}),delete B.languages.objectivec["class-name"],B.languages.objc=B.languages.objectivec,B.languages.reason=B.languages.extend("clike",{string:{pattern:/"(?:\\(?:\r\n|[\s\S])|[^\\\r\n"])*"/,greedy:!0},"class-name":/\b[A-Z]\w*/,keyword:/\b(?:and|as|assert|begin|class|constraint|do|done|downto|else|end|exception|external|for|fun|function|functor|if|in|include|inherit|initializer|lazy|let|method|module|mutable|new|nonrec|object|of|open|or|private|rec|sig|struct|switch|then|to|try|type|val|virtual|when|while|with)\b/,operator:/\.{3}|:[:=]|\|>|->|=(?:==?|>)?|<=?|>=?|[|^?'#!~`]|[+\-*\/]\.?|\b(?:asr|land|lor|lsl|lsr|lxor|mod)\b/}),B.languages.insertBefore("reason","class-name",{char:{pattern:/'(?:\\x[\da-f]{2}|\\o[0-3][0-7][0-7]|\\\d{3}|\\.|[^'\\\r\n])'/,greedy:!0},constructor:/\b[A-Z]\w*\b(?!\s*\.)/,label:{pattern:/\b[a-z]\w*(?=::)/,alias:"symbol"}}),delete B.languages.reason.function,function(e){for(var t=/\/\*(?:[^*/]|\*(?!\/)|\/(?!\*)|)*\*\//.source,n=0;n<2;n++)t=t.replace(//g,function(){return t});t=t.replace(//g,function(){return/[^\s\S]/.source}),e.languages.rust={comment:[{pattern:RegExp(/(^|[^\\])/.source+t),lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/b?"(?:\\[\s\S]|[^\\"])*"|b?r(#*)"(?:[^"]|"(?!\1))*"\1/,greedy:!0},char:{pattern:/b?'(?:\\(?:x[0-7][\da-fA-F]|u\{(?:[\da-fA-F]_*){1,6}\}|.)|[^\\\r\n\t'])'/,greedy:!0},attribute:{pattern:/#!?\[(?:[^\[\]"]|"(?:\\[\s\S]|[^\\"])*")*\]/,greedy:!0,alias:"attr-name",inside:{string:null}},"closure-params":{pattern:/([=(,:]\s*|\bmove\s*)\|[^|]*\||\|[^|]*\|(?=\s*(?:\{|->))/,lookbehind:!0,greedy:!0,inside:{"closure-punctuation":{pattern:/^\||\|$/,alias:"punctuation"},rest:null}},"lifetime-annotation":{pattern:/'\w+/,alias:"symbol"},"fragment-specifier":{pattern:/(\$\w+:)[a-z]+/,lookbehind:!0,alias:"punctuation"},variable:/\$\w+/,"function-definition":{pattern:/(\bfn\s+)\w+/,lookbehind:!0,alias:"function"},"type-definition":{pattern:/(\b(?:enum|struct|trait|type|union)\s+)\w+/,lookbehind:!0,alias:"class-name"},"module-declaration":[{pattern:/(\b(?:crate|mod)\s+)[a-z][a-z_\d]*/,lookbehind:!0,alias:"namespace"},{pattern:/(\b(?:crate|self|super)\s*)::\s*[a-z][a-z_\d]*\b(?:\s*::(?:\s*[a-z][a-z_\d]*\s*::)*)?/,lookbehind:!0,alias:"namespace",inside:{punctuation:/::/}}],keyword:[/\b(?:Self|abstract|as|async|await|become|box|break|const|continue|crate|do|dyn|else|enum|extern|final|fn|for|if|impl|in|let|loop|macro|match|mod|move|mut|override|priv|pub|ref|return|self|static|struct|super|trait|try|type|typeof|union|unsafe|unsized|use|virtual|where|while|yield)\b/,/\b(?:bool|char|f(?:32|64)|[ui](?:8|16|32|64|128|size)|str)\b/],function:/\b[a-z_]\w*(?=\s*(?:::\s*<|\())/,macro:{pattern:/\b\w+!/,alias:"property"},constant:/\b[A-Z_][A-Z_\d]+\b/,"class-name":/\b[A-Z]\w*\b/,namespace:{pattern:/(?:\b[a-z][a-z_\d]*\s*::\s*)*\b[a-z][a-z_\d]*\s*::(?!\s*<)/,inside:{punctuation:/::/}},number:/\b(?:0x[\dA-Fa-f](?:_?[\dA-Fa-f])*|0o[0-7](?:_?[0-7])*|0b[01](?:_?[01])*|(?:(?:\d(?:_?\d)*)?\.)?\d(?:_?\d)*(?:[Ee][+-]?\d+)?)(?:_?(?:f32|f64|[iu](?:8|16|32|64|size)?))?\b/,boolean:/\b(?:false|true)\b/,punctuation:/->|\.\.=|\.{1,3}|::|[{}[\];(),:]/,operator:/[-+*\/%!^]=?|=[=>]?|&[&=]?|\|[|=]?|<=?|>>?=?|[@?]/},e.languages.rust["closure-params"].inside.rest=e.languages.rust,e.languages.rust.attribute.inside.string=e.languages.rust.string}(B),B.languages.go=B.languages.extend("clike",{string:{pattern:/(^|[^\\])"(?:\\.|[^"\\\r\n])*"|`[^`]*`/,lookbehind:!0,greedy:!0},keyword:/\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/,boolean:/\b(?:_|false|iota|nil|true)\b/,number:[/\b0(?:b[01_]+|o[0-7_]+)i?\b/i,/\b0x(?:[a-f\d_]+(?:\.[a-f\d_]*)?|\.[a-f\d_]+)(?:p[+-]?\d+(?:_\d+)*)?i?(?!\w)/i,/(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?[\d_]+)?i?(?!\w)/i],operator:/[*\/%^!=]=?|\+[=+]?|-[=-]?|\|[=|]?|&(?:=|&|\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\.\.\./,builtin:/\b(?:append|bool|byte|cap|close|complex|complex(?:64|128)|copy|delete|error|float(?:32|64)|u?int(?:8|16|32|64)?|imag|len|make|new|panic|print(?:ln)?|real|recover|rune|string|uintptr)\b/}),B.languages.insertBefore("go","string",{char:{pattern:/'(?:\\.|[^'\\\r\n]){0,10}'/,greedy:!0}}),delete B.languages.go["class-name"],E=/\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\b/,_=/\b(?!)\w+(?:\s*\.\s*\w+)*\b/.source.replace(//g,function(){return E.source}),B.languages.cpp=B.languages.extend("c",{"class-name":[{pattern:RegExp(/(\b(?:class|concept|enum|struct|typename)\s+)(?!)\w+/.source.replace(//g,function(){return E.source})),lookbehind:!0},/\b[A-Z]\w*(?=\s*::\s*\w+\s*\()/,/\b[A-Z_]\w*(?=\s*::\s*~\w+\s*\()/i,/\b\w+(?=\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>\s*::\s*\w+\s*\()/],keyword:E,number:{pattern:/(?:\b0b[01']+|\b0x(?:[\da-f']+(?:\.[\da-f']*)?|\.[\da-f']+)(?:p[+-]?[\d']+)?|(?:\b[\d']+(?:\.[\d']*)?|\B\.[\d']+)(?:e[+-]?[\d']+)?)[ful]{0,4}/i,greedy:!0},operator:/>>=?|<<=?|->|--|\+\+|&&|\|\||[?:~]|<=>|[-+*/%&|^!=<>]=?|\b(?:and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\b/,boolean:/\b(?:false|true)\b/}),B.languages.insertBefore("cpp","string",{module:{pattern:RegExp(/(\b(?:import|module)\s+)/.source+"(?:"+/"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|<[^<>\r\n]*>/.source+"|"+/(?:\s*:\s*)?|:\s*/.source.replace(//g,function(){return _})+")"),lookbehind:!0,greedy:!0,inside:{string:/^[<"][\s\S]+/,operator:/:/,punctuation:/\./}},"raw-string":{pattern:/R"([^()\\ ]{0,16})\([\s\S]*?\)\1"/,alias:"string",greedy:!0}}),B.languages.insertBefore("cpp","keyword",{"generic-function":{pattern:/\b(?!operator\b)[a-z_]\w*\s*<(?:[^<>]|<[^<>]*>)*>(?=\s*\()/i,inside:{function:/^\w+/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:B.languages.cpp}}}}),B.languages.insertBefore("cpp","operator",{"double-colon":{pattern:/::/,alias:"punctuation"}}),B.languages.insertBefore("cpp","class-name",{"base-clause":{pattern:/(\b(?:class|struct)\s+\w+\s*:\s*)[^;{}"'\s]+(?:\s+[^;{}"'\s]+)*(?=\s*[;{])/,lookbehind:!0,greedy:!0,inside:B.languages.extend("cpp",{})}}),B.languages.insertBefore("inside","double-colon",{"class-name":/\b[a-z_]\w*\b(?!\s*::)/i},B.languages.cpp["base-clause"]),B.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0,greedy:!0},"string-interpolation":{pattern:/(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=\}$)/,lookbehind:!0},"conversion-option":{pattern://,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^[\t ]*)@\w+(?:\.\w+)*/m,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:False|None|True)\b/,number:/\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i,operator:/[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},B.languages.python["string-interpolation"].inside.interpolation.inside.rest=B.languages.python,B.languages.py=B.languages.python,B.languages.json={property:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},B.languages.webmanifest=B.languages.json;var U={},H={dracula:()=>V,duotoneDark:()=>W,duotoneLight:()=>q,github:()=>G,gruvboxMaterialDark:()=>ed,gruvboxMaterialLight:()=>ef,jettwaveDark:()=>el,jettwaveLight:()=>es,nightOwl:()=>Y,nightOwlLight:()=>K,oceanicNext:()=>J,okaidia:()=>ee,oneDark:()=>eu,oneLight:()=>ec,palenight:()=>et,shadesOfPurple:()=>en,synthwave84:()=>er,ultramin:()=>ea,vsDark:()=>eo,vsLight:()=>ei};for(var Q in H)P(U,Q,{get:H[Q],enumerable:!0});var V={plain:{color:"#F8F8F2",backgroundColor:"#282A36"},styles:[{types:["prolog","constant","builtin"],style:{color:"rgb(189, 147, 249)"}},{types:["inserted","function"],style:{color:"rgb(80, 250, 123)"}},{types:["deleted"],style:{color:"rgb(255, 85, 85)"}},{types:["changed"],style:{color:"rgb(255, 184, 108)"}},{types:["punctuation","symbol"],style:{color:"rgb(248, 248, 242)"}},{types:["string","char","tag","selector"],style:{color:"rgb(255, 121, 198)"}},{types:["keyword","variable"],style:{color:"rgb(189, 147, 249)",fontStyle:"italic"}},{types:["comment"],style:{color:"rgb(98, 114, 164)"}},{types:["attr-name"],style:{color:"rgb(241, 250, 140)"}}]},W={plain:{backgroundColor:"#2a2734",color:"#9a86fd"},styles:[{types:["comment","prolog","doctype","cdata","punctuation"],style:{color:"#6c6783"}},{types:["namespace"],style:{opacity:.7}},{types:["tag","operator","number"],style:{color:"#e09142"}},{types:["property","function"],style:{color:"#9a86fd"}},{types:["tag-id","selector","atrule-id"],style:{color:"#eeebff"}},{types:["attr-name"],style:{color:"#c4b9fe"}},{types:["boolean","string","entity","url","attr-value","keyword","control","directive","unit","statement","regex","atrule","placeholder","variable"],style:{color:"#ffcc99"}},{types:["deleted"],style:{textDecorationLine:"line-through"}},{types:["inserted"],style:{textDecorationLine:"underline"}},{types:["italic"],style:{fontStyle:"italic"}},{types:["important","bold"],style:{fontWeight:"bold"}},{types:["important"],style:{color:"#c4b9fe"}}]},q={plain:{backgroundColor:"#faf8f5",color:"#728fcb"},styles:[{types:["comment","prolog","doctype","cdata","punctuation"],style:{color:"#b6ad9a"}},{types:["namespace"],style:{opacity:.7}},{types:["tag","operator","number"],style:{color:"#063289"}},{types:["property","function"],style:{color:"#b29762"}},{types:["tag-id","selector","atrule-id"],style:{color:"#2d2006"}},{types:["attr-name"],style:{color:"#896724"}},{types:["boolean","string","entity","url","attr-value","keyword","control","directive","unit","statement","regex","atrule"],style:{color:"#728fcb"}},{types:["placeholder","variable"],style:{color:"#93abdc"}},{types:["deleted"],style:{textDecorationLine:"line-through"}},{types:["inserted"],style:{textDecorationLine:"underline"}},{types:["italic"],style:{fontStyle:"italic"}},{types:["important","bold"],style:{fontWeight:"bold"}},{types:["important"],style:{color:"#896724"}}]},G={plain:{color:"#393A34",backgroundColor:"#f6f8fa"},styles:[{types:["comment","prolog","doctype","cdata"],style:{color:"#999988",fontStyle:"italic"}},{types:["namespace"],style:{opacity:.7}},{types:["string","attr-value"],style:{color:"#e3116c"}},{types:["punctuation","operator"],style:{color:"#393A34"}},{types:["entity","url","symbol","number","boolean","variable","constant","property","regex","inserted"],style:{color:"#36acaa"}},{types:["atrule","keyword","attr-name","selector"],style:{color:"#00a4db"}},{types:["function","deleted","tag"],style:{color:"#d73a49"}},{types:["function-variable"],style:{color:"#6f42c1"}},{types:["tag","selector","keyword"],style:{color:"#00009f"}}]},Y={plain:{color:"#d6deeb",backgroundColor:"#011627"},styles:[{types:["changed"],style:{color:"rgb(162, 191, 252)",fontStyle:"italic"}},{types:["deleted"],style:{color:"rgba(239, 83, 80, 0.56)",fontStyle:"italic"}},{types:["inserted","attr-name"],style:{color:"rgb(173, 219, 103)",fontStyle:"italic"}},{types:["comment"],style:{color:"rgb(99, 119, 119)",fontStyle:"italic"}},{types:["string","url"],style:{color:"rgb(173, 219, 103)"}},{types:["variable"],style:{color:"rgb(214, 222, 235)"}},{types:["number"],style:{color:"rgb(247, 140, 108)"}},{types:["builtin","char","constant","function"],style:{color:"rgb(130, 170, 255)"}},{types:["punctuation"],style:{color:"rgb(199, 146, 234)"}},{types:["selector","doctype"],style:{color:"rgb(199, 146, 234)",fontStyle:"italic"}},{types:["class-name"],style:{color:"rgb(255, 203, 139)"}},{types:["tag","operator","keyword"],style:{color:"rgb(127, 219, 202)"}},{types:["boolean"],style:{color:"rgb(255, 88, 116)"}},{types:["property"],style:{color:"rgb(128, 203, 196)"}},{types:["namespace"],style:{color:"rgb(178, 204, 214)"}}]},K={plain:{color:"#403f53",backgroundColor:"#FBFBFB"},styles:[{types:["changed"],style:{color:"rgb(162, 191, 252)",fontStyle:"italic"}},{types:["deleted"],style:{color:"rgba(239, 83, 80, 0.56)",fontStyle:"italic"}},{types:["inserted","attr-name"],style:{color:"rgb(72, 118, 214)",fontStyle:"italic"}},{types:["comment"],style:{color:"rgb(152, 159, 177)",fontStyle:"italic"}},{types:["string","builtin","char","constant","url"],style:{color:"rgb(72, 118, 214)"}},{types:["variable"],style:{color:"rgb(201, 103, 101)"}},{types:["number"],style:{color:"rgb(170, 9, 130)"}},{types:["punctuation"],style:{color:"rgb(153, 76, 195)"}},{types:["function","selector","doctype"],style:{color:"rgb(153, 76, 195)",fontStyle:"italic"}},{types:["class-name"],style:{color:"rgb(17, 17, 17)"}},{types:["tag"],style:{color:"rgb(153, 76, 195)"}},{types:["operator","property","keyword","namespace"],style:{color:"rgb(12, 150, 155)"}},{types:["boolean"],style:{color:"rgb(188, 84, 84)"}}]},Z="#c5a5c5",X="#8dc891",J={plain:{backgroundColor:"#282c34",color:"#ffffff"},styles:[{types:["attr-name"],style:{color:Z}},{types:["attr-value"],style:{color:X}},{types:["comment","block-comment","prolog","doctype","cdata","shebang"],style:{color:"#999999"}},{types:["property","number","function-name","constant","symbol","deleted"],style:{color:"#5a9bcf"}},{types:["boolean"],style:{color:"#ff8b50"}},{types:["tag"],style:{color:"#fc929e"}},{types:["string"],style:{color:X}},{types:["punctuation"],style:{color:X}},{types:["selector","char","builtin","inserted"],style:{color:"#D8DEE9"}},{types:["function"],style:{color:"#79b6f2"}},{types:["operator","entity","url","variable"],style:{color:"#d7deea"}},{types:["keyword"],style:{color:Z}},{types:["atrule","class-name"],style:{color:"#FAC863"}},{types:["important"],style:{fontWeight:"400"}},{types:["bold"],style:{fontWeight:"bold"}},{types:["italic"],style:{fontStyle:"italic"}},{types:["namespace"],style:{opacity:.7}}]},ee={plain:{color:"#f8f8f2",backgroundColor:"#272822"},styles:[{types:["changed"],style:{color:"rgb(162, 191, 252)",fontStyle:"italic"}},{types:["deleted"],style:{color:"#f92672",fontStyle:"italic"}},{types:["inserted"],style:{color:"rgb(173, 219, 103)",fontStyle:"italic"}},{types:["comment"],style:{color:"#8292a2",fontStyle:"italic"}},{types:["string","url"],style:{color:"#a6e22e"}},{types:["variable"],style:{color:"#f8f8f2"}},{types:["number"],style:{color:"#ae81ff"}},{types:["builtin","char","constant","function","class-name"],style:{color:"#e6db74"}},{types:["punctuation"],style:{color:"#f8f8f2"}},{types:["selector","doctype"],style:{color:"#a6e22e",fontStyle:"italic"}},{types:["tag","operator","keyword"],style:{color:"#66d9ef"}},{types:["boolean"],style:{color:"#ae81ff"}},{types:["namespace"],style:{color:"rgb(178, 204, 214)",opacity:.7}},{types:["tag","property"],style:{color:"#f92672"}},{types:["attr-name"],style:{color:"#a6e22e !important"}},{types:["doctype"],style:{color:"#8292a2"}},{types:["rule"],style:{color:"#e6db74"}}]},et={plain:{color:"#bfc7d5",backgroundColor:"#292d3e"},styles:[{types:["comment"],style:{color:"rgb(105, 112, 152)",fontStyle:"italic"}},{types:["string","inserted"],style:{color:"rgb(195, 232, 141)"}},{types:["number"],style:{color:"rgb(247, 140, 108)"}},{types:["builtin","char","constant","function"],style:{color:"rgb(130, 170, 255)"}},{types:["punctuation","selector"],style:{color:"rgb(199, 146, 234)"}},{types:["variable"],style:{color:"rgb(191, 199, 213)"}},{types:["class-name","attr-name"],style:{color:"rgb(255, 203, 107)"}},{types:["tag","deleted"],style:{color:"rgb(255, 85, 114)"}},{types:["operator"],style:{color:"rgb(137, 221, 255)"}},{types:["boolean"],style:{color:"rgb(255, 88, 116)"}},{types:["keyword"],style:{fontStyle:"italic"}},{types:["doctype"],style:{color:"rgb(199, 146, 234)",fontStyle:"italic"}},{types:["namespace"],style:{color:"rgb(178, 204, 214)"}},{types:["url"],style:{color:"rgb(221, 221, 221)"}}]},en={plain:{color:"#9EFEFF",backgroundColor:"#2D2A55"},styles:[{types:["changed"],style:{color:"rgb(255, 238, 128)"}},{types:["deleted"],style:{color:"rgba(239, 83, 80, 0.56)"}},{types:["inserted"],style:{color:"rgb(173, 219, 103)"}},{types:["comment"],style:{color:"rgb(179, 98, 255)",fontStyle:"italic"}},{types:["punctuation"],style:{color:"rgb(255, 255, 255)"}},{types:["constant"],style:{color:"rgb(255, 98, 140)"}},{types:["string","url"],style:{color:"rgb(165, 255, 144)"}},{types:["variable"],style:{color:"rgb(255, 238, 128)"}},{types:["number","boolean"],style:{color:"rgb(255, 98, 140)"}},{types:["attr-name"],style:{color:"rgb(255, 180, 84)"}},{types:["keyword","operator","property","namespace","tag","selector","doctype"],style:{color:"rgb(255, 157, 0)"}},{types:["builtin","char","constant","function","class-name"],style:{color:"rgb(250, 208, 0)"}}]},er={plain:{backgroundColor:"linear-gradient(to bottom, #2a2139 75%, #34294f)",backgroundImage:"#34294f",color:"#f92aad",textShadow:"0 0 2px #100c0f, 0 0 5px #dc078e33, 0 0 10px #fff3"},styles:[{types:["comment","block-comment","prolog","doctype","cdata"],style:{color:"#495495",fontStyle:"italic"}},{types:["punctuation"],style:{color:"#ccc"}},{types:["tag","attr-name","namespace","number","unit","hexcode","deleted"],style:{color:"#e2777a"}},{types:["property","selector"],style:{color:"#72f1b8",textShadow:"0 0 2px #100c0f, 0 0 10px #257c5575, 0 0 35px #21272475"}},{types:["function-name"],style:{color:"#6196cc"}},{types:["boolean","selector-id","function"],style:{color:"#fdfdfd",textShadow:"0 0 2px #001716, 0 0 3px #03edf975, 0 0 5px #03edf975, 0 0 8px #03edf975"}},{types:["class-name","maybe-class-name","builtin"],style:{color:"#fff5f6",textShadow:"0 0 2px #000, 0 0 10px #fc1f2c75, 0 0 5px #fc1f2c75, 0 0 25px #fc1f2c75"}},{types:["constant","symbol"],style:{color:"#f92aad",textShadow:"0 0 2px #100c0f, 0 0 5px #dc078e33, 0 0 10px #fff3"}},{types:["important","atrule","keyword","selector-class"],style:{color:"#f4eee4",textShadow:"0 0 2px #393a33, 0 0 8px #f39f0575, 0 0 2px #f39f0575"}},{types:["string","char","attr-value","regex","variable"],style:{color:"#f87c32"}},{types:["parameter"],style:{fontStyle:"italic"}},{types:["entity","url"],style:{color:"#67cdcc"}},{types:["operator"],style:{color:"ffffffee"}},{types:["important","bold"],style:{fontWeight:"bold"}},{types:["italic"],style:{fontStyle:"italic"}},{types:["entity"],style:{cursor:"help"}},{types:["inserted"],style:{color:"green"}}]},ea={plain:{color:"#282a2e",backgroundColor:"#ffffff"},styles:[{types:["comment"],style:{color:"rgb(197, 200, 198)"}},{types:["string","number","builtin","variable"],style:{color:"rgb(150, 152, 150)"}},{types:["class-name","function","tag","attr-name"],style:{color:"rgb(40, 42, 46)"}}]},eo={plain:{color:"#9CDCFE",backgroundColor:"#1E1E1E"},styles:[{types:["prolog"],style:{color:"rgb(0, 0, 128)"}},{types:["comment"],style:{color:"rgb(106, 153, 85)"}},{types:["builtin","changed","keyword","interpolation-punctuation"],style:{color:"rgb(86, 156, 214)"}},{types:["number","inserted"],style:{color:"rgb(181, 206, 168)"}},{types:["constant"],style:{color:"rgb(100, 102, 149)"}},{types:["attr-name","variable"],style:{color:"rgb(156, 220, 254)"}},{types:["deleted","string","attr-value","template-punctuation"],style:{color:"rgb(206, 145, 120)"}},{types:["selector"],style:{color:"rgb(215, 186, 125)"}},{types:["tag"],style:{color:"rgb(78, 201, 176)"}},{types:["tag"],languages:["markup"],style:{color:"rgb(86, 156, 214)"}},{types:["punctuation","operator"],style:{color:"rgb(212, 212, 212)"}},{types:["punctuation"],languages:["markup"],style:{color:"#808080"}},{types:["function"],style:{color:"rgb(220, 220, 170)"}},{types:["class-name"],style:{color:"rgb(78, 201, 176)"}},{types:["char"],style:{color:"rgb(209, 105, 105)"}}]},ei={plain:{color:"#000000",backgroundColor:"#ffffff"},styles:[{types:["comment"],style:{color:"rgb(0, 128, 0)"}},{types:["builtin"],style:{color:"rgb(0, 112, 193)"}},{types:["number","variable","inserted"],style:{color:"rgb(9, 134, 88)"}},{types:["operator"],style:{color:"rgb(0, 0, 0)"}},{types:["constant","char"],style:{color:"rgb(129, 31, 63)"}},{types:["tag"],style:{color:"rgb(128, 0, 0)"}},{types:["attr-name"],style:{color:"rgb(255, 0, 0)"}},{types:["deleted","string"],style:{color:"rgb(163, 21, 21)"}},{types:["changed","punctuation"],style:{color:"rgb(4, 81, 165)"}},{types:["function","keyword"],style:{color:"rgb(0, 0, 255)"}},{types:["class-name"],style:{color:"rgb(38, 127, 153)"}}]},el={plain:{color:"#f8fafc",backgroundColor:"#011627"},styles:[{types:["prolog"],style:{color:"#000080"}},{types:["comment"],style:{color:"#6A9955"}},{types:["builtin","changed","keyword","interpolation-punctuation"],style:{color:"#569CD6"}},{types:["number","inserted"],style:{color:"#B5CEA8"}},{types:["constant"],style:{color:"#f8fafc"}},{types:["attr-name","variable"],style:{color:"#9CDCFE"}},{types:["deleted","string","attr-value","template-punctuation"],style:{color:"#cbd5e1"}},{types:["selector"],style:{color:"#D7BA7D"}},{types:["tag"],style:{color:"#0ea5e9"}},{types:["tag"],languages:["markup"],style:{color:"#0ea5e9"}},{types:["punctuation","operator"],style:{color:"#D4D4D4"}},{types:["punctuation"],languages:["markup"],style:{color:"#808080"}},{types:["function"],style:{color:"#7dd3fc"}},{types:["class-name"],style:{color:"#0ea5e9"}},{types:["char"],style:{color:"#D16969"}}]},es={plain:{color:"#0f172a",backgroundColor:"#f1f5f9"},styles:[{types:["prolog"],style:{color:"#000080"}},{types:["comment"],style:{color:"#6A9955"}},{types:["builtin","changed","keyword","interpolation-punctuation"],style:{color:"#0c4a6e"}},{types:["number","inserted"],style:{color:"#B5CEA8"}},{types:["constant"],style:{color:"#0f172a"}},{types:["attr-name","variable"],style:{color:"#0c4a6e"}},{types:["deleted","string","attr-value","template-punctuation"],style:{color:"#64748b"}},{types:["selector"],style:{color:"#D7BA7D"}},{types:["tag"],style:{color:"#0ea5e9"}},{types:["tag"],languages:["markup"],style:{color:"#0ea5e9"}},{types:["punctuation","operator"],style:{color:"#475569"}},{types:["punctuation"],languages:["markup"],style:{color:"#808080"}},{types:["function"],style:{color:"#0e7490"}},{types:["class-name"],style:{color:"#0ea5e9"}},{types:["char"],style:{color:"#D16969"}}]},eu={plain:{backgroundColor:"hsl(220, 13%, 18%)",color:"hsl(220, 14%, 71%)",textShadow:"0 1px rgba(0, 0, 0, 0.3)"},styles:[{types:["comment","prolog","cdata"],style:{color:"hsl(220, 10%, 40%)"}},{types:["doctype","punctuation","entity"],style:{color:"hsl(220, 14%, 71%)"}},{types:["attr-name","class-name","maybe-class-name","boolean","constant","number","atrule"],style:{color:"hsl(29, 54%, 61%)"}},{types:["keyword"],style:{color:"hsl(286, 60%, 67%)"}},{types:["property","tag","symbol","deleted","important"],style:{color:"hsl(355, 65%, 65%)"}},{types:["selector","string","char","builtin","inserted","regex","attr-value"],style:{color:"hsl(95, 38%, 62%)"}},{types:["variable","operator","function"],style:{color:"hsl(207, 82%, 66%)"}},{types:["url"],style:{color:"hsl(187, 47%, 55%)"}},{types:["deleted"],style:{textDecorationLine:"line-through"}},{types:["inserted"],style:{textDecorationLine:"underline"}},{types:["italic"],style:{fontStyle:"italic"}},{types:["important","bold"],style:{fontWeight:"bold"}},{types:["important"],style:{color:"hsl(220, 14%, 71%)"}}]},ec={plain:{backgroundColor:"hsl(230, 1%, 98%)",color:"hsl(230, 8%, 24%)"},styles:[{types:["comment","prolog","cdata"],style:{color:"hsl(230, 4%, 64%)"}},{types:["doctype","punctuation","entity"],style:{color:"hsl(230, 8%, 24%)"}},{types:["attr-name","class-name","boolean","constant","number","atrule"],style:{color:"hsl(35, 99%, 36%)"}},{types:["keyword"],style:{color:"hsl(301, 63%, 40%)"}},{types:["property","tag","symbol","deleted","important"],style:{color:"hsl(5, 74%, 59%)"}},{types:["selector","string","char","builtin","inserted","regex","attr-value","punctuation"],style:{color:"hsl(119, 34%, 47%)"}},{types:["variable","operator","function"],style:{color:"hsl(221, 87%, 60%)"}},{types:["url"],style:{color:"hsl(198, 99%, 37%)"}},{types:["deleted"],style:{textDecorationLine:"line-through"}},{types:["inserted"],style:{textDecorationLine:"underline"}},{types:["italic"],style:{fontStyle:"italic"}},{types:["important","bold"],style:{fontWeight:"bold"}},{types:["important"],style:{color:"hsl(230, 8%, 24%)"}}]},ed={plain:{color:"#ebdbb2",backgroundColor:"#292828"},styles:[{types:["imports","class-name","maybe-class-name","constant","doctype","builtin","function"],style:{color:"#d8a657"}},{types:["property-access"],style:{color:"#7daea3"}},{types:["tag"],style:{color:"#e78a4e"}},{types:["attr-name","char","url","regex"],style:{color:"#a9b665"}},{types:["attr-value","string"],style:{color:"#89b482"}},{types:["comment","prolog","cdata","operator","inserted"],style:{color:"#a89984"}},{types:["delimiter","boolean","keyword","selector","important","atrule","property","variable","deleted"],style:{color:"#ea6962"}},{types:["entity","number","symbol"],style:{color:"#d3869b"}}]},ef={plain:{color:"#654735",backgroundColor:"#f9f5d7"},styles:[{types:["delimiter","boolean","keyword","selector","important","atrule","property","variable","deleted"],style:{color:"#af2528"}},{types:["imports","class-name","maybe-class-name","constant","doctype","builtin"],style:{color:"#b4730e"}},{types:["string","attr-value"],style:{color:"#477a5b"}},{types:["property-access"],style:{color:"#266b79"}},{types:["function","attr-name","char","url"],style:{color:"#72761e"}},{types:["tag"],style:{color:"#b94c07"}},{types:["comment","prolog","cdata","operator","inserted"],style:{color:"#a89984"}},{types:["entity","number","symbol"],style:{color:"#924f79"}}]},ep=/\r\n|\r|\n/,em=e=>{0===e.length?e.push({types:["plain"],content:"\n",empty:!0}):1===e.length&&""===e[0].content&&(e[0].content="\n",e[0].empty=!0)},eh=(e,t)=>{let n=e.length;return n>0&&e[n-1]===t?e:e.concat(t)},eg=e=>{let t=[[]],n=[e],r=[0],a=[e.length],o=0,i=0,l=[],s=[l];for(;i>-1;){for(;(o=r[i]++)0?u:["plain"],e=c):(u=eh(u,c.type),c.alias&&(u=eh(u,c.alias)),e=c.content),"string"!=typeof e){i++,t.push(u),n.push(e),r.push(0),a.push(e.length);continue}let d=e.split(ep),f=d.length;l.push({types:u,content:d[0]});for(let e=1;e{let o,i=t.toLowerCase(),l=((e,t)=>{let{plain:n}=e,r=e.styles.reduce((e,n)=>{let{languages:r,style:a}=n;return r&&!r.includes(t)||n.types.forEach(t=>{let n=M(M({},e[t]),a);e[t]=n}),e},{});return r.root=n,r.plain=O(M({},n),j({backgroundColor:void 0})),r})(r,i),s=(0,A.useCallback)(e=>{var{className:t,style:n,line:r}=e;let a=O(M({},$(e,["className","style","line"])),j({className:(0,C.A)("token-line",t)}));return"object"==typeof l&&"plain"in l&&(a.style=l.plain),"object"==typeof n&&(a.style=M(M({},a.style||{}),n)),a},[l]),u=(o=(0,A.useCallback)(({types:e,empty:t})=>{if(null!=l)return 1===e.length&&"plain"===e[0]?null!=t?{display:"inline-block"}:void 0:1===e.length&&null!=t?l[e[0]]:Object.assign(null!=t?{display:"inline-block"}:{},...e.map(e=>l[e]))},[l]),(0,A.useCallback)(e=>{var{token:t,className:n,style:r}=e;let a=O(M({},$(e,["token","className","style"])),j({className:(0,C.A)("token",...t.types,n),children:t.content,style:o(t)}));return null!=r&&(a.style=M(M({},a.style||{}),r)),a},[o])),c=a.languages[i];return e({tokens:(({prism:e,code:t,grammar:n,language:r})=>(0,A.useMemo)(()=>{if(null==n)return eg([t]);let a={code:t,grammar:n,language:r,tokens:[]};return e.hooks.run("before-tokenize",a),a.tokens=e.tokenize(t,n),e.hooks.run("after-tokenize",a),eg(a.tokens)},[t,n,r,e]))({prism:a,language:i,code:n,grammar:c}),className:`prism-code language-${i}`,style:null!=l?l.root:{},getLineProps:s,getTokenProps:u})},ey=e=>(0,A.createElement)(eb,O(M({},e),j({prism:e.prism||B,theme:e.theme||eo,code:e.code,language:e.language})))},1561(e,t,n){"use strict";n.d(t,{A:()=>r});function r(e,t){if(!e)throw Error("Invariant failed")}},1635(e,t,n){"use strict";n.r(t),n.d(t,{__addDisposableResource:()=>I,__assign:()=>o,__asyncDelegator:()=>_,__asyncGenerator:()=>E,__asyncValues:()=>A,__await:()=>S,__awaiter:()=>m,__classPrivateFieldGet:()=>j,__classPrivateFieldIn:()=>R,__classPrivateFieldSet:()=>N,__createBinding:()=>g,__decorate:()=>l,__disposeResources:()=>D,__esDecorate:()=>u,__exportStar:()=>b,__extends:()=>a,__generator:()=>h,__importDefault:()=>L,__importStar:()=>O,__makeTemplateObject:()=>C,__metadata:()=>p,__param:()=>s,__propKey:()=>d,__read:()=>v,__rest:()=>i,__rewriteRelativeImportExtension:()=>z,__runInitializers:()=>c,__setFunctionName:()=>f,__spread:()=>k,__spreadArray:()=>x,__spreadArrays:()=>w,__values:()=>y,default:()=>M});var r=function(e,t){return(r=Object.setPrototypeOf||({__proto__:[]})instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)};function a(e,t){if("function"!=typeof t&&null!==t)throw TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}var o=function(){return(o=Object.assign||function(e){for(var t,n=1,r=arguments.length;nt.indexOf(r)&&(n[r]=e[r]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols)for(var a=0,r=Object.getOwnPropertySymbols(e);at.indexOf(r[a])&&Object.prototype.propertyIsEnumerable.call(e,r[a])&&(n[r[a]]=e[r[a]]);return n}function l(e,t,n,r){var a,o=arguments.length,i=o<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)i=Reflect.decorate(e,t,n,r);else for(var l=e.length-1;l>=0;l--)(a=e[l])&&(i=(o<3?a(i):o>3?a(t,n,i):a(t,n))||i);return o>3&&i&&Object.defineProperty(t,n,i),i}function s(e,t){return function(n,r){t(n,r,e)}}function u(e,t,n,r,a,o){function i(e){if(void 0!==e&&"function"!=typeof e)throw TypeError("Function expected");return e}for(var l,s=r.kind,u="getter"===s?"get":"setter"===s?"set":"value",c=!t&&e?r.static?e:e.prototype:null,d=t||(c?Object.getOwnPropertyDescriptor(c,r.name):{}),f=!1,p=n.length-1;p>=0;p--){var m={};for(var h in r)m[h]="access"===h?{}:r[h];for(var h in r.access)m.access[h]=r.access[h];m.addInitializer=function(e){if(f)throw TypeError("Cannot add initializers after decoration has completed");o.push(i(e||null))};var g=(0,n[p])("accessor"===s?{get:d.get,set:d.set}:d[u],m);if("accessor"===s){if(void 0===g)continue;if(null===g||"object"!=typeof g)throw TypeError("Object expected");(l=i(g.get))&&(d.get=l),(l=i(g.set))&&(d.set=l),(l=i(g.init))&&a.unshift(l)}else(l=i(g))&&("field"===s?a.unshift(l):d[u]=l)}c&&Object.defineProperty(c,r.name,d),f=!0}function c(e,t,n){for(var r=arguments.length>2,a=0;a0&&a[a.length-1])&&(6===u[0]||2===u[0])){o=0;continue}if(3===u[0]&&(!a||u[1]>a[0]&&u[1]=e.length&&(e=void 0),{value:e&&e[r++],done:!e}}};throw TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function v(e,t){var n="function"==typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,a,o=n.call(e),i=[];try{for(;(void 0===t||t-- >0)&&!(r=o.next()).done;)i.push(r.value)}catch(e){a={error:e}}finally{try{r&&!r.done&&(n=o.return)&&n.call(o)}finally{if(a)throw a.error}}return i}function k(){for(var e=[],t=0;t1||l(e,t)})},t&&(r[e]=t(r[e])))}function l(e,t){try{var n;(n=a[e](t)).value instanceof S?Promise.resolve(n.value.v).then(s,u):c(o[0][2],n)}catch(e){c(o[0][3],e)}}function s(e){l("next",e)}function u(e){l("throw",e)}function c(e,t){e(t),o.shift(),o.length&&l(o[0][0],o[0][1])}}function _(e){var t,n;return t={},r("next"),r("throw",function(e){throw e}),r("return"),t[Symbol.iterator]=function(){return this},t;function r(r,a){t[r]=e[r]?function(t){return(n=!n)?{value:S(e[r](t)),done:!1}:a?a(t):t}:a}}function A(e){if(!Symbol.asyncIterator)throw TypeError("Symbol.asyncIterator is not defined.");var t,n=e[Symbol.asyncIterator];return n?n.call(e):(e=y(e),t={},r("next"),r("throw"),r("return"),t[Symbol.asyncIterator]=function(){return this},t);function r(n){t[n]=e[n]&&function(t){return new Promise(function(r,a){var o,i,l;o=r,i=a,l=(t=e[n](t)).done,Promise.resolve(t.value).then(function(e){o({value:e,done:l})},i)})}}}function C(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e}var T=Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t},P=function(e){return(P=Object.getOwnPropertyNames||function(e){var t=[];for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[t.length]=n);return t})(e)};function O(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n=P(e),r=0;r{try{return u.getItem(n)}catch(e){return console.error(`Docusaurus storage error, can't get key=${n}`,e),null}},set:e=>{try{let t=u.getItem(n);u.setItem(n,e),i({key:n,oldValue:t,newValue:e,storage:u})}catch(t){console.error(`Docusaurus storage error, can't set ${n}=${e}`,t)}},del:()=>{try{let e=u.getItem(n);u.removeItem(n),i({key:n,oldValue:e,newValue:null,storage:u})}catch(e){console.error(`Docusaurus storage error, can't delete key=${n}`,e)}},listen:e=>{try{let t=t=>{t.storageArea===u&&t.key===n&&e(t)};return window.addEventListener("storage",t),()=>window.removeEventListener("storage",t)}catch(e){return console.error(`Docusaurus storage error, can't listen for changes of key=${n}`,e),()=>{}}}}}function c(e,t){let[n]=(0,r.useState)(()=>null===e?s:u(e,t)),a=(0,r.useCallback)(e=>"u"{}:n.listen(e),[n]);return[(0,r.useSyncExternalStore)(a,()=>n.get(),()=>null),n]}},4563(e,t,n){"use strict";n.d(t,{AL:()=>c,s$:()=>d});var r=n(6540),a=n(4586),o=n(6803),i=n(9532),l=n(4848);let s=({title:e,siteTitle:t,titleDelimiter:n})=>{let r=e?.trim();return r&&r!==t?`${r} ${n} ${t}`:t},u=(0,r.createContext)(null);function c({formatter:e,children:t}){return(0,l.jsx)(u.Provider,{value:e,children:t})}function d(){let e=function(){let e=(0,r.useContext)(u);if(null===e)throw new i.dV("TitleFormatterProvider");return e}(),{siteConfig:t}=(0,a.default)(),{title:n,titleDelimiter:l}=t,{plugin:c}=(0,o.A)();return{format:t=>e({title:t,siteTitle:n,titleDelimiter:l,plugin:c,defaultFormatter:s})}}},2131(e,t,n){"use strict";n.d(t,{o:()=>i});var r=n(4586),a=n(6347),o=n(440);function i(){let{siteConfig:{baseUrl:e,trailingSlash:t},i18n:{localeConfigs:n}}=(0,r.default)(),{pathname:i}=(0,a.zy)(),l=(0,o.applyTrailingSlash)(i,{trailingSlash:t,baseUrl:e}).replace(e,"");return{createUrl:function({locale:e,fullyQualified:t}){let r=function(e){let t=n[e];if(!t)throw Error(`Unexpected Docusaurus bug, no locale config found for locale=${e}`);return t}(e),a=`${t?r.url:""}`,o=r.baseUrl;return`${a}${o}${l}`}}}},5062(e,t,n){"use strict";n.d(t,{$:()=>i});var r=n(6540),a=n(6347),o=n(9532);function i(e){let t=(0,a.zy)(),n=(0,o.ZC)(t),i=(0,o._q)(e);(0,r.useEffect)(()=>{n&&t!==n&&i({location:t,previousLocation:n})},[i,t,n])}},6342(e,t,n){"use strict";n.d(t,{p:()=>a});var r=n(4586);function a(){return(0,r.default)().siteConfig.themeConfig}},2983(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.addTrailingSlash=a,t.default=function(e,t){let{trailingSlash:n,baseUrl:r}=t;if(e.startsWith("#")||void 0===n)return e;let[i]=e.split(/[#?]/),l="/"===i||i===r?i:n?a(i):o(i);return e.replace(i,l)},t.addLeadingSlash=function(e){return(0,r.addPrefix)(e,"/")},t.removeTrailingSlash=o;let r=n(2566);function a(e){return e.endsWith("/")?e:`${e}/`}function o(e){return(0,r.removeSuffix)(e,"/")}},253(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getErrorCausalChain=function e(t){return t.cause?[t,...e(t.cause)]:[t]}},440(e,t,n){"use strict";t.getErrorCausalChain=t.applyTrailingSlash=void 0;let r=n(1635);var a=n(2983);Object.defineProperty(t,"applyTrailingSlash",{enumerable:!0,get:function(){return r.__importDefault(a).default}}),n(2566);var o=n(253);Object.defineProperty(t,"getErrorCausalChain",{enumerable:!0,get:function(){return o.getErrorCausalChain}})},2566(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.addPrefix=function(e,t){return e.startsWith(t)?e:`${t}${e}`},t.removeSuffix=function(e,t){return""===t?e:e.endsWith(t)?e.slice(0,-t.length):e},t.addSuffix=function(e,t){return e.endsWith(t)?e:`${e}${t}`},t.removePrefix=function(e,t){return e.startsWith(t)?e.slice(t.length):e}},7426(e,t,n){"use strict";n.d(t,{A:()=>o}),n(6540);var r=n(4164),a=n(4848);function o({className:e}){return(0,a.jsxs)("div",{className:(0,r.A)("loadingRing_RJI3",e),children:[(0,a.jsx)("div",{}),(0,a.jsx)("div",{}),(0,a.jsx)("div",{}),(0,a.jsx)("div",{})]})}},5148(e,t,n){"use strict";let r;n.d(t,{w:()=>E,k:()=>S});let a=Symbol("Comlink.proxy"),o=Symbol("Comlink.endpoint"),i=Symbol("Comlink.releaseProxy"),l=Symbol("Comlink.finalizer"),s=Symbol("Comlink.thrown"),u=e=>"object"==typeof e&&null!==e||"function"==typeof e,c=new Map([["proxy",{canHandle:e=>u(e)&&e[a],serialize(e){let{port1:t,port2:n}=new MessageChannel;return function e(t,n=globalThis,r=["*"]){n.addEventListener("message",function o(i){let u;if(!i||!i.data)return;if(!function(e,t){for(let n of e)if(t===n||"*"===n||n instanceof RegExp&&n.test(t))return!0;return!1}(r,i.origin))return void console.warn(`Invalid origin '${i.origin}' for comlink proxy`);let{id:c,type:f,path:p}=Object.assign({path:[]},i.data),m=(i.data.argumentList||[]).map(k);try{var h,g,b;let n=p.slice(0,-1).reduce((e,t)=>e[t],t),r=p.reduce((e,t)=>e[t],t);switch(f){case"GET":u=r;break;case"SET":n[p.slice(-1)[0]]=k(i.data.value),u=!0;break;case"APPLY":u=r.apply(n,m);break;case"CONSTRUCT":h=new r(...m),u=Object.assign(h,{[a]:!0});break;case"ENDPOINT":{let{port1:n,port2:r}=new MessageChannel;e(t,r),g=n,b=[n],y.set(g,b),u=g}break;case"RELEASE":u=void 0;break;default:return}}catch(e){u={value:e,[s]:0}}Promise.resolve(u).catch(e=>({value:e,[s]:0})).then(e=>{let[r,a]=v(e);n.postMessage(Object.assign(Object.assign({},r),{id:c}),a),"RELEASE"===f&&(n.removeEventListener("message",o),d(n),l in t&&"function"==typeof t[l]&&t[l]())}).catch(e=>{let[t,r]=v({value:TypeError("Unserializable return value"),[s]:0});n.postMessage(Object.assign(Object.assign({},t),{id:c}),r)})}),n.start&&n.start()}(e,t),[n,[n]]},deserialize:e=>(e.start(),f(e))}],["throw",{canHandle:e=>u(e)&&s in e,serialize:({value:e})=>[e instanceof Error?{isError:!0,value:{message:e.message,name:e.name,stack:e.stack}}:{isError:!1,value:e},[]],deserialize(e){if(e.isError)throw Object.assign(Error(e.value.message),e.value);throw e.value}}]]);function d(e){"MessagePort"===e.constructor.name&&e.close()}function f(e,t){let n=new Map;return e.addEventListener("message",function(e){let{data:t}=e;if(!t||!t.id)return;let r=n.get(t.id);if(r)try{r(t)}finally{n.delete(t.id)}}),function e(t,n,r=[],a=function(){}){let l,s=!1,u=new Proxy(a,{get(a,o){if(p(s),o===i)return()=>{g&&g.unregister(u),m(t),n.clear(),s=!0};if("then"===o){if(0===r.length)return{then:()=>u};let e=w(t,n,{type:"GET",path:r.map(e=>e.toString())}).then(k);return e.then.bind(e)}return e(t,n,[...r,o])},set(e,a,o){p(s);let[i,l]=v(o);return w(t,n,{type:"SET",path:[...r,a].map(e=>e.toString()),value:i},l).then(k)},apply(a,i,l){p(s);let u=r[r.length-1];if(u===o)return w(t,n,{type:"ENDPOINT"}).then(k);if("bind"===u)return e(t,n,r.slice(0,-1));let[c,d]=b(l);return w(t,n,{type:"APPLY",path:r.map(e=>e.toString()),argumentList:c},d).then(k)},construct(e,a){p(s);let[o,i]=b(a);return w(t,n,{type:"CONSTRUCT",path:r.map(e=>e.toString()),argumentList:o},i).then(k)}});return l=(h.get(t)||0)+1,h.set(t,l),g&&g.register(u,t,u),u}(e,n,[],t)}function p(e){if(e)throw Error("Proxy has been released and is not useable")}function m(e){return w(e,new Map,{type:"RELEASE"}).then(()=>{d(e)})}let h=new WeakMap,g="FinalizationRegistry"in globalThis&&new FinalizationRegistry(e=>{let t=(h.get(e)||0)-1;h.set(e,t),0===t&&m(e)});function b(e){var t;let n=e.map(v);return[n.map(e=>e[0]),(t=n.map(e=>e[1]),Array.prototype.concat.apply([],t))]}let y=new WeakMap;function v(e){for(let[t,n]of c)if(n.canHandle(e)){let[r,a]=n.serialize(e);return[{type:"HANDLER",name:t,value:r},a]}return[{type:"RAW",value:e},y.get(e)||[]]}function k(e){switch(e.type){case"HANDLER":return c.get(e.name).deserialize(e.value);case"RAW":return e.value}}function w(e,t,n,r){return new Promise(a=>{let o=[,,,,].fill(0).map(()=>Math.floor(Math.random()*Number.MAX_SAFE_INTEGER).toString(16)).join("-");t.set(o,a),e.start&&e.start(),e.postMessage(Object.assign({id:o},n),r)})}function x(){return r||(r=(async()=>{let e=f(new Worker(new URL(n.p+n.u("9618"),n.b)));return await new e})()),r}async function S(e,t){{let n=await x();await n.fetchIndexes(e,t)}}async function E(e,t,n,r){return(await x()).search(e,t,n,r)}},4471(e,t,n){"use strict";function r(e){return e.join(" \u203A ")}n.d(t,{$:()=>r})},3103(e,t,n){"use strict";function r(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}n.d(t,{Z:()=>r})},7674(e,t,n){"use strict";function r(e,t){let n=[];for(let r of Object.values(e))r[t]&&n.push(...r[t].position);return n.sort((e,t)=>e[0]-t[0]||t[1]-e[1])}n.d(t,{g:()=>r})},6841(e,t,n){"use strict";n.d(t,{Z:()=>function e(t,n,a){let o=[];for(let a of n){let i=t.toLowerCase().indexOf(a);if(i>=0){i>0&&o.push(e(t.substr(0,i),n)),o.push(`${(0,r.Z)(t.substr(i,a.length))}`);let l=i+a.length;l${(0,r.Z)(t)}`:(0,r.Z)(t):o.join("")}});var r=n(3103)},8677(e,t,n){"use strict";n.d(t,{C:()=>l});var r=n(3103),a=n(6841);let o=/\w+|\p{Unified_Ideograph}/u;function i(e){let t=[],n=0,r=e;for(;r.length>0;){let a=r.match(o);if(!a){t.push(r);break}a.index>0&&t.push(r.substring(0,a.index)),t.push(a[0]),n+=a.index+a[0].length,r=e.substring(n)}return t}function l(e,t,n,o=50){let{chunkIndex:s,chunks:u}=function(e,t,n){let o=[],l=0,s=0,u=-1;for(;ls)for(let t of i(e.substring(s,c)).map(e=>({html:(0,r.Z)(e),textLength:e.length})))o.push(t);-1===u&&(u=o.length),s=c+d,o.push({html:(0,a.Z)(e.substring(c,s),n,!0),textLength:d})}}if(s({html:(0,r.Z)(e),textLength:e.length})))o.push(t);return{chunkIndex:u,chunks:o}}(e,t,n),c=u.slice(0,s),d=u[s],f=[d.html],p=u.slice(s+1),m=d.textLength,h=0,g=0,b=!1,y=!1;for(;m0){let e=c.pop();m+e.textLength<=o?(f.unshift(e.html),h+=e.textLength,m+=e.textLength):(b=!0,c.length=0)}else if(p.length>0){let e=p.shift();m+e.textLength<=o?(f.push(e.html),g+=e.textLength,m+=e.textLength):(y=!0,p.length=0)}else break;return(b||c.length>0)&&f.unshift("\u2026"),(y||p.length>0)&&f.push("\u2026"),f.join("")}},3385(e,t,n){"use strict";function r(e,t){if("string"==typeof e)return{label:e,path:e};{let{label:n,path:r}=e;return"string"==typeof n?{label:n,path:r}:Object.prototype.hasOwnProperty.call(n,t)?{label:n[t],path:r}:{label:r,path:r}}}n.d(t,{p:()=>r})},9913(e,t,n){"use strict";var r,a;n.d(t,{i:()=>r}),(a=r||(r={}))[a.Title=0]="Title",a[a.Heading=1]="Heading",a[a.Description=2]="Description",a[a.Keywords=3]="Keywords",a[a.Content=4]="Content"},3328(e,t,n){"use strict";n.d(t,{AO:()=>f,yJ:()=>p,sC:()=>T,TM:()=>A,zR:()=>k});var r=n(8168);function a(e){return"/"===e.charAt(0)}function o(e,t){for(var n=t,r=n+1,a=e.length;r=0;f--){var p=i[f];"."===p?o(i,f):".."===p?(o(i,f),d++):d&&(o(i,f),d--)}if(!u)for(;d--;)i.unshift("..");!u||""===i[0]||i[0]&&a(i[0])||i.unshift("");var m=i.join("/");return n&&"/"!==m.substr(-1)&&(m+="/"),m};var l=n(1561);function s(e){return"/"===e.charAt(0)?e:"/"+e}function u(e){return"/"===e.charAt(0)?e.substr(1):e}function c(e,t){return 0===e.toLowerCase().indexOf(t.toLowerCase())&&-1!=="/?#".indexOf(e.charAt(t.length))?e.substr(t.length):e}function d(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e}function f(e){var t=e.pathname,n=e.search,r=e.hash,a=t||"/";return n&&"?"!==n&&(a+="?"===n.charAt(0)?n:"?"+n),r&&"#"!==r&&(a+="#"===r.charAt(0)?r:"#"+r),a}function p(e,t,n,a){var o,l,s,u,c,d;"string"==typeof e?(s="",u="",-1!==(c=(l=e||"/").indexOf("#"))&&(u=l.substr(c),l=l.substr(0,c)),-1!==(d=l.indexOf("?"))&&(s=l.substr(d),l=l.substr(0,d)),(o={pathname:l,search:"?"===s?"":s,hash:"#"===u?"":u}).state=t):(void 0===(o=(0,r.A)({},e)).pathname&&(o.pathname=""),o.search?"?"!==o.search.charAt(0)&&(o.search="?"+o.search):o.search="",o.hash?"#"!==o.hash.charAt(0)&&(o.hash="#"+o.hash):o.hash="",void 0!==t&&void 0===o.state&&(o.state=t));try{o.pathname=decodeURI(o.pathname)}catch(e){if(e instanceof URIError)throw URIError('Pathname "'+o.pathname+'" could not be decoded. This is likely caused by an invalid percent-encoding.');throw e}return n&&(o.key=n),a?o.pathname?"/"!==o.pathname.charAt(0)&&(o.pathname=i(o.pathname,a.pathname)):o.pathname=a.pathname:o.pathname||(o.pathname="/"),o}function m(){var e=null,t=[];return{setPrompt:function(t){return e=t,function(){e===t&&(e=null)}},confirmTransitionTo:function(t,n,r,a){if(null!=e){var o="function"==typeof e?e(t,n):e;"string"==typeof o?"function"==typeof r?r(o,a):a(!0):a(!1!==o)}else a(!0)},appendListener:function(e){var n=!0;function r(){n&&e.apply(void 0,arguments)}return t.push(r),function(){n=!1,t=t.filter(function(e){return e!==r})}},notifyListeners:function(){for(var e=arguments.length,n=Array(e),r=0;rtypeof window&&window.document&&window.document.createElement);function g(e,t){t(window.confirm(e))}var b="popstate",y="hashchange";function v(){try{return window.history.state||{}}catch(e){return{}}}function k(e){void 0===e&&(e={}),h||(0,l.A)(!1);var t,n=window.history,a=(-1===(t=window.navigator.userAgent).indexOf("Android 2.")&&-1===t.indexOf("Android 4.0")||-1===t.indexOf("Mobile Safari")||-1!==t.indexOf("Chrome")||-1!==t.indexOf("Windows Phone"))&&window.history&&"pushState"in window.history,o=-1!==window.navigator.userAgent.indexOf("Trident"),i=e,u=i.forceRefresh,k=void 0!==u&&u,w=i.getUserConfirmation,x=void 0===w?g:w,S=i.keyLength,E=void 0===S?6:S,_=e.basename?d(s(e.basename)):"";function A(e){var t=e||{},n=t.key,r=t.state,a=window.location,o=a.pathname+a.search+a.hash;return _&&(o=c(o,_)),p(o,r,n)}function C(){return Math.random().toString(36).substr(2,E)}var T=m();function P(e){(0,r.A)(B,e),B.length=n.length,T.notifyListeners(B.location,B.action)}function O(e){(void 0!==e.state||-1!==navigator.userAgent.indexOf("CriOS"))&&N(A(e.state))}function L(){N(A(v()))}var j=!1;function N(e){j?(j=!1,P()):T.confirmTransitionTo(e,"POP",x,function(t){var n,r,a,o,i;t?P({action:"POP",location:e}):(n=e,r=B.location,-1===(a=I.indexOf(r.key))&&(a=0),-1===(o=I.indexOf(n.key))&&(o=0),(i=a-o)&&(j=!0,D(i)))})}var R=A(v()),I=[R.key];function F(e){return _+f(e)}function D(e){n.go(e)}var z=0;function M(e){1===(z+=e)&&1===e?(window.addEventListener(b,O),o&&window.addEventListener(y,L)):0===z&&(window.removeEventListener(b,O),o&&window.removeEventListener(y,L))}var $=!1,B={length:n.length,action:"POP",location:R,createHref:F,push:function(e,t){var r="PUSH",o=p(e,t,C(),B.location);T.confirmTransitionTo(o,r,x,function(e){if(e){var t=F(o),i=o.key,l=o.state;if(a)if(n.pushState({key:i,state:l},null,t),k)window.location.href=t;else{var s=I.indexOf(B.location.key),u=I.slice(0,s+1);u.push(o.key),I=u,P({action:r,location:o})}else window.location.href=t}})},replace:function(e,t){var r="REPLACE",o=p(e,t,C(),B.location);T.confirmTransitionTo(o,r,x,function(e){if(e){var t=F(o),i=o.key,l=o.state;if(a)if(n.replaceState({key:i,state:l},null,t),k)window.location.replace(t);else{var s=I.indexOf(B.location.key);-1!==s&&(I[s]=o.key),P({action:r,location:o})}else window.location.replace(t)}})},go:D,goBack:function(){D(-1)},goForward:function(){D(1)},block:function(e){void 0===e&&(e=!1);var t=T.setPrompt(e);return $||(M(1),$=!0),function(){return $&&($=!1,M(-1)),t()}},listen:function(e){var t=T.appendListener(e);return M(1),function(){M(-1),t()}}};return B}var w="hashchange",x={hashbang:{encodePath:function(e){return"!"===e.charAt(0)?e:"!/"+u(e)},decodePath:function(e){return"!"===e.charAt(0)?e.substr(1):e}},noslash:{encodePath:u,decodePath:s},slash:{encodePath:s,decodePath:s}};function S(e){var t=e.indexOf("#");return -1===t?e:e.slice(0,t)}function E(){var e=window.location.href,t=e.indexOf("#");return -1===t?"":e.substring(t+1)}function _(e){window.location.replace(S(window.location.href)+"#"+e)}function A(e){void 0===e&&(e={}),h||(0,l.A)(!1);var t=window.history;window.navigator.userAgent.indexOf("Firefox");var n=e,a=n.getUserConfirmation,o=void 0===a?g:a,i=n.hashType,u=e.basename?d(s(e.basename)):"",b=x[void 0===i?"slash":i],y=b.encodePath,v=b.decodePath;function k(){var e=v(E());return u&&(e=c(e,u)),p(e)}var A=m();function C(e){(0,r.A)(M,e),M.length=t.length,A.notifyListeners(M.location,M.action)}var T=!1,P=null;function O(){var e=E(),t=y(e);if(e!==t)_(t);else{var n,r=k(),a=M.location;if(!T&&a.pathname===r.pathname&&a.search===r.search&&a.hash===r.hash||P===f(r))return;P=null,n=r,T?(T=!1,C()):A.confirmTransitionTo(n,"POP",o,function(e){var t,r,a,o,i;e?C({action:"POP",location:n}):(t=n,r=M.location,-1===(a=R.lastIndexOf(f(r)))&&(a=0),-1===(o=R.lastIndexOf(f(t)))&&(o=0),(i=a-o)&&(T=!0,I(i)))})}}var L=E(),j=y(L);L!==j&&_(j);var N=k(),R=[f(N)];function I(e){t.go(e)}var F=0;function D(e){1===(F+=e)&&1===e?window.addEventListener(w,O):0===F&&window.removeEventListener(w,O)}var z=!1,M={length:t.length,action:"POP",location:N,createHref:function(e){var t=document.querySelector("base"),n="";return t&&t.getAttribute("href")&&(n=S(window.location.href)),n+"#"+y(u+f(e))},push:function(e,t){var n="PUSH",r=p(e,void 0,void 0,M.location);A.confirmTransitionTo(r,n,o,function(e){if(e){var t=f(r),a=y(u+t);if(E()!==a){P=t,window.location.hash=a;var o=R.lastIndexOf(f(M.location)),i=R.slice(0,o+1);i.push(t),R=i,C({action:n,location:r})}else C()}})},replace:function(e,t){var n="REPLACE",r=p(e,void 0,void 0,M.location);A.confirmTransitionTo(r,n,o,function(e){if(e){var t=f(r),a=y(u+t);E()!==a&&(P=t,_(a));var o=R.indexOf(f(M.location));-1!==o&&(R[o]=t),C({action:n,location:r})}})},go:I,goBack:function(){I(-1)},goForward:function(){I(1)},block:function(e){void 0===e&&(e=!1);var t=A.setPrompt(e);return z||(D(1),z=!0),function(){return z&&(z=!1,D(-1)),t()}},listen:function(e){var t=A.appendListener(e);return D(1),function(){D(-1),t()}}};return M}function C(e,t,n){return Math.min(Math.max(e,t),n)}function T(e){void 0===e&&(e={});var t=e,n=t.getUserConfirmation,a=t.initialEntries,o=void 0===a?["/"]:a,i=t.initialIndex,l=t.keyLength,s=void 0===l?6:l,u=m();function c(e){(0,r.A)(y,e),y.length=y.entries.length,u.notifyListeners(y.location,y.action)}function d(){return Math.random().toString(36).substr(2,s)}var h=C(void 0===i?0:i,0,o.length-1),g=o.map(function(e){return"string"==typeof e?p(e,void 0,d()):p(e,void 0,e.key||d())});function b(e){var t=C(y.index+e,0,y.entries.length-1),r=y.entries[t];u.confirmTransitionTo(r,"POP",n,function(e){e?c({action:"POP",location:r,index:t}):c()})}var y={length:g.length,action:"POP",location:g[h],index:h,entries:g,createHref:f,push:function(e,t){var r="PUSH",a=p(e,t,d(),y.location);u.confirmTransitionTo(a,r,n,function(e){if(e){var t=y.index+1,n=y.entries.slice(0);n.length>t?n.splice(t,n.length-t,a):n.push(a),c({action:r,location:a,index:t,entries:n})}})},replace:function(e,t){var r="REPLACE",a=p(e,t,d(),y.location);u.confirmTransitionTo(a,r,n,function(e){e&&(y.entries[y.index]=a,c({action:r,location:a}))})},go:b,goBack:function(){b(-1)},goForward:function(){b(1)},canGo:function(e){var t=y.index+e;return t>=0&&t0){var u=L.utils.clone(t)||{};u.position=[i,s],u.index=a.length,a.push(new L.Token(n.slice(i,o),u))}i=o+1}}return a},L.tokenizer.separator=/[\s\-]+/,L.Pipeline=function(){this._stack=[]},L.Pipeline.registeredFunctions=Object.create(null),L.Pipeline.registerFunction=function(e,t){t in this.registeredFunctions&&L.utils.warn("Overwriting existing registered function: "+t),e.label=t,L.Pipeline.registeredFunctions[e.label]=e},L.Pipeline.warnIfFunctionNotRegistered=function(e){e.label&&e.label in this.registeredFunctions||L.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},L.Pipeline.load=function(e){var t=new L.Pipeline;return e.forEach(function(e){var n=L.Pipeline.registeredFunctions[e];if(n)t.add(n);else throw Error("Cannot load unregistered function: "+e)}),t},L.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){L.Pipeline.warnIfFunctionNotRegistered(e),this._stack.push(e)},this)},L.Pipeline.prototype.after=function(e,t){L.Pipeline.warnIfFunctionNotRegistered(t);var n=this._stack.indexOf(e);if(-1==n)throw Error("Cannot find existingFn");n+=1,this._stack.splice(n,0,t)},L.Pipeline.prototype.before=function(e,t){L.Pipeline.warnIfFunctionNotRegistered(t);var n=this._stack.indexOf(e);if(-1==n)throw Error("Cannot find existingFn");this._stack.splice(n,0,t)},L.Pipeline.prototype.remove=function(e){var t=this._stack.indexOf(e);-1!=t&&this._stack.splice(t,1)},L.Pipeline.prototype.run=function(e){for(var t=this._stack.length,n=0;n1&&(oe&&(n=a),o!=e);)r=n-t,a=t+Math.floor(r/2),o=this.elements[2*a];return o==e||o>e?2*a:ol?u+=2:i==l&&(t+=n[s+1]*r[u+1],s+=2,u+=2);return t},L.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},L.Vector.prototype.toArray=function(){for(var e=Array(this.elements.length/2),t=1,n=0;t0){var o,i=a.str.charAt(0);i in a.node.edges?o=a.node.edges[i]:(o=new L.TokenSet,a.node.edges[i]=o),1==a.str.length&&(o.final=!0),r.push({node:o,editsRemaining:a.editsRemaining,str:a.str.slice(1)})}if(0!=a.editsRemaining){if("*"in a.node.edges)var l=a.node.edges["*"];else{var l=new L.TokenSet;a.node.edges["*"]=l}if(0==a.str.length&&(l.final=!0),r.push({node:l,editsRemaining:a.editsRemaining-1,str:a.str}),a.str.length>1&&r.push({node:a.node,editsRemaining:a.editsRemaining-1,str:a.str.slice(1)}),1==a.str.length&&(a.node.final=!0),a.str.length>=1){if("*"in a.node.edges)var s=a.node.edges["*"];else{var s=new L.TokenSet;a.node.edges["*"]=s}1==a.str.length&&(s.final=!0),r.push({node:s,editsRemaining:a.editsRemaining-1,str:a.str.slice(1)})}if(a.str.length>1){var u,c=a.str.charAt(0),d=a.str.charAt(1);d in a.node.edges?u=a.node.edges[d]:(u=new L.TokenSet,a.node.edges[d]=u),1==a.str.length&&(u.final=!0),r.push({node:u,editsRemaining:a.editsRemaining-1,str:c+a.str.slice(2)})}}}return n},L.TokenSet.fromString=function(e){for(var t=new L.TokenSet,n=t,r=0,a=e.length;r=e;t--){var n=this.uncheckedNodes[t],r=n.child.toString();r in this.minimizedNodes?n.parent.edges[n.char]=this.minimizedNodes[r]:(n.child._str=r,this.minimizedNodes[r]=n.child),this.uncheckedNodes.pop()}},L.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},L.Index.prototype.search=function(e){return this.query(function(t){new L.QueryParser(e,t).parse()})},L.Index.prototype.query=function(e){for(var t=new L.Query(this.fields),n=Object.create(null),r=Object.create(null),a=Object.create(null),o=Object.create(null),i=Object.create(null),l=0;l1?this._b=1:this._b=e},L.Builder.prototype.k1=function(e){this._k1=e},L.Builder.prototype.add=function(e,t){var n=e[this._ref],r=Object.keys(this._fields);this._documents[n]=t||{},this.documentCount+=1;for(var a=0;a=this.length)return L.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},L.QueryLexer.prototype.width=function(){return this.pos-this.start},L.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},L.QueryLexer.prototype.backup=function(){this.pos-=1},L.QueryLexer.prototype.acceptDigitRun=function(){var e,t;do t=(e=this.next()).charCodeAt(0);while(t>47&&t<58)e!=L.QueryLexer.EOS&&this.backup()},L.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(L.QueryLexer.TERM)),e.ignore(),e.more())return L.QueryLexer.lexText},L.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(L.QueryLexer.EDIT_DISTANCE),L.QueryLexer.lexText},L.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(L.QueryLexer.BOOST),L.QueryLexer.lexText},L.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(L.QueryLexer.TERM)},L.QueryLexer.termSeparator=L.tokenizer.separator,L.QueryLexer.lexText=function(e){for(;;){var t=e.next();if(t==L.QueryLexer.EOS)return L.QueryLexer.lexEOS;if(92==t.charCodeAt(0)){e.escapeCharacter();continue}if(":"==t)return L.QueryLexer.lexField;if("~"==t)return e.backup(),e.width()>0&&e.emit(L.QueryLexer.TERM),L.QueryLexer.lexEditDistance;if("^"==t)return e.backup(),e.width()>0&&e.emit(L.QueryLexer.TERM),L.QueryLexer.lexBoost;if("+"==t&&1===e.width()||"-"==t&&1===e.width())return e.emit(L.QueryLexer.PRESENCE),L.QueryLexer.lexText;if(t.match(L.QueryLexer.termSeparator))return L.QueryLexer.lexTerm}},L.QueryParser=function(e,t){this.lexer=new L.QueryLexer(e),this.query=t,this.currentClause={},this.lexemeIdx=0},L.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=L.QueryParser.parseClause;e;)e=e(this);return this.query},L.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},L.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},L.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},L.QueryParser.parseClause=function(e){var t=e.peekLexeme();if(void 0!=t)switch(t.type){case L.QueryLexer.PRESENCE:return L.QueryParser.parsePresence;case L.QueryLexer.FIELD:return L.QueryParser.parseField;case L.QueryLexer.TERM:return L.QueryParser.parseTerm;default:var n="expected either a field or a term, found "+t.type;throw t.str.length>=1&&(n+=" with value '"+t.str+"'"),new L.QueryParseError(n,t.start,t.end)}},L.QueryParser.parsePresence=function(e){var t=e.consumeLexeme();if(void 0!=t){switch(t.str){case"-":e.currentClause.presence=L.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=L.Query.presence.REQUIRED;break;default:var n="unrecognised presence operator'"+t.str+"'";throw new L.QueryParseError(n,t.start,t.end)}var r=e.peekLexeme();if(void 0==r){var n="expecting term or field, found nothing";throw new L.QueryParseError(n,t.start,t.end)}switch(r.type){case L.QueryLexer.FIELD:return L.QueryParser.parseField;case L.QueryLexer.TERM:return L.QueryParser.parseTerm;default:var n="expecting term or field, found '"+r.type+"'";throw new L.QueryParseError(n,r.start,r.end)}}},L.QueryParser.parseField=function(e){var t=e.consumeLexeme();if(void 0!=t){if(-1==e.query.allFields.indexOf(t.str)){var n=e.query.allFields.map(function(e){return"'"+e+"'"}).join(", "),r="unrecognised field '"+t.str+"', possible fields: "+n;throw new L.QueryParseError(r,t.start,t.end)}e.currentClause.fields=[t.str];var a=e.peekLexeme();if(void 0==a){var r="expecting term, found nothing";throw new L.QueryParseError(r,t.start,t.end)}if(a.type===L.QueryLexer.TERM)return L.QueryParser.parseTerm;var r="expecting term, found '"+a.type+"'";throw new L.QueryParseError(r,a.start,a.end)}},L.QueryParser.parseTerm=function(e){var t=e.consumeLexeme();if(void 0!=t){e.currentClause.term=t.str.toLowerCase(),-1!=t.str.indexOf("*")&&(e.currentClause.usePipeline=!1);var n=e.peekLexeme();if(void 0==n)return void e.nextClause();switch(n.type){case L.QueryLexer.TERM:return e.nextClause(),L.QueryParser.parseTerm;case L.QueryLexer.FIELD:return e.nextClause(),L.QueryParser.parseField;case L.QueryLexer.EDIT_DISTANCE:return L.QueryParser.parseEditDistance;case L.QueryLexer.BOOST:return L.QueryParser.parseBoost;case L.QueryLexer.PRESENCE:return e.nextClause(),L.QueryParser.parsePresence;default:var r="Unexpected lexeme type '"+n.type+"'";throw new L.QueryParseError(r,n.start,n.end)}}},L.QueryParser.parseEditDistance=function(e){var t=e.consumeLexeme();if(void 0!=t){var n=parseInt(t.str,10);if(isNaN(n)){var r="edit distance must be numeric";throw new L.QueryParseError(r,t.start,t.end)}e.currentClause.editDistance=n;var a=e.peekLexeme();if(void 0==a)return void e.nextClause();switch(a.type){case L.QueryLexer.TERM:return e.nextClause(),L.QueryParser.parseTerm;case L.QueryLexer.FIELD:return e.nextClause(),L.QueryParser.parseField;case L.QueryLexer.EDIT_DISTANCE:return L.QueryParser.parseEditDistance;case L.QueryLexer.BOOST:return L.QueryParser.parseBoost;case L.QueryLexer.PRESENCE:return e.nextClause(),L.QueryParser.parsePresence;default:var r="Unexpected lexeme type '"+a.type+"'";throw new L.QueryParseError(r,a.start,a.end)}}},L.QueryParser.parseBoost=function(e){var t=e.consumeLexeme();if(void 0!=t){var n=parseInt(t.str,10);if(isNaN(n)){var r="boost must be numeric";throw new L.QueryParseError(r,t.start,t.end)}e.currentClause.boost=n;var a=e.peekLexeme();if(void 0==a)return void e.nextClause();switch(a.type){case L.QueryLexer.TERM:return e.nextClause(),L.QueryParser.parseTerm;case L.QueryLexer.FIELD:return e.nextClause(),L.QueryParser.parseField;case L.QueryLexer.EDIT_DISTANCE:return L.QueryParser.parseEditDistance;case L.QueryLexer.BOOST:return L.QueryParser.parseBoost;case L.QueryLexer.PRESENCE:return e.nextClause(),L.QueryParser.parsePresence;default:var r="Unexpected lexeme type '"+a.type+"'";throw new L.QueryParseError(r,a.start,a.end)}}},O=function(){return L},"function"==typeof define&&define.amd?define(O):e.exports=O()}()},5947(e){var t;t=function(){var e,t,n,r={};r.version="0.2.0";var a=r.settings={minimum:.08,easing:"ease",positionUsing:"",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,showSpinner:!0,barSelector:'[role="bar"]',spinnerSelector:'[role="spinner"]',parent:"body",template:''};function o(e,t,n){return en?n:e}r.configure=function(e){var t,n;for(t in e)void 0!==(n=e[t])&&e.hasOwnProperty(t)&&(a[t]=n);return this},r.status=null,r.set=function(e){var t=r.isStarted();r.status=1===(e=o(e,a.minimum,1))?null:e;var n=r.render(!t),s=n.querySelector(a.barSelector),u=a.speed,c=a.easing;return n.offsetWidth,i(function(t){var o,i,d,f;""===a.positionUsing&&(a.positionUsing=r.getPositioningCSS()),l(s,(o=e,i=u,d=c,(f="translate3d"===a.positionUsing?{transform:"translate3d("+(-1+o)*100+"%,0,0)"}:"translate"===a.positionUsing?{transform:"translate("+(-1+o)*100+"%,0)"}:{"margin-left":(-1+o)*100+"%"}).transition="all "+i+"ms "+d,f)),1===e?(l(n,{transition:"none",opacity:1}),n.offsetWidth,setTimeout(function(){l(n,{transition:"all "+u+"ms linear",opacity:0}),setTimeout(function(){r.remove(),t()},u)},u)):setTimeout(t,u)}),this},r.isStarted=function(){return"number"==typeof r.status},r.start=function(){r.status||r.set(0);var e=function(){setTimeout(function(){r.status&&(r.trickle(),e())},a.trickleSpeed)};return a.trickle&&e(),this},r.done=function(e){return e||r.status?r.inc(.3+.5*Math.random()).set(1):this},r.inc=function(e){var t=r.status;return t?("number"!=typeof e&&(e=(1-t)*o(Math.random()*t,.1,.95)),t=o(t+e,0,.994),r.set(t)):r.start()},r.trickle=function(){return r.inc(Math.random()*a.trickleRate)},e=0,t=0,r.promise=function(n){return n&&"resolved"!==n.state()&&(0===t&&r.start(),e++,t++,n.always(function(){0==--t?(e=0,r.done()):r.set((e-t)/e)})),this},r.render=function(e){if(r.isRendered())return document.getElementById("nprogress");u(document.documentElement,"nprogress-busy");var t=document.createElement("div");t.id="nprogress",t.innerHTML=a.template;var n,o=t.querySelector(a.barSelector),i=e?"-100":(-1+(r.status||0))*100,s=document.querySelector(a.parent);return l(o,{transition:"all 0 linear",transform:"translate3d("+i+"%,0,0)"}),!a.showSpinner&&(n=t.querySelector(a.spinnerSelector))&&f(n),s!=document.body&&u(s,"nprogress-custom-parent"),s.appendChild(t),t},r.remove=function(){c(document.documentElement,"nprogress-busy"),c(document.querySelector(a.parent),"nprogress-custom-parent");var e=document.getElementById("nprogress");e&&f(e)},r.isRendered=function(){return!!document.getElementById("nprogress")},r.getPositioningCSS=function(){var e=document.body.style,t="WebkitTransform"in e?"Webkit":"MozTransform"in e?"Moz":"msTransform"in e?"ms":"OTransform"in e?"O":"";return t+"Perspective"in e?"translate3d":t+"Transform"in e?"translate":"margin"};var i=(n=[],function(e){n.push(e),1==n.length&&function e(){var t=n.shift();t&&t(e)}()}),l=function(){var e=["Webkit","O","Moz","ms"],t={};function n(n,r,a){var o;r=t[o=(o=r).replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(e,t){return t.toUpperCase()})]||(t[o]=function(t){var n=document.body.style;if(t in n)return t;for(var r,a=e.length,o=t.charAt(0).toUpperCase()+t.slice(1);a--;)if((r=e[a]+o)in n)return r;return t}(o)),n.style[r]=a}return function(e,t){var r,a,o=arguments;if(2==o.length)for(r in t)void 0!==(a=t[r])&&t.hasOwnProperty(r)&&n(e,r,a);else n(e,o[1],o[2])}}();function s(e,t){return("string"==typeof e?e:d(e)).indexOf(" "+t+" ")>=0}function u(e,t){var n=d(e),r=n+t;s(n,t)||(e.className=r.substring(1))}function c(e,t){var n,r=d(e);s(e,t)&&(e.className=(n=r.replace(" "+t+" "," ")).substring(1,n.length-1))}function d(e){return(" "+(e.className||"")+" ").replace(/\s+/gi," ")}function f(e){e&&e.parentNode&&e.parentNode.removeChild(e)}return r},"function"==typeof define&&define.amd?define(t):e.exports=t()},5302(e,t,n){var r=n(4634);e.exports=function e(t,n,a){if(r(n)||(a=n||a,n=[]),a=a||{},t instanceof RegExp){var i,l,s=n,d=t.source.match(/\((?!\?)/g);if(d)for(var f=0;f-1?"[^"+s(c)+"]+?":s(d)+"|(?:(?!"+s(d)+")[^"+s(c)+"])+?")})}return i>=?|&&|\|\||[=!+\-*/%<>^&|]=?|[?~:]/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)[\w-]+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b[\w-]+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?:\.\w+)*(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},parameter:{pattern:/(^|\s)-{1,2}(?:\w+:[+-]?)?\w+(?:\.\w+)*(?=[=\s]|$)/,alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+)\s[\s\S]*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:r},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s[\s\S]*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0,inside:{bash:n}},{pattern:/(^|[^\\](?:\\\\)*)"(?:\\[\s\S]|\$\([^)]+\)|\$(?!\()|`[^`]+`|[^"\\`$])*"/,lookbehind:!0,greedy:!0,inside:r},{pattern:/(^|[^$\\])'[^']*'/,lookbehind:!0,greedy:!0},{pattern:/\$'(?:[^'\\]|\\[\s\S])*'/,greedy:!0,inside:{entity:r.entity}}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:r.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:false|true)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|=[=~]?|!=?|<<[<-]?|[&\d]?>>|\d[<>]&?|[<>][&=]?|&[>&]?|\|[&|]?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}},n.inside=e.languages.bash;for(var a=["comment","function-name","for-or-select","assign-left","parameter","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],o=r.variable[1].inside,i=0;i",unchanged:" ",diff:"!"}).forEach(function(n){var r=t[n],a=[];/^\w+$/.test(n)||a.push(/\w+/.exec(n)[0]),"diff"===n&&a.push("bold"),e.languages.diff[n]={pattern:RegExp("^(?:["+r+"].*(?:\r\n?|\n|(?![\\s\\S])))+","m"),alias:a,inside:{line:{pattern:/(.)(?=[\s\S]).*(?:\r\n?|\n)?/,lookbehind:!0},prefix:{pattern:/[\s\S]/,alias:/\w+/.exec(n)[0]}}}}),Object.defineProperty(e.languages.diff,"PREFIXES",{value:t})},568(){!function(e){var t=/\\[\r\n](?:\s|\\[\r\n]|#.*(?!.))*(?![\s#]|\\[\r\n])/.source,n=/(?:[ \t]+(?![ \t])(?:)?|)/.source.replace(//g,function(){return t}),r=/"(?:[^"\\\r\n]|\\(?:\r\n|[\s\S]))*"|'(?:[^'\\\r\n]|\\(?:\r\n|[\s\S]))*'/.source,a=/--[\w-]+=(?:|(?!["'])(?:[^\s\\]|\\.)+)/.source.replace(//g,function(){return r}),o={pattern:RegExp(r),greedy:!0},i={pattern:/(^[ \t]*)#.*/m,lookbehind:!0,greedy:!0};function l(e,t){return RegExp(e=e.replace(//g,function(){return a}).replace(//g,function(){return n}),t)}e.languages.docker={instruction:{pattern:/(^[ \t]*)(?:ADD|ARG|CMD|COPY|ENTRYPOINT|ENV|EXPOSE|FROM|HEALTHCHECK|LABEL|MAINTAINER|ONBUILD|RUN|SHELL|STOPSIGNAL|USER|VOLUME|WORKDIR)(?=\s)(?:\\.|[^\r\n\\])*(?:\\$(?:\s|#.*$)*(?![\s#])(?:\\.|[^\r\n\\])*)*/im,lookbehind:!0,greedy:!0,inside:{options:{pattern:l(/(^(?:ONBUILD)?\w+)(?:)*/.source,"i"),lookbehind:!0,greedy:!0,inside:{property:{pattern:/(^|\s)--[\w-]+/,lookbehind:!0},string:[o,{pattern:/(=)(?!["'])(?:[^\s\\]|\\.)+/,lookbehind:!0}],operator:/\\$/m,punctuation:/=/}},keyword:[{pattern:l(/(^(?:ONBUILD)?HEALTHCHECK(?:)*)(?:CMD|NONE)\b/.source,"i"),lookbehind:!0,greedy:!0},{pattern:l(/(^(?:ONBUILD)?FROM(?:)*(?!--)[^ \t\\]+)AS/.source,"i"),lookbehind:!0,greedy:!0},{pattern:l(/(^ONBUILD)\w+/.source,"i"),lookbehind:!0,greedy:!0},{pattern:/^\w+/,greedy:!0}],comment:i,string:o,variable:/\$(?:\w+|\{[^{}"'\\]*\})/,operator:/\\$/m}},comment:i},e.languages.dockerfile=e.languages.docker}(Prism)},824(){Prism.languages.ini={comment:{pattern:/(^[ \f\t\v]*)[#;][^\n\r]*/m,lookbehind:!0},section:{pattern:/(^[ \f\t\v]*)\[[^\n\r\]]*\]?/m,lookbehind:!0,inside:{"section-name":{pattern:/(^\[[ \f\t\v]*)[^ \f\t\v\]]+(?:[ \f\t\v]+[^ \f\t\v\]]+)*/,lookbehind:!0,alias:"selector"},punctuation:/\[|\]/}},key:{pattern:/(^[ \f\t\v]*)[^ \f\n\r\t\v=]+(?:[ \f\t\v]+[^ \f\n\r\t\v=]+)*(?=[ \f\t\v]*=)/m,lookbehind:!0,alias:"attr-name"},value:{pattern:/(=[ \f\t\v]*)[^ \f\n\r\t\v]+(?:[ \f\t\v]+[^ \f\n\r\t\v]+)*/,lookbehind:!0,alias:"attr-value",inside:{"inner-value":{pattern:/^("|').+(?=\1$)/,lookbehind:!0}}},punctuation:/=/}},2514(){Prism.languages.json={property:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},Prism.languages.webmanifest=Prism.languages.json},267(){var e,t;t=/("|')(?:\\(?:\r\n?|\n|.)|(?!\1)[^\\\r\n])*\1/,(e=Prism).languages.json5=e.languages.extend("json",{property:[{pattern:RegExp(t.source+"(?=\\s*:)"),greedy:!0},{pattern:/(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/,alias:"unquoted"}],string:{pattern:t,greedy:!0},number:/[+-]?\b(?:NaN|Infinity|0x[a-fA-F\d]+)\b|[+-]?(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[eE][+-]?\d+\b)?/})},9700(){var e=Prism;function t(e,t){return"___"+e.toUpperCase()+t+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(n,r,a,o){if(n.language===r){var i=n.tokenStack=[];n.code=n.code.replace(a,function(e){if("function"==typeof o&&!o(e))return e;for(var a,l=i.length;-1!==n.code.indexOf(a=t(r,l));)++l;return i[l]=e,a}),n.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(n,r){if(n.language===r&&n.tokenStack){n.grammar=e.languages[r];var a=0,o=Object.keys(n.tokenStack);!function i(l){for(var s=0;s=o.length);s++){var u=l[s];if("string"==typeof u||u.content&&"string"==typeof u.content){var c=o[a],d=n.tokenStack[c],f="string"==typeof u?u:u.content,p=t(r,c),m=f.indexOf(p);if(m>-1){++a;var h=f.substring(0,m),g=new e.Token(r,e.tokenize(d,n.grammar),"language-"+r,d),b=f.substring(m+p.length),y=[];h&&y.push.apply(y,i([h])),y.push(g),b&&y.push.apply(y,i([b])),"string"==typeof u?l.splice.apply(l,[s,1].concat(y)):u.content=y}}else u.content&&i(u.content)}return l}(n.tokens)}}}})},9587(){var e,t;(e=Prism).languages.typescript=e.languages.extend("javascript",{"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|type)\s+)(?!keyof\b)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?:\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/,lookbehind:!0,greedy:!0,inside:null},builtin:/\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\b/}),e.languages.typescript.keyword.push(/\b(?:abstract|declare|is|keyof|readonly|require)\b/,/\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/,/\btype\b(?=\s*(?:[\{*]|$))/),delete e.languages.typescript.parameter,delete e.languages.typescript["literal-property"],t=e.languages.extend("typescript",{}),delete t["class-name"],e.languages.typescript["class-name"].inside=t,e.languages.insertBefore("typescript","function",{decorator:{pattern:/@[$\w\xA0-\uFFFF]+/,inside:{at:{pattern:/^@/,alias:"operator"},function:/^[\s\S]+/}},"generic-function":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\s*\()/,greedy:!0,inside:{function:/^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:t}}}}),e.languages.ts=e.languages.typescript},4230(e,t,n){var r={"./prism-bash":"7022","./prism-diff":"7839","./prism-docker":"568","./prism-ini":"824","./prism-json":"2514","./prism-json5":"267","./prism-typescript":"9587"};function a(e){return n(o(e))}function o(e){if(!n.o(r,e)){var t=Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return r[e]}a.keys=function(){return Object.keys(r)},a.resolve=o,e.exports=a,a.id=4230},2694(e,t,n){"use strict";var r=n(6925);function a(){}function o(){}o.resetWarningCache=a,e.exports=function(){function e(e,t,n,a,o,i){if(i!==r){var l=Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw l.name="Invariant Violation",l}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:o,resetWarningCache:a};return n.PropTypes=n,n}},5556(e,t,n){e.exports=n(2694)()},6925(e){"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},1247(e,t,n){"use strict";var r,a=n(9982),o=n(6540),i=n(961);function l(e){var t="https://react.dev/errors/"+e;if(1D||(e.current=F[D],F[D]=null,D--)}function $(e,t){F[++D]=e.current,e.current=t}var B=z(null),U=z(null),H=z(null),Q=z(null);function V(e,t){switch($(H,t),$(U,e),$(B,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?ui(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)e=ul(t=ui(t),e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}M(B),$(B,e)}function W(){M(B),M(U),M(H)}function q(e){null!==e.memoizedState&&$(Q,e);var t=B.current,n=ul(t,e.type);t!==n&&($(U,e),$(B,n))}function G(e){U.current===e&&(M(B),M(U)),Q.current===e&&(M(Q),uK._currentValue=I)}var Y=Object.prototype.hasOwnProperty,K=a.unstable_scheduleCallback,Z=a.unstable_cancelCallback,X=a.unstable_shouldYield,J=a.unstable_requestPaint,ee=a.unstable_now,et=a.unstable_getCurrentPriorityLevel,en=a.unstable_ImmediatePriority,er=a.unstable_UserBlockingPriority,ea=a.unstable_NormalPriority,eo=a.unstable_LowPriority,ei=a.unstable_IdlePriority,el=a.log,es=a.unstable_setDisableYieldValue,eu=null,ec=null;function ed(e){if("function"==typeof el&&es(e),ec&&"function"==typeof ec.setStrictMode)try{ec.setStrictMode(eu,e)}catch(e){}}var ef=Math.clz32?Math.clz32:function(e){return 0==(e>>>=0)?32:31-(ep(e)/em|0)|0},ep=Math.log,em=Math.LN2,eh=256,eg=4194304;function eb(e){var t=42&e;if(0!==t)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return 4194048&e;case 4194304:case 8388608:case 0x1000000:case 0x2000000:return 0x3c00000&e;case 0x4000000:return 0x4000000;case 0x8000000:return 0x8000000;case 0x10000000:return 0x10000000;case 0x20000000:return 0x20000000;case 0x40000000:return 0;default:return e}}function ey(e,t,n){var r=e.pendingLanes;if(0===r)return 0;var a=0,o=e.suspendedLanes,i=e.pingedLanes;e=e.warmLanes;var l=0x7ffffff&r;return 0!==l?0!=(r=l&~o)?a=eb(r):0!=(i&=l)?a=eb(i):n||0!=(n=l&~e)&&(a=eb(n)):0!=(l=r&~o)?a=eb(l):0!==i?a=eb(i):n||0!=(n=r&~e)&&(a=eb(n)),0===a?0:0!==t&&t!==a&&0==(t&o)&&((o=a&-a)>=(n=t&-t)||32===o&&0!=(4194048&n))?t:a}function ev(e,t){return 0==(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)}function ek(){var e=eh;return 0==(4194048&(eh<<=1))&&(eh=256),e}function ew(){var e=eg;return 0==(0x3c00000&(eg<<=1))&&(eg=4194304),e}function ex(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function eS(e,t){e.pendingLanes|=t,0x10000000!==t&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function eE(e,t,n){e.pendingLanes|=t,e.suspendedLanes&=~t;var r=31-ef(t);e.entangledLanes|=t,e.entanglements[r]=0x40000000|e.entanglements[r]|4194090&n}function e_(e,t){var n=e.entangledLanes|=t;for(e=e.entanglements;n;){var r=31-ef(n),a=1<)":-1a||s[r]!==u[a]){var c="\n"+s[r].replace(" at new "," at ");return e.displayName&&c.includes("")&&(c=c.replace("