Re-queue valid ingress NP update on client rather than spamming all clients with duplicate updates from memory source
This should have the same functional affect with way less log noise
* Add optional username/apiKey config fields
* Identify Source by optional username/sk/api_key
* Emulate auth.getMobileSession response with sk derived from Source uid
* Update docs with guidance for multiple sources and improved setup language
If the extension is provided by the server and playback data is reported by the client, make use of the new position and state fields when polling the current play entry from subsonic. If the new fields are not provided, fallback to the old timestamp based tracking listen progress.
* Replace "manual listening behavior" for azuracast/icecast with monitoring functionality for all sources/clients
* Rename "systemScrobble" to "autoMonitor"
* Implement functionality for all sources/clients
* only use docker init for fixing permission
* don't rely on docker init for dir creation as other distros may need this functionality (nix)
* check for and create config/data dirs recursively on app start
#616
* Break configDir into config and data directories so config and database/cache can be stored in different directories
* Use `env-paths` to fallback to OS-standard paths for config and data if CONFIG_DIR and DATA_DIR envs are not present
* Additionally fallback to process CWD/config if none of the above are present for some reason
#616
* Use id OR name for matching client to scrobble to, during transition period
* Deprecate using name-only and warn when this is detected
* Consolidate logging filtered clients to one statement to reduce log noise
* Improve filter matching by making strings case and whitepsace insensitive
* On startup log `clients` options for sources
Updated Apple Music configuration documentation with step-by-step instructions and screenshots for extracting authorization tokens in Chrome, Edge, Firefox, and Safari.
latest_listen_ts is *always* the latest of *all-time*, not for the current min/max_ts range requested.
Instead, use count < than desired (and non-zero) to indicate more results
* Ensure ingress queue always has an updated timestamp before dead creation
* Don't re-assign createdAt for scrobble after dead queue has already been updated
* Use timeline summary formatting to fix spacing
* Provide client name from fetched component data, if present, so scrobble summary is explicit where it sent/response scrobble from
Replaces the generic `headers` configuration field with a specific `origin` field for Apple Music. Updates the config schema, environment variable handling, documentation, and source implementation to simplify origin header management.
Implemented album name normalization in AppleMusicSource to ensure consistency with other sources by removing standard suffixes from EPs and singles. Updated documentation to reflect this behavior.
Added logic to detect and recover 'top-rebound' history patterns where Apple Music deduplicates re-listened tracks by bumping them to the top. This ensures intermediate tracks are captured and re-listened top tracks are processed correctly. Includes a new configuration option `recoverUnchangedTopHistory` to toggle this behavior.
Refactored Apple Music source configuration to support token-based authentication via key files and custom headers. Renamed environment variable prefixes from 'AM' to 'APPLEMUSIC' for consistency.
Added documentation for the APPLEMUSIC_HEADERS environment variable and updated code blocks with titles for better clarity in the Apple Music source configuration.
Implemented status code validation for Apple Music authentication and connection checks. The module now throws errors instead of failing silently when the API is unreachable or auth is invalid.
Reorganized the Apple Music documentation to prioritize the Browser Token (Free) authentication method and clarify requirements for the paid MusicKit Key approach.
Introduced a `headers` configuration option to allow setting custom request headers, such as 'Origin', for Apple Music API requests. Updated configuration schema and source implementation to apply these headers.
Moved the reduceRight logic for calculating play timestamps into a dedicated private method 'applyCalculatedTimestamps' to improve code readability and maintainability.
Refactored AppleMusicSource to use defined types for history consistency, updated the getRecentlyPlayedTracks limit type, and renamed variable for clarity.
Removed the unused `getBackloggedPlays` method and cleaned up the `getRecentlyDiscoveredPlays` logic in `AppleMusicSource` to streamline track processing.
Implement Apple Music source, including configuration types, environment variable handling, and source initialization logic. Refactored SourceType definitions to improve maintainability.
Store opened state, and position/size, to local storage so logs stay where user wants them across refreshes/initial load.
Resets window to default position if any part of log window is outside viewport bounds on open.
* Revert removing state on "should clear" statuses, causing false positive updates
* Insteat, filter out NP state to api based on "should clear" statuses
* Use structuralSharing to add signal that play data was updated from invalidated query
* Make update badge more generic and fix timeout/re-render
* Play update story updates random play
* reduce preset options to make smaller viewports easier to use
* Add icon to mobile users can avoid keyboard popup
* Disable openOnClick to avoid popup annoyance when user is trying to directly type dates
* Remove indexed any prop so meta is stricter (better catching bugs)
* Implement mergeable generic with meta where we do need extra prop
* Fix bad usage of meta now that indexed prop is gone
* Return error response when no sources found by request, or none configured
* try-catch and return well-formatted error response if source handling fails
* Refactor post init behavior into promise so we can fallback to recent sync if full hydration fails
* This way user can still compare real time scrobble activity without full hydration
* Simplify sync status usage
* Log reason based on presence of sync error
* Always try to do full hydration if not synced
* Fix missing logging for hydration error
* Add MS-scrobbled scrobbles to historical database
Example is already in the main cache configuration docs. Consolidate/remove the transform example so there is only one authorative example in the docs.
* Break out player status checks and thresholds into separate functions
* Consolidate all should update functionality into shouldUpdatePlayingNow for easier testing
* Introduce realtime flag for teal/discord and check if clearing is possible/desired
* Remove equivalent functionality in individual clients
* (tealfm): Correctly set max threshold using track duration
* (tealfm): Skip clearing update if close to/after expiration time
* (tealfm): Increase min threshold to avoid spamming pds updates for pauses/skip track
* Utilize existing methods for added queued/dead scrobbled when migrating from cache
* Using queueScrobble fixes migration not creating queued state
* Using addDeadLetterScrobble reduces duplicated code in migration
* Drop listenranges to avoid bad cache data issue (potentially)
* Use ErrorLike or ErrorIsh for error utilities, rather than requiring object be an Error instance
* Render error summary on dead scrobble page and have copy action include all error data
* serialize errors when sending through api
* fix error assignment for new dead scrobbles
* Update hast-util-to-jsx-runtime with npm override
* Re-export JSX as namespace for remaining packages with issues
May be addressed by future package updates or upgrading to docusaurus 3.10.x
* Remove ListenProgress specific serialization tests and function since its not needed anymore
* postfix tmp dirs with test name hints for easier debugging
* try to prevent filecache from leaving files behind
* re-use dead scrobble removal method for clearing all
* temp fix for rendering queue errors in frontend
* properly decrement queue based on dead scrobble queue status
* display total dead on status card
* Refactor should play methods to return result and reason, rather than always logging reason
* Combine reasons when logging to reduce log statements
* Demote "no update" logging to TRACE, promot "update" logging to VERBOSE
* Use new updatedAt column to determine if plays should be deleted
* Update plays on already discovered to keep them from being delted
* Fix various bugs and missed awaits in retention cleanup function
multiplatform is still useful for memory players but is not useful for discovery and just complicates code paths.
there is no reason to discover based on platform as the Source is the smallest UoW anyway. If a play should be discovered (or not) it shouldn't matter what platform is was played on, it's all the same Source
* Don't keep sticky now playing if the player has gone stale/orphaned
* When multiple players are present, prefer those that are not stale/orphaned
#480
* Use and return only plain PlayProgress on player state finish
* Removes the need to serialize/deserialize ListenProgress class and compare/persist toad scheduler or timeout functions
* Potentially fixes timeout comparison stack error #569
* Helper script to build a static font
* Move all image rendering code into own folder
* Simplify generated text to description OR title
* Use text-wrap balance to make it look prettier
* Add tagline and increase font sizes to fill card dimensions
This document outlines the Contributor Covenant Code of Conduct, including our pledge, standards, enforcement responsibilities, and guidelines for community behavior.
* Use filtered, processable scrobbles in for-loop to avoid any weird in-place array splice issues when iterating
* Add more logging with labels/ids to make logging more readable
* Move dead scrobble removal function closer blocks where it should happen
* Re-use lifecycle steps/patch to build diffs
* Break out step generation into own function to simplify logic
* Cache steps instead of play to avoid returning a cached play when data is the same #495
* Replace patch object/patching logic with json-diff-ts as its more robust and actually maintained
* Reduces need to stringify everywhere due to lack of cloning in jsonpatchdiff
Some items on Plex store the MBID in the `guid` field in the `Metadata`
response, not in the `Guid` field. This change checks the `guid` field
as a fallback if the `Guid` field is not set.
* Check queue before adding so that previously cached or re-discovered plays from a source are not added multiple times to the queue (should only be unique plays)
* Add timestamp heuristic to dupe matching -- if score is *close* to matching and timestamp is *exact* then nudge it into a match
* Use p-retry to standard retry behavior
* Simplify/remove other own-rolled retry logic
* Fix superagent request instantiation
* Requests are mutable, cannot be retried once executed. Need to make a new request for each retry
Use a limit/maxFetch upper limit to return a bulk of scrobbles by count, rather than timerange, so that backlogged scrobbles are less likely to cause many fetches on first startup
* Remove design bias towards "recent" scrobbles with limited timerange and complex shouldRefresh logic
* Replace with logic to fetch scrobbles from timeranges that are relevant to queued/dead scrobbles
* timeranges are grouped by closely timestamped queued scrobbles
* timeranges are cached based on staleAfter
Koito functionality has diverged enough that it doesn't make sense to provide partial support via LZ components.
This also makes timerange pagination easier.
* All logger use trace as minimum level
* Force app log stream to trace minimum so we always show all possible logs in dashboard
* Add trace level to dashboard
* Make dashboard start in debug logs for readability
* Set auth failure on 4000 from server (invalid client id)
* Better logging for connection issues
* Don't fail on connect so we can keep retrying on MS activity
* Handle connect and auth separately so it can be restarted by scrobble heartbeat
* make ms-initiated reconnect use manual close/open since isosockets won't
* better error handling for auth/connection init stages
* send heartbeats even if not authed
* Use pocike icon for placeholder art
* Use MS logo for small art with link to docs
* Use web link as track url, if available
* Use MB link as track or artist url
* Add as dev dependency with npm script so it can be run anywhere project is installed
* Update config with newer template based on scopesorted
* Add more scopes and give them pretty emojis
The old jellyfin config had optional options which kept the new jf options required from *always* being required. Removing the old config revealed this bug.
Fixes#469
label:Please check existing knowledge before opening an issue
options:
- label:I have [read the Help docs](https://foxxmd.github.io/multi-scrobbler/docs/help/)
- label:I have [read the Help docs](https://foxxmd.github.io/multi-scrobbler/help/)
required:true
- label:I have [checked the FAQ](https://foxxmd.github.io/multi-scrobbler/docs/FAQ/) and [documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration/)
- label:I have [checked the FAQ](https://foxxmd.github.io/multi-scrobbler/FAQ/) and [documentation](https://foxxmd.github.io/multi-scrobbler/configuration/)
required:true
- label:I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
required:true
@@ -65,7 +65,7 @@ body:
Please copy and paste any/all relevant from the DEBUG level log output.
Even if your issue does not seem to be shown in the logs you should still add as much detail as possible, a _lack_ of issues in logs may be a good hint during debugging.
Your logs will be automatically formatted into code. Do not use fenced code blocks (backticks).
If possible reproduce the issue with [**Debug Mode** enabled](https://foxxmd.github.io/multi-scrobbler/docs/configuration#debug-mode).
If possible reproduce the issue with [**Debug Mode** enabled](https://foxxmd.github.io/multi-scrobbler/configuration#debug-mode).
label:Please check existing knowledge before opening an issue
options:
- label:I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration/)
- label:I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/configuration/)
required:true
- label:I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
label:Please check existing knowledge before opening an issue
options:
- label:I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration/)
- label:I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/configuration/)
required:true
- label:I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
A javascript app to scrobble music you listen to, to [Maloja](https://github.com/krateng/maloja), [Last.fm](https://www.last.fm), [ListenBrainz](https://listenbrainz.org), and other services.
A dockerized app that monitors your music listening activity from *everywhere* and scrobbles it *anywhere*.
* Supports scrobbling from many [**Sources**](https://foxxmd.github.io/multi-scrobbler/configuration/sources)
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](https://foxxmd.github.io/multi-scrobbler/configuration#webhook-configurations), [healthcheck endpoints](https://foxxmd.github.io/multi-scrobbler/configuration#health-endpoint), or [prometheus metrics](https://foxxmd.github.io/multi-scrobbler/configuration/#prometheus).
* Supports [Now Playing](https://foxxmd.github.io/multi-scrobbler/configuration/clients#now-playing) for scrobble Clients
* Supports scrobbling from many [**Sources**](https://docs.multi-scrobbler.app/configuration/sources)
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](https://docs.multi-scrobbler.app/configuration#webhook-configurations), [healthcheck endpoints](https://docs.multi-scrobbler.app/configuration#health-endpoint), or [prometheus metrics](https://docs.multi-scrobbler.app/configuration/#prometheus).
* Supports [Now Playing](https://docs.multi-scrobbler.app/configuration/clients#now-playing) for scrobble Clients
* Supports configuring for single or multiple users (scrobbling for your friends and family!)
* Web server interface for stats, basic control, and detailed logs
* Graceful network and client failure handling (queued scrobbles that auto-retry)
* Smart handling of credentials (persistent, authorization through app)
* Easy configuration through [ENVs or JSON](ttps://foxxmd.github.io/multi-scrobbler/configuration#configuration-types)
* Modify data before scrobbling with [regular expression or search patterns](https://foxxmd.github.io/multi-scrobbler/configuration/transforms)
* Install using [Docker images for x86/ARM](https://foxxmd.github.io/multi-scrobbler/installation#docker) or [locally with NodeJS](https://foxxmd.github.io/multi-scrobbler/installation#nodejs)
* Modify data before scrobbling with [regular expression or search patterns](https://docs.multi-scrobbler.app/configuration/transforms)
* Install using [Docker images for x86/ARM](https://docs.multi-scrobbler.app/installation#docker) or [locally with NodeJS](https://docs.multi-scrobbler.app/installation#nodejs)
**Why should I use this over a browser extension and/or mobile app scrobbler?**
@@ -71,15 +74,15 @@ A javascript app to scrobble music you listen to, to [Maloja](https://github.com
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
Yes! You can use [Last.fm as a **Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-source) or [Listenbrainz as a **Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources/listenbrainz-source) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
Yes! You can use [Last.fm as a **Source**](https://docs.multi-scrobbler.app/configuration/sources/lastfm-source) or [Listenbrainz as a **Source**](https://docs.multi-scrobbler.app/configuration/sources/listenbrainz-source) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
## How Does multi-scrobbler (MS) Work?
You set up [configurations](https://foxxmd.github.io/multi-scrobbler/configuration) for one or more [**Sources**](https://foxxmd.github.io/multi-scrobbler/configuration/sources) and one or more [**Clients**](https://foxxmd.github.io/multi-scrobbler/configuration/clients). MS monitors all of your configured **Sources**. When new tracks are played by a Source MS grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to forward to.
You set up [configurations](https://docs.multi-scrobbler.app/configuration) for one or more [**Sources**](https://docs.multi-scrobbler.app/configuration/sources) and one or more [**Clients**](https://docs.multi-scrobbler.app/configuration/clients). MS monitors all of your configured **Sources**. When new tracks are played by a Source MS grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to forward to.
### Source
A [**Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources) is a data source that contains information about music you are playing or have listened to, like a desktop player, web music player, or cloud music service. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Navidrome**, etc...
A [**Source**](https://docs.multi-scrobbler.app/configuration/sources) is a data source that contains information about music you are playing or have listened to, such as: a desktop player, web music player, or cloud music service. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Navidrome**, etc...
A [**Client**](https://foxxmd.github.io/multi-scrobbler/configuration/clients) is an application that stores the historical information about what music you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
A [**Client**](https://docs.multi-scrobbler.app/configuration/clients) is an application that stores the historical information about what music you have played (scrobbles). Examples are **Koito, Last.fm, Listenbrainz**...
@@ -60,6 +61,21 @@ If multi-scrobbler is not running on the same machine your browser is on then th
EX `http://localhost:9078/lastfm/callback` -> `http://192.168.0.220:9078/lastfm/callback`
### I get errors with `ECONNRESET` `ETIMEDOUT` `ENOTFOUND` `EAI_AGAIN`
These are errors indicating a network issue is occuring between you and the Source/Client.
*`ECONNRESET` means the upstream service unexpectedly closed the connection
*`ETIMEDOUT` means the upstream service did not respond to a request MS
*`ENOTFOUND` means that MS could not find a DNS record for the domain of the upstream service
*`EAI_AGAIN` means that a DNS server did not respond to MS's request to lookup the record for a domain
`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.
Please check the [Upstream Services Status Page](https://status.multi-scrobbler.app) 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.
`ENOTFOUND` and `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 *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.
### Youtube Music fails after some time
If you see errors in multi-scrobbler for YTM that contain **401** or **403** like
@@ -70,7 +86,7 @@ Error: Could not send the specified request to browse. Status code: 401
then YTM has invalidated your authentication.
First, ensure you are NOT using [YoutubeTV authentication.](/configuration/sources/youtube-music?ytmAuth=ytt#authentication) If you completed authentication by entering a "User Code" you are using YoutubeTV which has stopped working. You should reauthenticate using **Cookies** or **Custom OAuth.**
First, ensure you are NOT using [YoutubeTV authentication.](/configuration/sources/youtube-music?ytmAuth=ytt#ytm-auth) If you completed authentication by entering a "User Code" you are using YoutubeTV which has stopped working. You should reauthenticate using **Cookies** or **Custom OAuth.**
#### When using OAuth Client Authentication
@@ -80,7 +96,7 @@ Refresh your authentication by using the **(Re)authenticate** link from MS's web
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 _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 [getting the cookie from an Incognito/Private Session](https://github.com/LuanRT/YouTube.js/issues/803#issuecomment-2504032666) and then immediately closing the browser afterwards.
To re-authenticate MS [follow the YTM instructions to retrieve a new set of cookies for multi-scrobbler](/configuration/sources/youtube-music?ytmAuth=cookie#authentication) and then restart MS to potentially resolve the problem.
To re-authenticate MS [follow the YTM instructions to retrieve a new set of cookies for multi-scrobbler](/configuration/sources/youtube-music?ytmAuth=cookie#ytm-auth) and then restart MS to potentially resolve the problem.
## Configuration Issues
@@ -98,6 +114,12 @@ It means the JSON in your configuration file is not valid. Copy and paste your c
## Scrobbling Issues
### How do I prevent duplicate scrobbles?
Multi-scrobbler has a [robust duplicate scrobble detection system](/configuration/duplicates#how-duplicates-are-detected) that should handle this for you in the majority of usecases. If you have a [simple setup without any circular dependencies](/configuration/duplicates#valid-configs) then you are already protected against duplicates.
If your scenario is more complex or you have a service(s) acting as **both** a **Client** and **Source** you should refer to the [**Duplicate Scrobble Guidance**](/configuration/duplicates#valid-configs) page to determine if you can avoid a configuration that may result in duplicate scrobbles.
### Last.fm does not scrobble tracks with multiple artists correctly
This is a limitation of the [Last.fm API](https://www.last.fm/api/show/track.scrobble) where the **artist** field is only one string and Last.fm does not recognize (play well) with "combined" artists.
Using [File Config](/configuration/?configType=file#configuration-types)
For non-ENV config, [Musicbrainz Transformer](/configuration/transforms/musicbrainz/#api-setup) must be setup using the [AIO Config](/configuration/?configType=aio#configuration-types) `config.json` file. This configuration uses the [sensible defaults](/configuration/transforms/musicbrainz/#sensible-default) in the `defaults` property. This is a suggestion and can be removed if not desired.
Using [AIO Config](/configuration/?configType=aio#configuration-types)
This configuration uses the [sensible defaults](/configuration/transforms/musicbrainz/#sensible-default) in the `defaults` property. This is a suggestion and can be removed if not desired.
| _**`ROCKSKY_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `ROCKSKY_NAME` | string | Value of `ROCKSKY_ID` | A vanity name EX `My Cool Component` |
| `ROCKSKY_ENABLE` | boolean | true | Should this component be used? |
| `ROCKSKY_KEY` | string | | API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account |
| `ROCKSKY_TOKEN` | string | | Access Token generated from https://rocksky.app/access-tokens in Rocksky for your account |
| _**`ROCKSKY_HANDLE`**_ | string | | The **fully-qualified** handle for your ATPRoto/Bluesky account, like: |
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.
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:
* Similarity of Title, Artists, and Album
* Temporal closeness of the Play's timestamp to the existing scrobble's timestamp
* Whether MS detected the Play as a repeat (applicable to Sources that report realtime Player data)
Detailed scoring breakdowns against each existing scrobble are logged at the `TRACE` logging level or in the [debug data](/help#copy-play-debug-data) for a scrobble.
See the [**Duplicate Scrobble Guidance**](/configuration/duplicates#how-duplicates-are-detected) page to learn how duplicates are detected and how you can ensure your configuration avoids possible duplicates.
### Dead Scrobbles
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.
import TcpWarning from "@site/src/components/snippets/_discord-port-warn.mdx";
import JsonConfig from '!!raw-loader!@site/../config/discord-local.json.example';
This scrobbler uses **[Now Playing](/configuration/clients#now-playing)** functionality to set your Discord [Rich Presence](https://docs.discord.com/developers/rich-presence/overview) to the music you are currently monitoring with multi-scrobbler.
This integration uses an **existing Discord application** to set your Rich Presence.
:::tip
Check the [main Discord docs page](/configuration/clients/discord) for a comparison between **App** and **Headless**.
:::
## Required Setup
### Discord Application
You must create a [Discord App](https://docs.discord.com/developers/quick-start/overview-of-apps) in order to use this integration. Creating an App is free and simple.
Go to the [Discord Developer Portal](https://discord.com/developers/applications)
* Click on "New Application" and give it a name
* Copy the **Application Id** (Client Id) shown after creation
* Add this Id to your Multi-Scrobbler config
* Env Config => `DISCORD_APPLICATION_ID=12345678`
* File Config => `"applicationId": "12345678"`
### Discord Connection
This integration uses an existing, running Discord application to communicate with Discord and set Rich Presence. Multi-scrobbler must be able to communicate with the Discord application, either by being on the same machine or over a network.
[Kasm Workspaces](https://kasm.com) provides a self-hostable [Discord container image](https://hub.docker.com/r/kasmweb/discord). This image provides a desktop-like experience, through browser-based VNC, to use the official Discord application.
This containerized approach can be used instead of configuring MS to use a [Native Discord](./?discordType=native#discord-connection) application on a specific machine.
**Advantages**
* Setup is much simpler
* Discord runs in the same stack as multi-scrobbler
* **Configuration is copy-paste.** Skip all of the [Discord Connection](#discord-connection) instructions.
* Get a [Headless](/configuration/clients/discord/discord-headless)-like experience without needing to break Discord TOS
**Disadvantages**
* Image only available for x86 arch
* Image and container are large (1GB+)
* Discord does not notify *other* clients of notifications in a channel if *any* client is viewing that channel.
* To avoid this, make sure your container discord instance is not viewing any server/channel/dm.
* Sometimes, Discord does not deliver "notifications you missed" to mobile because this depends on *all* clients being offline.
<h2>Setup</h2>
Setup is basically the same as using the instructions for [Discord on Linux](./?osType=linux&discordType=native#find-ipc-connection) with [MS on Same Host (Docker)](./?osType=linux&discordType=native#pass-ipc-to-multi-scrobbler).
<h3>1. Add Discord Service to Multi-Scrobbler Docker Compose Stack</h3>
<details>
<summary>Detailed Explanation</summary>
* Add the `kasmweb/discord` service to your compose stack
* Mount the directory containing Discord's unix socket to a named volume
* Configure MS to use Discord's unix socket
* Mount the unix socket volume into MS's container
* Add ENV `DISCORD_IPC_LOCATIONS` to point to the mounted volume
</details>
Use this example `docker-compose.yaml` to modify your existing multi-scrobbler `docker-compose.yaml`:
Navigate in your browser to the IP/host Multi-Scrobbler/Discord are running on. Use the `port` set in the `kasmcord` service:
```
# basic auth login
# user: kasm_user
# pass is what was set in env VNC_PW
https://192.168.MY.HOST:6901
```
Login to Discord normally. After login is complete MS should connect automatically when a Now Playing event happens. If it does not or reports authentication failure then restart MS (but not Discord).
If MS works then you are done and can move on to [Optional Setup](#optional-setup).
</TabItem>
<TabItem value="native" label="Native Discord">
Use this method if you want to connect MS to Discord running on a physical desktop.
<DetailsAdmo type="tip" summary="Local MS Installation Requires No Config">
If MS was [installed locally](/installation#local-installation) (not Docker) on the same machine as the Discord application then MS should [automatically detect the correct method](#find-ipc-connection) and [use it](#pass-ipc-to-multi-scrobbler) **without** any configuration from you. This will work for **any OS**.
If MS cannot determine this automatically, follow the steps for your OS below.
</DetailsAdmo>
Follow the sections below to configure MS for your specific environment.
#### Find IPC Connection
Discord uses OS-specific methods to communicate with other applications on the same host, called [IPC](https://en.wikipedia.org/wiki/Inter-process_communication). You may need to determine some details about the IPC method in order for MS to communicate with it correctly.
<Tabs groupId="osType" queryString>
<TabItem value="linux" label="Discord on Linux">
On Linux, Discord uses a [Unix Socket](https://en.wikipedia.org/wiki/Unix_domain_socket) for IPC.
<TabItem value="windows" label="Discord on Windows">
Windows uses [**Named Pipes**](https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipes) for IPC. These aren't easily iteratable without external software, like [Pipetap](https://github.com/sensepost/pipetap).
The syntax for a named pipe is like this:
```
\\.\pipe\app_named_pipe_foo
```
and Discord's names follow this pattern:
```
\\.\pipe\discord-ipc-0
```
Where `0` increments for each different instance of Discord (official, canary, nightly, etc...).
The default Discord pipe name is: `discord-ipc-0`
</TabItem>
</Tabs>
#### Pass IPC to Multi-Scrobbler
After [finding your IPC connection path](#find-ipc-connection) you will need to make it accessible to Multi-Scrobbler.
<Tabs groupId="osType" queryString>
<TabItem value="linux" label="Discord on Linux">
<Tabs groupId="msConnection">
<TabItem value="remote" label="MS on Different Host">
<TcpWarning/>
Use [`socat`](https://linux.die.net/man/1/socat) to bidirectionally relay communication from Discord's [unix socket](#find-ipc-connection) to a listening TCP port on the same host. Likely, you will want to setup a service to start this command on login.
<TabItem value="local-docker" label="MS on Same Host (Docker)">
In your MS docker compose file, pass the path of Discord's [unix socket](#find-ipc-connection) as a volume and add this path to `DISCORD_IPC_LOCATIONS` in `environment:`
```yaml title="docker-compose.yaml"
environment:
# ...
DISCORD_IPC_LOCATIONS=/run/discord-ipc-0
volumes:
- /run/user/1000/discord-ipc-0:/run/discord-ipc-0
```
</TabItem>
<TabItem value="local" label="MS on Same Host (Non-Docker)">
No configuration is required. If MS does not automatically work for you then try the **MS on Same Host (Docker)** method using the `DISCORD_IPC_LOCATIONS` environment variable.
</TabItem>
</Tabs>
</TabItem>
<TabItem value="macos" label="Discord on MacOS">
<Tabs groupId="msConnection">
<TabItem value="remote" label="MS on Different Host">
<TcpWarning/>
Use one of the methods below to setup `socat` on your macOS machine.
<details>
<summary>`socat` installed natively</summary>
Install [`socat`](https://formulae.brew.sh/formula/socat) using homebrew to use it as a native command on your host.
Then, use [`socat`](https://linux.die.net/man/1/socat) to bidirectionally relay communication from Discord's [unix socket](#find-ipc-connection) to a listening TCP port on the same host. Likely, you will want to setup a service to start this command on login.
<TabItem value="local-docker" label="MS on Same Host (Docker)">
In your MS docker compose file, pass the path of Discord's [unix socket](#find-ipc-connection) as a volume and add this path to `DISCORD_IPC_LOCATIONS` in `environment:`
```yaml title="docker-compose.yaml"
environment:
# ...
DISCORD_IPC_LOCATIONS=/discord-ipc-0
volumes:
- /run/user/1000/discord-ipc-0:/run/discord-ipc-0
```
</TabItem>
<TabItem value="local" label="MS on Same Host (Non-Docker)">
No configuration is required. If MS does not automatically work for you then try the **MS on Same Host (Docker)** method using the `DISCORD_IPC_LOCATIONS` environment variable.
</TabItem>
</Tabs>
</TabItem>
<TabItem value="windows" label="Discord on Windows">
<Tabs groupId="msConnection">
<TabItem value="remote" label="MS in Docker">
:::note
While it is possible to use [named pipes directly in Docker](https://forums.docker.com/t/volume-mount-path-error/143626/3) this has not been tested and [may only work for windows containers](https://github.com/docker/for-win/issues/6813#issuecomment-632859857). If you are adventurous, try this out and if you can get it working please [report your findings in an issue.](https://github.com/FoxxMD/multi-scrobbler/issues)
:::
:::warning
This method is **experimental** and may be buggy and prone to crashes. If you are a Windows or Rust developer please consider helping improve the [pipe proxy implementation](https://github.com/FoxxMD/named-pipe-to-tcp-proxy).
:::
<TcpWarning/>
Use [**pipeProxy**](https://github.com/FoxxMD/named-pipe-to-tcp-proxy) to connect to the Discord named pipe and expose it on a TCP port.
Download the latest [`pipeProxy.exe`](https://github.com/FoxxMD/named-pipe-to-tcp-proxy/releases) from the releases page.
The host/ip should be the IP of the machine running discord.
In your MS docker compose file add the `host:port` of the host running `pipeProxy` to `DISCORD_IPC_LOCATIONS` `environment:`
```yaml title="docker-compose.yaml"
environment:
# ...
DISCORD_IPC_LOCATIONS=192.168.0.100:6655
```
</TabItem>
<TabItem value="local" label="MS on Same Host (Non-Docker)">
No configuration is required. If MS does not automatically work for you then try the **MS in Docker** method using the `DISCORD_IPC_LOCATIONS` environment variable.
</TabItem>
</Tabs>
</TabItem>
</Tabs>
</TabItem>
</Tabs>
## Optional Setup
:::note
Due to the limitations of the Discord local API the other listening activity detection and online status customizations found in [Headless](/configuration/clients/discord/discord-headless#optional-setup) are not available for the Local integration.
:::
### Artwork
Multi-scrobbler can display Album Art in your Discord status. The image that is used differs based on if the image URL is publically accessible.
The order in which multi-scrobbler determines what image to use is the same as the order of the tabs shown below.
External URLs that are not from known music services are **disabled by default.** This is to preserve *your* privacy because Discord does not host images, it only links to them and the URL is visible to other users.
<details>
<summary>Why Should I Care?</summary>
There are some scenarios where you may not want your artwork URLs to be visible, for example:
* A Source may be accessible only on your LAN (Jellyfin at `http://192.168.0.101`)
* A Source may be internet-facing but you do not want to expose this information to discord/other users
* A Source may be internet-facing but requires authentication to view the image
</details>
MS will use your artwork URLs only if:
* artwork url is from a known music service/[Cover Art Archive](./?artworkUrl=caa#artwork-url-types) or
* artwork url starts with `https` and
* `artwork` (`DISCORD_ARTWORK`) is
* `true` => always uses the url
* a list of custom domains keywords an external artwork url should contain EX
None of the below URLs will **ever** be used because they are not `https`
```
http://foobar.com/cool.jpg <-- NOT allowed, not http
http://192.168.0.112/fun.png <-- NOT allowed, not http
```
When `DISCORD_ARTWORK` is not set:
```
https://archive.org/download/dir/myimage.jpg <-- allowed, is https and known service
https://spotify.com/assets/cover.png?size=1000 <-- allowed, is https and known service
https://example.com/cool.jpg <-- NOT allowed, is https but not a known service and DISCORD_ARTWORK is not set
https://anything.com/neat.jpg <-- NOT allowed, is https but not a known service and DISCORD_ARTWORK is not set
```
Setting `DISCORD_ARTWORK=true`
```
https://archive.org/my/dir/myimage.jpg <-- allowed, is https and known service
http://foobar.com/cool.jpg <-- NOT allowed, not http
https://example.com/cool.jpg <-- allowed, is https
https://anything.com/neat.jpg <-- allowed, is https
```
Setting `DISCORD_ARTWORK=mycdn,jellyfin`
```
https://archive.org/my/dir/myimage.jpg <-- allowed, is https and known service
http://foobar.com/cool.jpg <-- NOT allowed, not http
https://example.com/cool.jpg <-- NOT allowed, is https but does not contain "mycdn" or "jellyfin"
https://jellyfin.mydomain.com/neat.jpg <-- allowed, is https and contains "jellyfin"
```
</details>
</TabItem>
<TabItem label="Musicbrainz/Cover Art Achive" value="caa">
MS can try to use [Cover Art Archive](http://coverartarchive.org/) to get album art. The URL it retrieves is public which means it acts like an External URL that is always available for you.
* The scrobble data does not contain an album art url (no art in dashboard) or the existing url has failed an [External URL](./?artworkUrl=external#artwork-url-types) condition
* and **the scrobble data contains a [Musicbrainz](/configuration/transforms/musicbrainz) Release (Album) MBID**
Basically, if MS would normally use the [MS Default](./?artworkUrl=ms-default#artwork-url-types) art, it will first try to use Cover Art Archive if the scrobble has a Release MBID.
Some Source, like [Jellyfin](/configuration/sources/jellyfin) and [Plex](/configuration/sources/plex), can automatically provide this ID if the music is "matched" in your library.
For all others, you can [**configure MS to use the Musicbrainz Stage**](/configuration/transforms/musicbrainz#use-metadata-for-discord-album-art) for Discord to try to get this MBID.
</TabItem>
<TabItem label="MS Default" value="ms-default">
When these conditions are true:
* Cannot use an [External URL](./?artworkUrl=external#artwork-url-types) and
* Cannot get album art from [Cover Art Archive](./?artworkUrl=caa#artwork-url-types) and
* Play does not contain artwork information (no artwork shown in dashboard)
The status artwork will use a default URL instead of your artwork URL.
Without any other configuration, the default URL is an icon from the [multi-scrobbler repository](https://github.com/FoxxMD/multi-scrobbler/blob/master/assets/default-artwork.png):
| `DISCORD_APPLICATION_ID` | Yes | | Application ID |
| `DISCORD_IPC_LOCATIONS` | No | | A commera-separated list of `host:port` network locations or file paths to unix socket for Discord |
| `DISCORD_ARTWORK` | No | | A boolean indicating if external artwork URLs should be used. Or a comma-separated list of allowed domains |
| `DISCORD_ARTWORK_DEFAULT_URL` | No | | A URL of an image to use as a fallback if the album art URL cannot be used. Or `false` to use discord's default. |
import JsonConfig from '!!raw-loader!@site/../config/discord.json.example';
This scrobbler uses **[Now Playing](/configuration/clients#now-playing)** functionality to set your Discord [Rich Presence](https://docs.discord.com/developers/rich-presence/overview) to the music you are currently monitoring with multi-scrobbler.
This integration is **headless**, it does not depend on any specific Discord application to be running.
:::tip
Check the [main Discord docs page](/configuration/clients/discord) for a comparison between **App** and **Headless**.
:::
<DetailsAdmo type="note" summary="How MS Uses Discord API">
Multi-scrobbler does its utmost to implement, to-spec, [Gateway API communication](https://docs.discord.com/developers/events/gateway) and respect usage. It requires no [permissions](https://docs.discord.com/developers/events/gateway#gateway-intents) and reads no data other than monitoring what other user sessions are active.
Aside from the on-paper violation of using a user token programmatically (on your behalf), there is no misuse of the API to achieve rich presence.
Additionally, MS uses the state of non-MS sessions to conservatively update presence. [By default it will not conflict](#configure-when-presence-is-used) with any other official presence activities and is only used when you are actually active on a device.
</DetailsAdmo>
<DetailsAdmo type="note" summary="REQUIRED Interaction after Starting Multi-Scrobbler">
Every time multi-scrobbler is started a discord session update needs to be triggered so that [MS can capture the signals it uses to determine if presence can be updated.](#configure-when-presence-is-used) This only needs to be *once*, after starting multi-scrobbler. But it does need to be *every time* multi-scrobbler is (re)started. Without these signals MS will not update presence.
This session update can happen automatically after *some* time so you may not need to do anything. If you want to force the update then do one of the following:
* Open discord on a device it was not recently active on
* Exit discord on an active device
* Change your status (online, idle, etc...)
* Set a custom status
* Cause a presence update with a different app (listening to..., playing..., etc.)
</DetailsAdmo>
## Required Setup
### User Token
You must provide a **Discord User Token** for your account in order to authenticate with Discord.
This functionality requires you to use your own **Discord User Token** in a way that is **against Discord's policies/terms of service.** If you use this scrobbler you do so **at your risk**.
There is no precedent for Discord punishing users who use their own token for non-abuse/spam reasons but there is also no guarantee this lack of enforcement will continue.
Refer to the **How MS Uses Discord API** section above to gauge whether this risk is acceptable to you.
</DetailsAdmo>
<DetailsAdmo type="warning" summary="User Token is a Sensitive Credential">
Treat your User Token like a login credential. Anyone who has your token can access Discord as you.
When finding instructions for obtaining this token only run commands, or follow steps, that you can understand and trust. Do not run commands you do not understand. Never give your token to someone else.
</DetailsAdmo>
No instructions will be provided for obtaining a User Token because it is against Discord's policies to use it. Instead, you can search online for "how to obtain discord user token".
## Optional Setup
### Configure When Presence Is Used
MS uses several signals from your "real" Discord sessions to determine if it should update presence. The default configuration is conservative to ensure that MS does not conflict with other apps using presence. Additionally, it only updates if a "real" user device is active on discord.
:::tip[TLDR]
Without any additional configuration, MS will only update presence if:
* you have discord open on a real device
* your status is either: online, idle, or dnd (not invisible)
* no other apps are broadcasting listening activities (listening to spotify, etc...)
:::
#### Online Status
Configure if MS is allowed to update presence based on your online status. If this setting is not defined MS will only update if your status is **online**, **idle**, or **dnd**.
Allowed Values: `online` `idle` `dnd` `invisible`
<details>
<summary>Example</summary>
Only allow presence updating when all of your "real" sessions are either online or idle.
Without any additional configuration, MS will not set Rich Presence if **other** apps are broadcasting **listening** activities EX "Listening to Spotify".
<details>
<summary>What about other activity types?</summary>
Discord uses an undocumented activity type priority to determine what activity is shown "first" for your profile. The Listening activity type has the *lowest priority*.
This means that if you start any other type of activity (streaming, watching, competing, etc...) it will take precedence over MS's listening activity.
Your listening activity will still be visible by clicking into your full profile.
</details>
Configure the names of **other** listening activities that MS **is allowed** to broadcast at the same time as.
<details>
<summary>Example</summary>
You want to allow MS to update presence even if Spotify is also broadcasting a listening activity.
Creating a [Discord App](https://docs.discord.com/developers/quick-start/overview-of-apps) enables you to use custom album cover art and secondary status images for more links. Creating an App is free and simple, it is recommended you do this to get the most functionality from Rich Presence.
<details>
<summary>Create An Application</summary>
Go to the [Discord Developer Portal](https://discord.com/developers/applications)
* Click on "New Application" and give it a name
* Copy the **Application Id** (Client Id) shown after creation
* Add this Id to your Multi-Scrobbler config
* Env Config => `DISCORD_APPLICATION_ID=12345678`
* File Config => `"applicationId": "12345678"`
</details>
### Artwork
Multi-scrobbler can display Album Art in your Discord status. The image that is used differs based on if the image URL is publically accessible.
:::note
If no [**Application Id**](#discord-application) is configured the status art will **always** be the [Discord Default](./?artworkUrl=discord-default#artwork-url-types).
:::
The order in which multi-scrobbler determines what image to use is the same as the order of the tabs shown below.
#### Artwork URL Types
<Tabs groupId="artworkUrl" queryString>
<TabItem label="External URLs" value="external">
When [**Application Id**](#discord-application) **is** configured MS can use external URLs for artwork.
External URLs that are not from known music services are **disabled by default.** This is to preserve *your* privacy because Discord does not host images, it only links to them and the URL is visible to other users.
<details>
<summary>Why Should I Care?</summary>
There are some scenarios where you may not want your artwork URLs to be visible, for example:
* A Source may be accessible only on your LAN (Jellyfin at `http://192.168.0.101`)
* A Source may be internet-facing but you do not want to expose this information to discord/other users
* A Source may be internet-facing but requires authentication to view the image
</details>
MS will use your artwork URLs only if:
* artwork url is from a known music service/[Cover Art Archive](./?artworkUrl=caa#artwork-url-types) or
* artwork url starts with `https` and
* `artwork` (`DISCORD_ARTWORK`) is
* `true` => always uses the url
* a list of custom domains keywords an external artwork url should contain EX
None of the below URLs will **ever** be used because they are not `https`
```
http://foobar.com/cool.jpg <-- NOT allowed, not http
http://192.168.0.112/fun.png <-- NOT allowed, not http
```
When `DISCORD_ARTWORK` is not set:
```
https://archive.org/download/dir/myimage.jpg <-- allowed, is https and known service
https://spotify.com/assets/cover.png?size=1000 <-- allowed, is https and known service
https://example.com/cool.jpg <-- NOT allowed, is https but not a known service and DISCORD_ARTWORK is not set
https://anything.com/neat.jpg <-- NOT allowed, is https but not a known service and DISCORD_ARTWORK is not set
```
Setting `DISCORD_ARTWORK=true`
```
https://archive.org/my/dir/myimage.jpg <-- allowed, is https and known service
http://foobar.com/cool.jpg <-- NOT allowed, not http
https://example.com/cool.jpg <-- allowed, is https
https://anything.com/neat.jpg <-- allowed, is https
```
Setting `DISCORD_ARTWORK=mycdn,jellyfin`
```
https://archive.org/my/dir/myimage.jpg <-- allowed, is https and known service
http://foobar.com/cool.jpg <-- NOT allowed, not http
https://example.com/cool.jpg <-- NOT allowed, is https but does not contain "mycdn" or "jellyfin"
https://jellyfin.mydomain.com/neat.jpg <-- allowed, is https and contains "jellyfin"
```
</details>
</TabItem>
<TabItem label="Musicbrainz/Cover Art Achive" value="caa">
When [**Application Id**](#discord-application) **is** configured MS can try to use [Cover Art Archive](http://coverartarchive.org/) to get album art. The URL it retrieves is public which means it acts like an External URL that is always available for you.
* The scrobble data does not contain an album art url (no art in dashboard) or the existing url has failed an [External URL](./?artworkUrl=external#artwork-url-types) condition
* and **the scrobble data contains a [Musicbrainz](/configuration/transforms/musicbrainz) Release (Album) MBID**
Basically, if MS would normally use the [MS Default](./?artworkUrl=ms-default#artwork-url-types) art, it will first try to use Cover Art Archive if the scrobble has a Release MBID.
Some Source, like [Jellyfin](/configuration/sources/jellyfin) and [Plex](/configuration/sources/plex), can automatically provide this ID if the music is "matched" in your library.
For all others, you can [**configure MS to use the Musicbrainz Stage**](/configuration/transforms/musicbrainz#use-metadata-for-discord-album-art) for Discord to try to get this MBID.
</TabItem>
<TabItem label="MS Default" value="ms-default">
When [**Application Id**](#discord-application) **is** configured and these conditions are true:
* Cannot use an [External URL](./?artworkUrl=external#artwork-url-types) and
* Cannot get album art from [Cover Art Archive](./?artworkUrl=caa#artwork-url-types) and
* Play does not contain artwork information (no artwork shown in dashboard)
The status artwork will use a default URL instead of your artwork URL.
Without any other configuration, the default URL is an icon from the [multi-scrobbler repository](https://github.com/FoxxMD/multi-scrobbler/blob/master/assets/default-artwork.png):
When [**Application Id**](#discord-application) is **not** configured Discord will show its own image. No custom artwork can be used in this configuration.
This is also the image Discord will display if the artwork URL cannot be fetched.
| `DISCORD_TOKEN` | Yes | | User Token acquired from an active Discord sessions |
| `DISCORD_APPLICATION_ID` | No | | Application ID used to display album art |
| `DISCORD_ARTWORK` | No | | A boolean indicating if external artwork URLs should be used. Or a comma-separated list of allowed domains |
| `DISCORD_ARTWORK_DEFAULT_URL` | No | | A URL of an image to use as a fallback if the album art URL cannot be used. Or `false` to use discord's default. |
| `DISCORD_STATUS_OVERRIDE_ALLOW` | No | `online,idle,dnd` | A comma-separated list of statuses are allowed to have presence. |
| `DISCORD_LISTENING_ACTIVITY_ALLOW` | No | | A commera-seperated list of listening activity names MS **is allowed** to broadcast presence at the same time as. |
This scrobbler uses **[Now Playing](/configuration/clients#now-playing)** functionality to set your Discord [Rich Presence](https://docs.discord.com/developers/rich-presence/overview) to the music you are currently monitoring with multi-scrobbler.
import JsonConfig from '!!raw-loader!@site/../config/koito.json.example';
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_koito.md"
:::tip[Other Uses]
@@ -45,16 +46,12 @@ Using the URL path `/apis/listenbrainz` [Koito docs describe](https://koito.io/g
:::tip
Ensure that Koito is configured to allow requests from multi-scrobbler! In Koito config set [`KOITO_ALLOWED_HOSTS`](https://koito.io/reference/configuration/#koito_allowed_hosts) to the IP the multi-scrobbler dashboard is accessible from.
For Koito versions `<=0.1.7`, you must have the environment variable `KOITO_ALLOWED_HOSTS` set correctly in your Koito environment/container. Ensure it is set to the IP address or domain name that you use to access Koito with e.g. `koito.mydomain.com`.
import JsonConfig from '!!raw-loader!@site/../config/lastfm.json.example';
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_lastfm.md"
:::tip[Other Users]
@@ -16,11 +17,25 @@ import JsonConfig from '!!raw-loader!@site/../config/lastfm.json.example';
:::
<DetailsAdmo type="warning" summary="Limits for Old Scrobbles">
Last.fm will not accept scrobbles that are timestamped as **older than 2 weeks** from the current time.
This is not officially documented but has been confirmed by multiple people/mods in the last.fm support forums: [[1]](https://support.last.fm/t/importing-listening-history-spotify-apple-music-and-itunes-wmp/1424/3) [[2]](https://support.last.fm/t/suggestion-allow-last-fm-pro-members-to-use-the-api-to-submit-scrobbles-older-than-2-weeks/69190/2) [[3]](https://support.last.fm/t/how-to-scrobble-complete-spotify-history/44558)
Multi-scrobbler can queue scrobbles for you if the Last.fm service is unavailable, or if there is an issue with a specific scrobble. However, you are responsible for making sure these scrobbles are eventually sent to Last.fm (by retrying in the UI or retarting Last.fm client in UI) before the two week window has passed or else they will not be scrobblable *at all* unless the timestamp is changed.
A future update to Multi-scrobbler will enable editing of unsent scrobble data and modifying the timestamp from the UI.
If you have issues with specific scrobbles not being sent and retries/restarts do not fix it, please make sure to [create a new issue](https://github.com/FoxxMD/multi-scrobbler/issues/new?template=01-bug-report.yml) in a timely manner so that it can be resolved before the window has passed.
</DetailsAdmo>
You will need a [Last.fm](https://www.last.fm) account to use the Last.fm API.
After creating your account, [register for an API account here.](https://www.last.fm/api/account/create)
The callback URL in the API account form is not used, but to keep it consistent use the same value you set using the [Callback/Redirect URL](#callback-redirect-url) section.
The callback URL in the API account form is not used, but to keep it consistent use the same value you set using the [Callback/Redirect URL](#callbackredirect-url) section.
**Take note of your API Key and Secret after account creation.**
@@ -49,10 +64,5 @@ The Redirect URL can be explicitly specified in the configuration below, or by u
| `LASTFM_API_KEY` | Yes | | Api Key from your API Account |
| `LASTFM_SECRET` | Yes | | Shared secret from your API Account |
| `LASTFM_REDIRECT_URI` | No | `http://localhost:9078/lastfm/callback` | Url to use for authentication. Must include `lastfm/callback` somewhere in it |
| `LASTFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
import JsonConfig from '!!raw-loader!@site/../config/librefm.json.example';
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_librefm.md"
:::tip[Other Users]
@@ -15,9 +16,9 @@ import JsonConfig from '!!raw-loader!@site/../config/librefm.json.example';
:::
To use the official [Libre.fm](https://libre.fm/) instance you will need an account. Or, you can run your own Libre.fm instance [using the legacy source code](https://github.com/foocorp/gnu-fm).
To use the official [Libre.fm](https://libre.fm/) website you will need an account. Or, you can run your own GNU FM instance [using the source code](https://github.com/foocorp/gnu-fm).
No API account, [API Key, or Secret](https://github.com/libre-fm/developer/wiki/Libre.fm-fundamentals#api-keys) is needed to use the Libre.fm API. Instead, use **any values** for API Key/Secret in the configuration. Authentication is done by allowing access from your account on redirect.
No API account, [API Key, or Secret](https://github.com/libre-fm/developer/wiki/Libre.fm-fundamentals#api-keys) is currently needed to use the Libre.fm API. Instead, use **any values** for API Key/Secret in the configuration. Authentication is done by allowing access from your account on redirect.
| `LIBREFM_REDIRECT_URI` | No | `http://localhost:9078/librefm/callback` | Url to use for authentication. Must include `librefm/callback` somewhere in it |
| `LIBREFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
| `LIBREFM_URLBASE` | No | | Use for custom Libre.fm instance only. The host + path prefix EX `https://my.libre.example/2.0/` |
import Bluesky from "@site/src/components/snippets/_bluesky.mdx";
import JsonConfig from '!!raw-loader!@site/../config/rocksky.json.example';
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_rocksky.md"
:::tip[Other Uses]
@@ -13,7 +14,7 @@ To monitor and re-scrobble activity **from** Rocksky create a [Rocksky (Source)]
:::
[**Rocksky**](https://rocksky.app/) is music tracking and discovery platform built on the AT Protocol (Bluesky).
[**Rocksky**](https://rocksky.app/) is music tracking and discovery platform built on the AT Protocol and your your [**Atmosphere Account**](https://atmosphereaccount.com/).
<details>
@@ -23,13 +24,13 @@ To monitor and re-scrobble activity **from** Rocksky create a [Rocksky (Source)]
</details>
<DetailsAdmo type="warning" summary="Your Data Is Public">
<DetailsAdmo type="important" summary="Your Data Is Public">
The default Bluesky PDS is **public**, meaning your scrobbles are also public (read-only). This isn't any different than using Last.fm or Listenbrainz, in terms of privacy.
Rocksky **does not guarantee** that every scrobble you send will be persisted. Rocksky's design goal is to store scrobbles with **metdata-complete, 3rd party verifiable track information** so that it can be a metadata-rich discovery platform, rather than an exhaustive scrobble data source-of-truth.
@@ -41,22 +42,16 @@ If you need your scrobbles to be 100% captured then you should scrobble to an *a
## Setup
You will need a [Bluesky](https://bsky.app/) account to use Rocksky.
You will need an [**Atmosphere Account**](https://atmosphereaccount.com/) to use Rocksky.
:::note
Usage is adapted from [Rocksky docs](https://docs.rocksky.app/migrating-to-rocksky-scrobble-api-957839m0) on [docs.rocksky.app](https://docs.rocksky.app)
:::
* Navigate to [rocksky.app](https://rocksky.app/) and sign up/logn using your Bluesky account
* Then, navigate to the [RockSky developer dashboard](https://rocksky.app/apikeys) and obtain an API Key
* Navigate to [rocksky.app](https://rocksky.app/) and sign up/logn using your Atmosphere Account
* Then, navigate to the [Access Tokens](https://rocksky.app/access-tokens) in the Rocksky settings dropdown, and create a new token
## Configuration
### Handle
The handle used with multi-scrobbler should be your **full** ATProto handle, including TLD.
The handle used with multi-scrobbler should be your **full** [**Atmosphere Account**](https://atmosphereaccount.com/) handle, including TLD.
* For regular Bluesky account this will be like: `alice.bsky.social`
* For Bluesky accounts that [use their website as their account](https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial) this is your domain: `mydomain.com`
@@ -64,9 +59,23 @@ The handle used with multi-scrobbler should be your **full** ATProto handle, inc
If you do not include a TLD then multi-scrobbler will automatically append `.bsky.social` to your handle value.
In < 0.14.1, Multi-Scrobber used Rocksky's API Appication "API Key" for communication with a Listenbrainz-compatible API from Rocksky.
Going forward, this should be replaced with the official guidance in the [Setup](#setup) section above: use an **Access Token**. This uses the native Rocksky API which has more functionality.
If your configuration is using either
* (File/AIO) `'key': ...`
* (ENV) `ROCKSKY_KEY`
you should redo the [Setup](#setup) section and modify your configuration to use an Access Token instead.
Keys are now **deprecated** and functionality for them will be removed in a future version of Multi-Scrobbler.
@@ -9,6 +9,7 @@ import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/tealfm.json.example';
import Bluesky from "@site/src/components/snippets/_bluesky.mdx";
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_tealfm.md"
:::tip[Other Uses]
@@ -16,7 +17,7 @@ To monitor and re-scrobble activity from a ATProto Profile create a [teal.fm (So
:::
[**teal.fm**](https://teal.fm/) is a social discovery and web viewing service for scrobbles stored/available on the ATProto network/your repository in a [PDS (Personal Data Server)](https://github.com/Bluesky-social/pds).
[**teal.fm**](https://teal.fm/) is a social discovery and browsing app for scrobbles stored in your [**Atmosphere Account**](https://atmosphereaccount.com/).
<details>
@@ -46,6 +47,13 @@ To view your teal.fm scrobble data you can:
## Setup
:::note
Multi-scrobbler will work with *any* Atmosphere Account, not just Bluesky. See [Configuration](#configuration) below for provider-agnostic config.
:::
#### Bluesky
* Create a [Bluesky](https://bsky.app) account, if you don't already have one
@@ -57,10 +65,10 @@ To view your teal.fm scrobble data you can:
### Identifier
The ATPRoto identifier used with multi-scrobbler should be either:
The ATProto identifier used with multi-scrobbler should be either:
* A valid [DID](https://atproto.com/specs/did#at-protocol-did-identifier-syntax), starting with `did:plc:...` or `did:web:...`
* Your **full** ATProto handle, including TLD
* Your **full** [Atmosphere Account](https://atmosphereaccount.com/) ATProto handle, including TLD
If using a handle:
@@ -71,8 +79,5 @@ If using a handle:
If your identifier does not look like a DID and does not include a TLD then multi-scrobbler will automatically append `.bsky.social` to your handle value.
@@ -12,7 +13,7 @@ import FileExample from "../../src/components/FileExample";
import ScrobbleThreshold from "@site/src/components/snippets/_scrobble-threshold.mdx"
import AIOConfig from '!!raw-loader!../../../config/config.json.example';
import SpotifyConfig from '!!raw-loader!../../../config/spotify.json.example';
import SingleConfig from '!!raw-loader!../../../config/jellyfin.json.example';
:::tip
@@ -28,40 +29,35 @@ Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
* client/source specific json config files
* an all-in-one json config file
**MS will parse configuration from all configuration types.** You can mix and match configurations but it is generally better to stick to one or the other.
**MS will parse configuration from all configuration types.** You can mix and match configurations but it is generally better to stick to one type.
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
MS will parse environmental variables present in the OS/container when it is run. **This method means MS does not require files to run.**
MS will parse environmental variables present in the OS/container when it is run.
This method means MS **does not require config files** but it will still create and use data files in the [local](/installation/#config-and-data-directories) or [docker](/installation/?dockerSetting=storage#recommended-settings) data directory.
* You are the only person for whom MS is scrobbling for
* You have a very simple setup for MS such as one scrobble [Client](/configuration/clients) and one [Source](/configuration/sources) IE Plex -> Maloja
* You are the only person for whom MS is scrobbling for or
* You have a very simple setup for MS such as one scrobble [Client](/configuration/clients) and one [Source](/configuration/sources) IE Jellyfin -> Koito, or
* You are not comfortable editing JSON files
</details>
<details>
<summary>Config Example</summary>
For Docker container...
```shell
docker run -e "SPOTIFY_CLIENT_ID=yourId" -e "SPOTIFY_CLIENT_SECRET=yourSecret" ...
```
For Docker Compose
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
environment:
- SPOTIFY_CLIENT_ID=yourId
- SPOTIFY_CLIENT_SECRET=yourSecret
- MALOJA_URL=http://domain.tld:42010
- MALOJA_API_KEY=1234
- JELLYFIN_URL=192.168.0.110:8096
- JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56b
- JELLYFIN_USER=MyUser
- JELLYFIN_ID=myJellyin
# ...
# ...
```
@@ -76,77 +72,114 @@ Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
<TabItem value="file" label="File">
MS will parse configuration files located in the directory specified by the `CONFIG_DIR` environmental variable. This variable defaults to:
* Local installation -> `PROJECT_DIR/config`
* Docker -> `/config` (in the container) -- see the [install docs](../installation/installation.mdx#docker) for how to configure this correctly
* Local installation -> See [Config and Data Directories](/installation/#config-and-data-directories)
* Docker -> `/config` (in the container) -- see the [**Storage** tab in the Docker installation docs](/installation/?dockerSetting=storage#recommended-settings) for how to configure this correctly
* You have many of each type of **Source** you want to scrobble from IE 2x Plex accounts, 3x Spotify accounts, 1x
Funkwhale...
* You have more than one scrobble **Client** you want to scrobble to IE multiple Maloja servers
* You want only to scrobble to specific **Clients**
* You are comfortable editing JSON files
* You have more than one of the same type of **Source/Client** you want to use EX 2x Last.fm accounts, 3x Jellyfin accounts
* You want activity from specific **Sources** to scrobble to specific **Clients**
* You need to setup more advanced configuration for a Source/Client
* Most Source/Clients only support basic configuration through ENV, all configuration is possible using File/AIO
* Most Source/Clients only support basic configuration through ENV, more configuration is possible using File. AIO enables complete configuration.
</details>
:::tip
* There are **example configurations** for all Source/Client types and AIO config located in the [`/config`](https://github.com/FoxxMD/multi-scrobbler/tree/master/config) directory of this project. These can be used as-is by renaming them to `.json`.
* For docker installations these examples are copied to your configuration directory on first-time use.
* These are the same examples you will find in the **Configuration** section of each Source/Client
* There is also a [**kitchensink example**](/configuration/kitchensink) that provides examples of using all sources/clients in a complex configuration.
:::
Each file is named by the **type** of the Client/Source found in below sections. Each file as an **array** of that type of Client/Source.
Each config file is named after the **type** of the Client/Source. Within each file, a JSON **array** defines all of the instances for that type of Client/Source.
<summary>Multiple of Same Config Example</summary>
```yaml title="jellyfin.json"
[
{
"name": "Foxx JF Server",
"id": "foxxJf",
"data": {
"url": "http://localhost:8096",
"user": "FoxxMD",
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
}
},
// highlight-start
{
"name": "Foo's JF Server",
"id": "fooJf",
"data": {
"url": "http://192.168.0.150:8096",
"user": "foo",
"apiKey": "9c5bb56bd6540c756fbf481ebd9c5c40"
}
}
// highlight-end
]
```
</details>
You can find an example of the file name that should be used under the **File** Configuration for each Source/Client. Note: this is the same value that is used for `type` in AIO configs.
</TabItem>
<TabItem value="aio" label="File AIO">
MS will parse an **all-in-one** configuration file located in the directory specified by the `CONFIG_DIR` environmental variable. This variable defaults to:
MS will parse an **all-in-one** configuration file (`config.json`) located in the directory specified by the `CONFIG_DIR` environmental variable. The file's location defaults to:
* Local installation -> `PROJECT_DIR/config/config.json`
* Docker -> `/config/config.json` (in the container) -- see the [install docs](../installation/installation.mdx#docker) for how to configure this correctly
* Local installation -> See [Config and Data Directories](/installation/#config-and-data-directories)
* Docker -> `/config/config.json` (in the container) -- see the [**Storage** tab in the Docker installation docs](/installation/?dockerSetting=storage#recommended-settings) for how to configure this correctly
* You have many of each type of **Source** you want to scrobble from IE 2x Plex accounts, 3x Spotify accounts, 1x
Funkwhale...
* You have more than one scrobble **Client** you want to scrobble to IE multiple Maloja servers
* You want only to scrobble to specific **Clients**
* You need to setup [monitoring/webhooks](#monitoring)
* You want to setup defaults for all Sources/Clients
* ...All of the reasons listed in [File](./?configType=file#configuration-types)
* You want to setup [monitoring/webhooks](#monitoring), default settings for all Sources/Clients, or global config options
* You want to use [Transformers](/configuration/transforms) (other than what is provided for [Musicbrainz ENV](/configuration/transforms/musicbrainz/#env-configuration))
* You want complete control over all potential MS configuration
</details>
**The AIO config also enables setting default options for sources/clients as well as global options for MS itself.**
:::tip
* An example AIO config files can be found in the project directory at [`/config/config.json.example`](https://github.com/FoxxMD/multi-scrobbler/tree/master/config/config.json.example)
* For docker installations this example is copied to your configuration directory on first-time use.
* There is also a [**kitchensink example**](/configuration/kitchensink) that provides examples of using all sources/clients in a complex configuration.
* **AIO config is the "complete" way to configure MS.**
* All settings available in File/ENV are available in AIO and AIO has *more* exclusive settings.
* AIO enables configuring **default settings** for all Sources/Clients
* There is a [**kitchensink example**](/configuration/kitchensink) that provides examples of using all sources/clients in a complex configuration.
* Use the [**Config Playground**](/playground) to explore the entire config schema with an editable example
* The example used can also be found in the repo directory at [`/config/config.json.example`](https://github.com/FoxxMD/multi-scrobbler/tree/master/config/config.json.example) in the example below
:::
[**Explore the schema for this configuration, along with an example generator and validator, here**](https://json-schema.app/view/%23?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
<details>
<summary>Config Example</summary>
@@ -154,6 +187,32 @@ Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
<DetailsAdmo type="important" summary="Source/Client AIO Configs require a 'type' property">
Compared to their [File](./?configType=file#configuration-types) equivalent, Sources/Clients configured using AIO require an additional `type` property.
```yaml title="config.json"
{
"sources": [
{
"name": "Foxx JF Server",
// highlight-start
"type: "jellyfin",
// highlight-end
"id": "foxxJf",
"data": {
"url": "http://localhost:8096",
"user": "FoxxMD",
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
}
}
]
}
```
You can find an example of the required `type` value by referring to the **AIO** tab in each Source/Client's Configuration section example. This is the same value that is used as the name of individual [File](./?configType=file#configuration-types) config files.
</DetailsAdmo>
</TabItem>
</Tabs>
@@ -168,7 +227,7 @@ Each entry for a Source/Client includes a **Configuration** section that describ
## Secrets Interpolation
When using [File](./?configType=file#configuration-types) or [AIO](./?configType=aio#configuration-types) Configuration, Multi-Scrobbler can interpolate Environmental Variables into your json files. This can be used, for example, to keep sensitive data (like [Last.fm Client/Secret](/configurations/clients/lastfm#configuration)) out of your configuration files so that they can be committed to git.
When using [File](./?configType=file#configuration-types) or [AIO](./?configType=aio#configuration-types) Configuration, Multi-Scrobbler can interpolate Environmental Variables into your json files. This can be used, for example, to keep sensitive data (like [Last.fm Client/Secret](/configuration/clients/lastfm#configuration)) out of your configuration files so that they can be committed to git.
Multi-scrobbler will look for patterns in text fields within _all_ of your json files:
@@ -179,7 +238,7 @@ Multi-scrobbler will look for patterns in text fields within _all_ of your json
<summary>Example</summary>
Given this [Last.fm](/configurations/clients/lastfm#configuration) [File](./?configType=file#configuration-types) config:
Given this [Last.fm](/configuration/clients/lastfm#configuration) [File](./?configType=file#configuration-types) config:
These options affect multi-scrobbler's behavior and are not specific to any source/client.
## Base URL
### Base URL
Defines the URL that is used to generate default redirect URLs for authentication on [spotify](/configuration/sources/spotify) and [lastfm](/configuration/clients/lastfm) -- as well as some logging hints.
Defines the URL that is used to generate default redirect URLs for authentication for services like [spotify](/configuration/sources/spotify) and [lastfm](/configuration/clients/lastfm) -- as well as some logging hints.
* Default => `http://localhost:9078`
* Set with [ENV](./?configType=env#configuration-types) `BASE_URL` or `baseUrl` [all-in-one configuration](./?configType=aio#configuration-types)
@@ -279,79 +335,51 @@ Useful when running with [docker](../installation/installation.mdx#docker) so th
</details>
### Caching
## Caching
Multi-scrobbler caches some activities to persist important data across restarts, reduce external API calls, and make some actions faster.
Multi-scrobbler implements caching to reduce external API calls and make some actions faster.
All of the activities below are **always** cached **in-memory** with an optional, configurable [**secondary** store](#secondary-caching-configuration) for persistence.
A default **in-memory** cache store is used so that you always benefit from some caching. An optional, [**secondary** store](#secondary-caching) can be configured for greater caching capabilities.
<Tabs groupId="cachedThings" queryString>
<TabItem value="scrobbles" label="Scrobbles">
**Queued** and **Failed** Scrobbles are cached so that any un-scrobbled data you have is persisted across restarts of multi-scrobbler.
:::tip
By default, this data use a [Secondary](#secondary-caching-configuration) [File](./?cacheType=file#secondary-caching-configuration) store, configured for you automatically.
If you have configured a [persisted volume/bind mount](/installation#storage) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)) then you are already done. If you are not persisting this directory then you should consider setting up [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration) for this.
:::
##### Configuration
Use any [Secondary Cache](#secondary-caching-configuration), the config examples below show the default values:
The results of [transform rules](/configuration/transforms) are cached so that if a scrobble with identical data (track/artists/album) is identified and it has the same set of transform rules then the cached transform results can be applied.
</TabItem>
<TabItem value="metadata" label="Transform API Calls">
API Calls to external (metadata) services used to [Enhance Scrobbles](/configuration/transforms), like calls to [Musicbrainz](/configuration/transforms/musicbrainz), can be cached to avoid duplicate calls and speed up scrobble transformations.
</TabItem>
</Tabs>
</TabItem>
<TabItem value="auth" label="Auth Data">
</DetailsAdmo>
Authentication sessions/tokens/etc... are cached for quicker requests and for persistence across restarts.
Auth caching defaults to a **file** that is stored in the `DATA_DIR` directory using the pre-defined file name `ms-auth.cache`.
By default, this data use a [Secondary](#secondary-caching-configuration) [File](./?cacheType=file#secondary-caching-configuration) store, configured for you automatically.
This provides automatic persistence across restarts for long-lived auth data/credentials if you have configured a [persisted volume/bind mount](/installation?dockerSetting=storage#recommended-settings) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)).
If you have configured a [persisted volume/bind mount](/installation#storage) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)) then you are already done. If you are not persisting this directory then you should consider setting up [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration) for this.
If you wish to use the [secondary store](#secondary-caching) for caching Auth you must explicitly configure it. This is because valkey can potentially be *ephemeral* if you do not provide a volume for its data directory.
:::
To explicitly configure auth to use the secondary store:
##### Configuration
Use any [Secondary Cache](#secondary-caching-configuration), the config examples below show the default values:
| `CACHE_AUTH` | No | `file` | The cache type to use |
| `CACHE_AUTH_CONN` | No | `/config` | |
```yaml title="compose.yaml"
services:
multi-scrobbler:
# ...
environment:
// highlight-start
- CACHE_AUTH=valkey
// highlight-end
# ...
```
</TabItem>
@@ -361,8 +389,7 @@ Use any [Secondary Cache](#secondary-caching-configuration), the config examples
{
"cache": {
"auth": {
"provider": "file",
"connection": "/config"
"provider": "valkey"
}
},
// ...
@@ -371,93 +398,19 @@ Use any [Secondary Cache](#secondary-caching-configuration), the config examples
</TabItem>
</Tabs>
</TabItem>
<TabItem value="metadata" label="Transform API Calls">
</DetailsAdmo>
API Calls to external (metadata) services used to [Enhance Scrobbles](/configuration/transforms), like calls to [Musicbrainz](/configuration/transforms/musicbrainz), can be cached to avoid duplicate calls and speed up scrobble transformations.
### Secondary Caching Store {#secondary-caching}
By default, these calls are only cached in memory. If you wish for cached calls to be persisted across restarts then setup [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration).
Using a secondary store enables:
##### Configuration
* persistence of cached data across restarts
* a larger store (more data is saved)
* a longer time-to-live in the store (cached data is fetchable for a longer period)
Use any [Secondary Cache](#secondary-caching-configuration), the config examples below show the default values:
These benefits are particularly beneficial when using [transforms](/configuration/transforms) like Musicbrainz and it is **strongly recommended** for these scenarios.
| `CACHE_METADATA` | Yes | `valkey` | The cache type to use |
| `CACHE_METADATA_CONN` | Yes | | The host/IP and port to connect to EX: `redis://192.168.0.120:6379` |
```yaml title="compose.yaml"
services:
multi-scrobbler:
# ...
environment:
// highlight-start
- CACHE_VALKEY=redis://192.168.0.120:6379
// highlight-end
# ...
```
</TabItem>
@@ -512,10 +490,7 @@ Example
```json5 title="config.json"
{
"cache": {
"metadata": {
"provider": "valkey",
"connection": "redis://192.168.0.120:6379"
}
"valkey": "redis://192.168.0.120:6379"
},
// ...
}
@@ -524,11 +499,275 @@ Example
</TabItem>
</Tabs>
## Database
Multi-scrobbler depends on a SQLite database (`ms.db`) that is created on first run and stored in the [`CONFIG_DIR`](/installation/?dockerSetting=storage#recommended-settings). When upgrading Multi-scrobbler version, if there are any required database changes than this database is [automatically backed up and migrated.](/updating#database)
The database stores *all* Plays for your Sources/Clients as well as metadata and debugging information to help troubleshoot issues. Each Play is associated with a Source/Config in the database based on your configuration.
You **must set IDs for each Source/Client** so that the database can identify these even when the configuration is changed.
### Retention
<DetailsAdmo status="note" summary="How much storage does the database use?">
The amount of data stored for each Play can widely vary based on a few factors:
* how much data the Source service exposes
* how many clients each Source is scrobble to (Plays are duplicated for each Client a Source sends a scrobble to)
* if you are using any [Transforms](/configuration/transforms) the diff of each step is stored, along with any external request/response data (like [Musicbrainz queries](/configuration/transforms/musicbrainz))
The [MS repository contains a benchmark](https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/tests/database/drizzle.test.ts#L551) to measure an average database size in a few common scenarios.
<Tabs groupId="dbSize">
<TabItem label="Minimal" value="minimal">
Assuming your Sources send a minimal amount of data or you have compacted all plays:
| Play Count | DB Size |
| ---------- | -------- |
| 100 | `160kb` |
| 1000 | `1MB` |
| 10k | `10.2MB` |
</TabItem>
<TabItem label="With Original Input" value="input">
Assuming your Sources have a non-trivial amount of input data (like Spotify or Listenbrainz) that is not compacted:
| Play Count | DB Size |
| ---------- | -------- |
| 100 | `356kb` |
| 1000 | `3MB` |
| 10k | `29.6MB` |
</TabItem>
<TabItem label="All Debug Deta" value="all">
Assuming your Sources/Clients:
* have a non-trivial amount of input data (like Spotify or Listenbrainz)
* and has many [Transforms](/configuration/transforms) steps that include requests
* nothing is compacted
| Play Count | DB Size |
| ---------- | -------- |
| 100 | `684kb` |
| 1000 | `6.28MB` |
| 10k | `61.3MB` |
</TabItem>
</Tabs>
### Debug Mode
</DetailsAdmo>
A retention policy can be configured to delete Plays, or unused debug data, from the database after a certain amount of time. If no configuration is provided then a default policy is used that should be reasonable for most users.
<Tabs groupId="policy" queryString>
<TabItem label="Compaction" value="compaction">
The **Compaction** Retention Policy is used to delete different types of debug data from your stored Plays.
This is a useful way to reduce used storage space when you are not having problems with your Plays, or iterating on a configuration, that requires referencing all this extra data.
There are two types of data that can be compacted (deleted from the Play):
* `input` - this is the untouched data retrieved by Multi-scrobbler, from a Source, and used to generate a Play/scrobble. This can be used to reconstruct and replay a Play, when used from troubleshooting or reporting an issue
* `transform` - this is all of the steps generated by [transforms](/configuration/transforms), the diff of the Play resulting from the step, and any request/responses used to complete the step
:::note[Defaults]
When no Compact configuration is provided, Multi-scrobbler uses this policy:
* Compact (delete) `input` and `transform` data on all Plays after 3 days
:::
#### Configuring Compaction Policy
<details>
<summary>Details</summary>
Each value in the configuration properties below can be either
* a number of seconds EX `3600` = 10 minutes
* a unit of a common duration with the pattern `X unit` EX
* `30 minutes`
* `5 hours`
* `2 days`
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
* `COMPACT_PROPERTIES` - which properties to compact
* `RETENTION_COMPACT_AFTER` - Default to use for all Plays
* `RETENTION_COMPACT_COMPLETED_AFTER` - Compact only completed Plays after...
* `RETENTION_COMPACT_FAILED_AFTER` - Compact only failed Plays after...
* `RETENTION_COMPACT_DUPED_AFTER` - Compact only duped/discard Plays after...
Example
```ini
# only delete input when compacting
COMPACT_PROPERTIES=input
# compact all plays after 3 days
RETENTION_COMPACT_AFTER=3 days
# specifically compact completed plays after 30 minutes
RETENTION_COMPACT_COMPLETED_AFTER=30 minutes
```
</TabItem>
<TabItem value="aio" label="AIO">
Compacting all Play types and deleting both input and transform:
```json title="config.json"
{
"database": {
"retention": {
"compactAfter": "3 days",
"compact": [
"input",
"transform"
]
}
}
}
```
* Delete only input during compacting
* Compact all after 3 days except completed which compacts after 30 minutes
```json title="config.json"
{
"database": {
"retention": {
"compactAfter": {
"completed": "30 minutes",
"duped": "3 days",
"failed": "3 days"
},
"compact": [
"input"
]
}
}
}
```
</TabItem>
</Tabs>
</details>
</TabItem>
<TabItem label="Deletion" value="deletion">
The **Deletion** Retention Policy is used to delete different types of stored Plays from Multi-scrobbler database.
**This does not delete Plays from your Clients.** It's only deleting the "in-flight" data MS used to create the scrobble that was eventually sent to your clients.
:::warning[Plays Should be Ephemeral]
**Multi-Scrobbler is not designed to store Plays/Scrobbles indefinitely.**
It should scale fine for thousands of scrobbles but it not meant to store 10's of thousands of scrobbles forever. It is not a scrobbler server.
You **should** set a reasonable deletion policy so that MS stores less than 1000 scrobbles at a time, ideally less.
:::
:::note[Defaults]
When no Deletion policy configuration is provided, Multi-scrobbler uses this policy:
* Delete all Plays after 7 days
:::
#### Configuring Deletion Policy
<details>
<summary>Details</summary>
Each value in the configuration properties below can be either
* a number of seconds EX `3600` = 10 minutes
* a unit of a common duration with the pattern `X unit` EX
* `30 minutes`
* `5 hours`
* `2 days`
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
* `RETENTION_DELETE_AFTER` - Default to use for all Plays
* `RETENTION_DELETE_COMPLETED_AFTER` - Delete only completed Plays after...
* `RETENTION_DELETE_FAILED_AFTER` - Delete only failed Plays after...
* `RETENTION_DELETE_DUPED_AFTER` - Delete only duped/discard Plays after...
Example
```ini
# delete all plays after 3 days
RETENTION_DELETE_AFTER=3 days
# specifically, delete completed plays after 30 minutes
RETENTIOND_DELETE_COMPLETED_AFTER=30 minutes
```
</TabItem>
<TabItem value="aio" label="AIO">
Deleting all Play types after 3 days:
```json title="config.json"
{
"database": {
"retention": {
"deleteAfter": "3 days"
}
}
}
```
* Delete all after 3 days except completed which are deleted after 30 minutes
```json title="config.json"
{
"database": {
"retention": {
"deleteAfter": {
"completed": "30 minutes",
"duped": "3 days",
"failed": "3 days"
},
}
}
}
```
</TabItem>
</Tabs>
</details>
</TabItem>
</Tabs>
## Debug Mode
Turning on Debug Mode will
@@ -549,13 +788,13 @@ To set debug mode either add it to [AIO `config.json`](./?configType=aio#configu
or set the [ENV](./?configType=env#configuration-types) `DEBUG_MODE=true`
### Disable Web
## Disable Web
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.
:::warning
Any **[ingress-based sources](/configuration/sources#ingress) will be unusable** (Webscrobbler, etc...) if this is disabled.
Any **[ingress-based sources](/configuration/sources?sourceComm=ingress#by-communication-method) will be unusable** (Webscrobbler, etc...) if this is disabled.
:::
@@ -566,7 +805,19 @@ Disable using either:
## Monitoring
Multi-scrobbler supports some common webhooks and a healthcheck endpoint in order to monitor Sources and Clients for errors.
### Upstream Services Status
A status page for monitoring the reachability of public services used by some Sources/Clients is available at
**https://status.multi-scrobbler.app**
This monitor runs on a VPS and checks the uptime of *actual* APIs for these services, not just landing pages. On a ~minute interval it checks:
* Listenbrainz API
* Musicbrainz API
* CoverArtArchive API
* Last.fm API
* A few MS-hosted support services
### Webhook Configurations
@@ -617,7 +868,7 @@ Webhooks are configured in the AIO [config.json](#configuration-types) file unde
#### [Gotify](https://gotify.net/)
Refer to the [config schema for GotifyConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FGotifyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
Refer to the [config schema for GotifyConfig](/playground)
multi-scrobbler optionally supports setting message notification priority via `info` `warn` and `error` mappings.
Refer to the [config schema for NtfyConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FNtfyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
Refer to the [config schema for NtfyConfig](/playground)
multi-scrobbler optionally supports setting message notification priority via `info` `warn` and `error` mappings.
Refer to the [config schema for AppriseConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FAppriseConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
Refer to the [config schema for AppriseConfig](/playground)
multi-scrobbler supports [stateless](https://github.com/caronc/apprise-api?tab=readme-ov-file#stateless-solution) and [persistent storage](https://github.com/caronc/apprise-api?tab=readme-ov-file#persistent-storage-solution) endpoints as well as [tags](https://github.com/caronc/apprise-api?tab=readme-ov-file#tagging)/
@@ -713,5 +964,37 @@ It includes metrics for:
* Count of Queued/Scrobbled/Deadletter scrobbles per [Client](/configuration/clients)
* Number of issues per Source/Client
* If any of these metrics is > 0 it means your Source/Client is not operating normally
* Caching metrics
Additionally, general process metrics like cpu and memory usage can be enabled with the env `PROMETHEUS_FULL=true`
Additionally, general process metrics like cpu and memory usage can be enabled with the env `PROMETHEUS_FULL=true`
<details>
<summary>Example Usage</summary>
Update your [Prometheus Configuration file](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#configuration-file) (usually at `/etc/prometheus.yml`) to add a job to the `scrape_configs`[section](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config) like this:
Github user [4rft5](https://github.com/FoxxMD/multi-scrobbler/issues/507#issuecomment-4144982295) created a Grafana dashboard that uses the Prometheus metrics from above.
description: Common Scenarios and Solutions for Duplicate Scrobbles
---
Multi-scrobbler's flexibility in configuration allows you to build some complex pipelines for scrobbling but it can also be a footgun for duplicating scrobbles.
## How Duplicates Are Detected
Multi-scrobbler uses multiple techniques for evaluating the **similarity** of text in scrobbles against existing data.
These techniques are used on all text from a scrobble (title, artist, album...):
<details>
<summary>Character and string normalization</summary>
Symbols and multiple whitespaces are removed. All characters are converted to lowercase. Non ascii characters are converted to their ascii counterparts.
```
this string! is the. same => this string is the same
this string is the same => this string is the same
ThIs STRING iS THe SAMe => this string is the same
Dina Ögon => Dina Ogon
Nanã => Nana
```
</details>
<details>
<summary>Word-order invariant text similarity</summary>
Strings with multiple words (tokens) are re-arranged so that tokens are ordered to be as close to other, similar tokens are possible:
Example 1
```
String A: there change is => there change is
String B: there is change => there change is
```
Example 2
```
String A: you, doin hwat are => you doin hwat are
String B: what are you doing => you doing what are
```
Then, the strings are independently compared using [Levenshtein Distance](https://en.wikipedia.org/wiki/Levenshtein_distance) and [Dice's Coefficient](https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient). The algorithm that gives the highest score for similarity is used so that matching is aggressive.
</details>
For **Artists**, Multi-scrobbler attempts to extract unique Artists from common patterns in **single-string** artist values and track **titles**.
<details>
<summary>Matching Common Artist Patterns</summary>
Common joiners and patterns are detected in artist/track strings and the artists are extracted. Example of common joiners are: `ft.` `feat` `vs.` etc...
Example 1
```
Melendi, Ryan Lewis, The Righteous Brothers (featuring Joan Jett & The Blackhearts, Robin Schulz)
becomes
Melendi, Ryan Lewis, The Righteous Brothers, Joan Jett, The Blackhearts, Robin Schulz
* MS uses "smart" heuristics to prevent artists with actual punctuation (`Tyler, The Creator`) from accidentally being separated
* The extraction examples above are done **only for matching** other scrobble data (which is also parsed like the above), the resulting scrobble data is never transformed unless the user explicitly configures it via a [native transform](/configuration/transforms/native)
:::
</details>
The **timestamps** for Scrobbles are compared at different levels of granularity based on where they originate from.
<details>
<summary>Temporally Comparing Scrobbles</summary>
Timestamp closeness for two scrobbles can be classified in one of these states:
**Exact**
Timestamps differ by 1 seconds or less.
**Close**
Timestamps differ by less than the known granularity of the Source they are from. For most Sources (and all Clients) this is 10 seconds or less. For some sources (subsonic) the granularity is 60 seconds because they only report "now playing" changes every 60 seconds.
**During**
If the Play was monitoring in real-time by Multi-scrobbler then it knows the time range during which the Play was listened to. If an existing scrobble with similar artist/track/album has a timestamp that occurred *during* the known listening time then it is classified as during.
**Fuzzy**
If the timestamp occurs within a few seconds +/- of (existing scrobble + track duration) then, potentially, the Source timestamped the existing scrobble at the *end* of when the user listened to it, rather than when the user *started* listening to it. This is common for Spotify plays.
___
Each of the above classification results in a different score that is *roughly* descending from the order given above. Exact score is based on the Souce of the scrobble and if it was monitored in real-time by Multi-Scrobbler.
</details>
Finally, all of the above comparison/normalization techniques are combined when comparing a "new" Play/Scrobble against existing data. Each comparison is given a weighted score. If the score is above the match threshold then it is marked as a duplicated.
<details>
<summary>Match Scoring Example</summary>
```
Frédéric Chopin - Ballade No. 4 in F Minor, Op. 52 @ 2026-05-04T13:01:51-04:00 <-- New scrobble
Frédéric Chopin / Krystian Zimerman - Ballade No. 4 in F Minor, Op. 52 @ 2026-05-04T13:01:48-04:00 <-- Existing scrobble from client
```
After comparing against all existing scrobbles from the client, in a timerange around the new scrobble, MS finds a match and logs the match along with the score breakdown of the consistuent parts:
```
Source:
Frédéric Chopin - Ballade No. 4 in F Minor, Op. 52 @ 2026-05-04T13:01:51-04:00 (S)
Closest Scrobble:
Frédéric Chopin / Krystian Zimerman - Ballade No. 4 in F Minor, Op. 52 @ 2026-05-04T13:01:48-04:00 (S)
Artist: (0.30 + Whole Match Bonus 0.52) * (0.3 + Whole Match Bonus 0.05) = 0.29
Title: 1.00 * 0.4 = 0.40
Time: (Close) 1 * 0.5 = 0.50
Time Detail => Existing: 13:01:48-04:00 - Candidate: 13:01:51-04:00 | Temporal Sameness: Close | Play Diff: 3s (Needed <10s) | Range Comparison N/A
Score 1.19 => Matched!
```
</details>
All of the above techniques and scoring gives Multi-scrobbler a *very good chance* of detecting duplicate scrobbles, regardless of where they were originally from. Of Multi-Scrobbler's [400+ unit tests](https://github.com/FoxxMD/multi-scrobbler/tree/master/src/backend/tests), 70+ are related to testing scoring, artist extraction, and [fuzzing](https://en.wikipedia.org/wiki/Fuzzing) string normalization/simiarlity.
The dupe detection is so good that it is virtually gauranteed for [most configurations](#valid-configs) and is used for *backlogging* missed scrobbles for Sources that support fetching history.
**However**, it's not possible to gaurantee this for *all* possible Multi-scrobbler configurations. See the below sections to learn more.
## Configurations Where Duplicates DO NOT Occur {#valid-configs}
* Any Source/Client configuration where there is only one Source and one Client and they are not the same service/account
* Any Source/Client configuration where scrobbles only flow in **one direction**
* IE you *do not* have a Last.fm **Client** Account A that is *also* a Last.fm **Source** Account A
:::tip
**The majority of simple Multi-scrobbler configurations fall into these categories. If your workflow is straightforward you do not need to worry about duplicates at all.**
:::
Multi-scrobbler can handle recieving the same data from a Source multiple times. This is fine because the data is *always the same* and *always unique* to each Source. Since MS scrobbled the data to your clients you can be pretty confident that the *same* cleanup/matching process for that data will result in an indentical/similar match to the existing scrobble.
IE Same inputs in -> same inputs out.
**Even this scenario with multiple, mixed Sources is OK:**
Because the scrobbles coming from each **Source** are **independent** of each other. And they end up in Clients that are not *also* used as Sources.
:::tip
This is a common scenario when scrobbles are *backlogged* from a Source IE on startup MS fetches the last 50 Plays from Spotify/Lastfm/Listenbrainz.
:::
## Configurations Where Duplicates CAN Occur {#invalid-configs}
### Upstream Scrobbles Sent to Multiple Sources
In this scenario your device using Spotify is configured in MS as a Spotify **Source** but it **also** is scrobbling to Last.fm (via Pano Scrobbler, as an example).
❌ **Time Race Condition**: Potentially, the scrobbles arrive in Multi-scrobbler at exactly the same time. There are no existing scrobbles in Koito to check against, yet. So Multi-scrobbler scrobbles both to Koito, resulting in duplicate scrobbles.
❌ **Different Scrobble Data**: Pano Scrobbler, or Last.fm, [may modify the scrobble data.](#lastfm) When MS recieves the scrobble from Last.fm it may be *just different enough* that it does not get detected as a duplicate.
### Sources and Clients are Dependent {#dependent-source-client}
In this scenario, you have a **Client** that is also a **Source** and that Source is feeding scrobbles to a different Client that receives scrobbles from everywhere (or has some overlap with the original Client).
❌ **Different Scrobble Data**: [Last.fm may modify the scrobble data.](#lastfm) When MS recieves the scrobble from Last.fm it may be *just different enough* that it does not get detected as a duplicate.
❌ **Consistency Issues**: If the Koito Client has a backed up queue or is not currently scrobbling (imagine it was Last.fm or Listenbrainz, and they had upstream availability issues), then there is no existing scrobble yet and nothing to match against.
## Configuration Solutions to Avoid Duplicates
### Independent Sources
Use a configuration mentioned in [Configurations Where Duplicates DO NOT Occur](#valid-configs): make sure all of your Sources are **independent**. The activity/scrobbles from each Source do not have any upstream duplicates of activity.
Imagine you have a scenario where a device/player can **only** scrobble to Last.fm. But you want to use MS for all other devices/sources and still have all your scrobbles end up in your "main" Last.fm account for social reasons/stats.
Instead of using the same Last.fm account for all activity and potentially having [dependent client/sources](#dependent-source-client), create an *additional* last.fm account that is used *only for the device that needs it*. This ensures the scrobbles from that device are isolated from the main account and do not prevent duplicates, so you can continue to use the main account as an MS Client:
### Mirror-Only Clients or Limiting Scrobble Destination {#isolated-sources}
In Multi-scrobbler, by default, all Sources scrobble to all Clients. Using [Limit Scrobble Destinations](/configuration/sources/#limiting-scrobble-destination) configuration can ensure that your dependent Source/Clients only scrobble to destinations where the scrobbles won't be duplicated. And/or limit other Sources to *not* scrobble to dependent Sources/Clients.
Here is an example where all Sources only scrobble to a *dependent* Last.fm Client, and then the *dependent* Last.fm Source only scrobbles to a "mirror-only" (backup) Koito Client:
If using one of the above solutions is not possible you can also try to *aggressively normalize all scrobble data* using [the Musicbrainz transform stage](/configuration/transforms/musicbrainz).
In this scenario you still have [dependent Source/Clients](#dependent-source-client) that are [not isolated](#isolated-sources) but you can *potentially* improve duplicate detection by first using a transform stage to match scrobbles against the Musibrainz database and normalize their track/artist/album data.
:::warning
**There is no gaurantee this will work** but it is better than nothing. You should attempt to re-configure MS to use any of the other solutions before using this one.
Last.fm will happily accept almost any scrobble data you throw at it.
It can then decide, opaquely, to match or modify that scrobble to some more "official" data it has server-side. This could be modifying the artists, title, or album sent in the scrobble. It can also mean modifying the [`mbid` (unique identifier)](https://musicbrainz.org/doc/MusicBrainz_Identifier) or adding what it _thinks_ is the correct one.
There is nothing in the scrobble history API response from Last.fm that identifies it has done any of this. And [it's not super great at being right about these modifications](https://github.com/FoxxMD/multi-scrobbler/issues/333#issuecomment-3145161339).
What this means for you, the user, is that this is very plausible scenario:
* You scrobble `Artist Foo, Artist Bar - My Cool Track` to Last.fm via Pano Scrobbler (outside of Multi-scrobbler)
* Last.fm modifies the data to `The Artist Foo - My Cool Track ft. Artist Bar (Album Version)`
* You _also_ scrobble `Artist Foo, Artist Bar - My Cool Track` to Multi-scrobbler via Pano LFM endpoint **Source**
* MS forwards this to Koito Client as-is
* Soon after, MS polls your Last.fm **Source** for new scrobbles and finds `The Artist Foo - My Cool Track ft. Artist Bar (Album Version)`
* It queries Koito to see if this was scrobbled
* It sees that a scrobble did happen at almost the same time BUT the track titles and artist strings are _just different enough_ that it decides it's not a duplicate
* MS forwards the scrobble to Koito and you now have a duplicate
This behavior is unique to Last.fm. It's truly the **worst** out of all the scrobbler services in terms of preserving your data as-is, or providing a unqiue identifier for your data.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.