---
title: Spotify Stage
description: Enhancing Scrobbles with Spotify
toc_min_heading_level: 2
toc_max_heading_level: 5
---
The **Spotify** [Stage](/configuration/transforms#stage) matches your Play data with the [Spotify](https://open.spotify.com/) catalog, using the [Spotify Web API](https://developer.spotify.com/documentation/web-api). If the match is confident enough then Multi-Scrobbler uses it to correct and fill-in missing information in your Play data.
**This Stage is useful for standardizing your Scrobble's Play data, regardless of the Source it is coming from**, and is a good complement (or alternative) to the [Musicbrainz Stage](/configuration/transforms/musicbrainz) if you'd prefer matches to come from Spotify's own catalog.
:::important
Like the [Spotify Source](/configuration/sources/spotify), this Stage requires **Spotify Premium** to access the Spotify API. This is not due to any specific functionality this Stage uses but due to Spotify now requiring Premium for *any* access to their API.
See the **Premium Account Required** expandable section in the Spotify Source docs for your options, if you are not a premium user.
:::
:::tip
If you are using [ENV Config](/configuration?config-type=env#configuration-types) for multi-scrobbler and just want a quick and easy setup, skip to [**ENV Configuration**](#env-configuration).
:::
:::tip
Set up [Valkey Caching](/configuration?cachedThings=metadata#caching) to cache Spotify API calls for faster processing.
:::
## Configuration
### API Setup
This Stage uses Spotify's API to search/lookup the Spotify catalog. This does **not** require a user to log in -- only an app `clientId`/`clientSecret` is needed. [Create a Spotify application](https://developer.spotify.com/documentation/web-api/concepts/apps) if you don't already have one (the same application used for the [Spotify Source](/configuration/sources/spotify) can be reused here).
```json5 title="config.json"
{
// ...
"transformers": [
{
"type": "spotify",
"name": "MySpotify",
"data": {
"clientId": "787c921a2a2ab42320831aba0c8f2fc2",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a"
},
}
]
}
```
Rate Limiting
`rate` can be used to configure how many requests are made to the Spotify API, defined by **max number of requests** within **timespan of N seconds** (default `10 req/1s`):
```json5
{
"type": "spotify",
"name": "MySpotify",
"data": {
"clientId": "787c921a2a2ab42320831aba0c8f2fc2",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
// highlight-start
"rate": {
"requests": 10,
"perTime": 1
}
// highlight-end
},
}
```
### Stage Configuration
All of the properties found in [**Matching with Spotify**](#matching-with-spotify) section are configured in [Stage Configuration](/configuration/transforms#configuring-stages) as `defaults`.
These are all also **optional** so you can use the Spotify Transform stage without configuring any of this.
```json5 title="config.json"
{
// ...
"transformers": [
{
"type": "spotify",
"name": "MySpotify",
"data": {
"clientId": "787c921a2a2ab42320831aba0c8f2fc2",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a"
},
// highlight-start
"defaults": {
"score": 0.6,
"deprioritizeCompilations": true
}
// highlight-end
}
]
}
```
### Rules and Hooks
[Add your Stage](/configuration/transforms/#stage) to a Source or Client by specifying it in a [Hook](/configuration/transforms/#hook):
```json5 title="subsonic.json"
[
{
"name": "MySubsonic",
"data": { /* ... */},
"options": {
"playTransform": {
"preCompare": [
{
"type": "spotify",
"name": "MySpotify"
}
]
}
}
}
]
```
Each [**Stage Rule**](/configuration/transforms#stage-rules) (`title`/`artists`/`albumArtists`/`album`/`duration`/`meta`/`art`) works the same way as it does for the [Musicbrainz Stage](/configuration/transforms/musicbrainz#rules-and-hooks): either a boolean specifying whether the transformed data should be used for this field, or a [`when` condition](/configuration/transforms#conditional-modification).
:::tip[Per Component Override]
The `defaults` you set in [Stage Configuration](#stage-configuration) [can be overriden/added to](/configuration/transforms/#overriding-configuration) (per property) in each Hook, exactly as with the [Musicbrainz Stage](/configuration/transforms/musicbrainz#rules-and-hooks).
:::
## ENV Configuration
Your Spotify Stage can be configured using [ENV Config](/configuration?configType=env#configuration-types) by specifying client/secret and, optionally, combining different presets.
Client ID and Secret are the only **required** configuration:
* Client Id => ENV `SPOTIFY_TRANSFORM_CLIENT_ID` or ENV `SPOTIFY_CLIENT_ID` from the [Spotify Source](/configuration/sources/spotify)
* Client Secret => ENV `SPOTIFY_TRANSFORM_CLIENT_SECRET` or ENV `SPOTIFY_CLIENT_SECRET` from the [Spotify Source](/configuration/sources/spotify)
To configure [stage defaults](#stage-configuration) use ENV `SPOTIFY_TRANSFORM_PRESETS=` with a *comma-delimited* list of presets you wish to apply. If `SPOTIFY_TRANSFORM_PRESETS` is not set the stage will use defaults.
* `searchIsrc` - Restricts [search methods](#search-methods) to `isrc` only (default is `isrc` and `basic`)
* `missingFields` - Only run transform if `album`, `artist`, `title`, or `duration` are [missing in data](#should-ms-search)
* `missingIds` - Only run transform if spotify ids are [missing in data](#should-ms-search)
* `missingIsrc` - Only run transform if `isrc` is [missing in data](#should-ms-search)
* `missingArt` - Only run transform if `art` is [missing in data](#should-ms-search)
* `decomp` - [Deprioritize compilations](#deprioritize-compilations) when ranking matches
More than one preset can be applied, in which case they combine.
Example
```yaml title="docker-compose.yaml"
services:
multi-scrobbler:
# ...
environment:
# ...
- SPOTIFY_CLIENT_ID=787c921a2a2ab42320831aba0c8f2fc2
- SPOTIFY_CLIENT_SECRET=ec42e09d5ae0ee0f0816ca151008412a
// highlight-start
# only runs transform if album, artists, title, duration, or art are missing in data
# and only searches by isrc
- SPOTIFY_TRANSFORMS_PRESETS=missingArt,missingFields,searchIsrc
// highlight-end
# ...
```
Finally, use ENV `*_TRANSFORMS=spotify` on each Source/Client you wish to apply this stage to. This applies the stage in the [`preCompare` Hook](/configuration/transforms#lifecycle-hooks) with all [Rules](#rules-and-hooks) enabled.
The `*` stands for the prefix used for each Source/Client's ENV keys. Refer to the individual Source/Client Configuration sections to find this. Example:
* All [Subsonic Source](/configuration/sources/subsonic) ENVs look like `SUBSONIC_USER=myuser` etc...
* Use `SUBSONIC_TRANSFORMS=spotify`
Example Full Docker Deploy with ENV Configuration
```yaml
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
container_name: multi-scrobbler
environment:
// highlight-start
# only runs transform if art or fields are missing in data
# and only searches by isrc
- SPOTIFY_TRANSFORMS_PRESETS=missingArt,missingFields,searchIsrc
- SPOTIFY_CLIENT_ID=787c921a2a2ab42320831aba0c8f2fc2
- SPOTIFY_CLIENT_SECRET=ec42e09d5ae0ee0f0816ca151008412a
// highlight-end
- JELLYFIN_URL=192.168.0.110:8096
- JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56b
- JELLYFIN_USER=MyUser
// highlight-start
# applies spotify Stage to preCompare of Jellyfin source
- JELLYFIN_TRANSFORMS=spotify
// highlight-end
# maloja receives enhanced scrobble from Jellyfin
- MALOJA_URL=http://192.168.0.100:42010
- MALOJA_API_KEY=myApiKey
volumes:
- "./config:/config"
ports:
- "9078:9078"
restart: unless-stopped
```
To use the configured ENV stage in [Rules and Hooks](#rules-and-hooks) with File/AIO config either [omit `name` or specify it as `"name": "MSEnv"`.](/configuration/transforms#stage-names-and-default-stages)
## Matching with Spotify
:::note
**All properties found in this section are optional.**
:::
Matching your Scrobble's Play data with a result from Spotify is comprised of two steps:
* [**Searching**](#searching) Spotify using parts of your Scrobble as queries
* [**Ranking**](#ranking) matched results to select the desired match
### Searching
#### Should MS Search?
Before MS begins a search it checks if your Scrobble data already contains:
* A track title, artists, album, and duration
* All spotify IDs and an ISRC
If it already has all required data types then the entire Spotify Stage [is **skipped**.](/configuration/transforms/#flow-control) If any are missing then a search is performed.
Define which data types are required using:
* `searchWhenMissing` - A list containing any of: `album`,`title`,`artists`,`duration`,`isrc`, and `ids`
* `ids` is *any* missing spotify id (track, artists, albumArtists, and album ids)
* the default when `searchWhenMissing` is not defined is: `album`,`artists`,`title`, and `duration`
* `forceSearch` (default `false`) - Force searching even if all required data is present
#### Search Methods
Multi-scrobbler searches for a Spotify match using, in order, up to two methods. Use `searchOrder` to control which methods run and in what order:
```json5
// ...
"defaults": {
"searchOrder": ["isrc", "basic"]
}
```
ISRC (`isrc`)
If your Scrobble data contains an [ISRC](https://musicbrainz.org/doc/ISRC) then Multi-scrobbler searches Spotify's catalog using this ID.
**If the ISRC is present on more than one Spotify album/track** (which happens often IE singles, re-releases, and compilation appearances of the same recording all share an ISRC) then the results are [ranked using fuzzy matching](#ranking) against your scrobble's existing album/artist data to pick the best candidate.
:::tip[ISRC Matches Skip the Score Threshold]
If an ISRC search finds results then filtering results by [score](#score) is **skipped** since all results are confirmed matches. Ranking by fuzzy matching, as described above, still occurs.
This matters because Spotify's title/artist text for the same recording can differ substantially from your scrobble source (localized titles, "feat." credits, movie/theatrical edition suffixes, etc) while still being the correct match.
Only results from a `basic` search are filtered by the minimum `score`.
:::
Album, Artist, and Title Fields (`basic`)
Searches Spotify using any/all available text fields from your scrobble: Album, primary Artist, and Title. Results are always [ranked using fuzzy matching](#ranking) since Spotify's text search does not otherwise guarantee an accurate/confident result.
:::tip[Default Search Methods]
If `searchOrder` is undefined Multi-scrobbler defaults to using `isrc` then `basic`.
:::
:::note
If all defined search methods do not return any results (or, for a `basic` search, no results score high enough, see [Ranking](#ranking)) then the stage is marked as [**failed** (`onFailure`) for **Flow Control**](/configuration/transforms/#flow-control).
A failed Stage never modifies your play data unless you eplicitly configure it to. See [Failed and Skipped Stages](/configuration/transforms/#flow-control) for details.
:::
`market` (an [ISO 3166-1 alpha-2 country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)) can be set to bias/limit search results to what is available in a specific market:
Example
```json5
{
"type": "spotify",
"name": "MySpotify",
"data": {/* */},
"defaults": {
// ...
"market": "US"
}
}
```
`locale` (in `ISO-639-1_ISO-3166-1` format, EX `en_US`, `ja_JP`) can additionally be set to try to bias which translation of a localized catalog name (artist/album/track) Spotify returns for a match:
Example
```json5
{
"type": "spotify",
"name": "MySpotify",
"data": {/* */},
"defaults": {
// ...
"locale": "en_US"
}
}
```
:::tip[Real World Example]
Some catalog entries carry a translated name depending on locale, independent of `market`. EX the Japanese game-music artist "ATLUS Sound Team" is stored in Spotify's catalog under multiple localized names for the exact same artist ID.
* Without `locale` set, a lookup returns `アトラスサウンドチーム`;
* with `"locale": "en_US"` the same lookup returns `ATLUS Sound Team`.
`market` alone does **not** control this.
:::
:::important
`locale` is an **UNDOCUMENTED** parameter of the Spotify Web API **and may break at any time, without notice or warning**.
Its behavior may also be inconsistent. Some catalog entries only exist with one language's name at all, in which case no `locale`/`market` combination will produce an alternate translation because Spotify doesn't have one to return.
:::
### Ranking
Spotify's search results do not carry a confidence/similarity score so Multi-scrobbler fuzzy-matches every candidate against your original scrobble's title/artist(s)/album to:
* disambiguate between multiple candidates (EX an ISRC present on more than one album)
* for `basic` search results only, determine whether any candidate is a confident enough match to use at all
#### Score
Each candidate is scored between `0` and `1` based on how similar its title/artist(s)/album are to your original scrobble. Set `score` to change the minimum score a `basic`-search candidate must have to be used. Default is `0.6`. This has no effect on `isrc` matches, which are always used.
```json5
{
// ...
"score": 0.6 // matches must score 0.6 or higher to be considered
}
```
You can bias which of title/artist/album contributes most to a candidate's score with `titleWeight`, `artistWeight`, and `albumWeight` (defaults are `0.4`/`0.3`/`0.3`, respectively):
```json5
// ...
"defaults": {
"titleWeight": 0.4,
"artistWeight": 0.3,
"albumWeight": 0.3
}
```
#### Deprioritize Compilations
Spotify catalogs many recordings across multiple compilation albums (Greatest Hits, movie soundtracks that reuse a song, etc...) in addition to their "proper" studio album. If you'd prefer matches to avoid compilation albums when a better alternative exists, enable `deprioritizeCompilations`:
```json5
// ...
"defaults": {
"deprioritizeCompilations": true
}
```
This does not **exclude** compilations -- it only lowers their score relative to other candidates, so a compilation can still be used if it's the only match found.
## Best Practices
### Caching
You **should** setup [metadata caching](/configuration/transforms#caching) to reduce API calls, improve transform performance, and reduce memory usage when using this stage.
### Using Partial Match
Use [Rules](#rules-and-hooks) to apply Spotify match data selectively. This is useful if you don't want your scrobble's Artist/Title/Album modified but still want Spotify IDs or ISRC defined for [Clients](/configuration/clients) or other Stages to leverage them.
:::note
With these Rules IDs are added to the title, album, and artists your Play already has. See [how `meta` combines with other Rules](/configuration/transforms#stage-rules) to learn how IDs are matched to existing artists and what happens to existing IDs when `meta` is `false`.
:::
Example
```json5 title="subsonic.json"
[
{
"name": "MySubsonic",
"data": { /* ... */},
"options": {
"playTransform": {
"preCompare": [
{
"type": "spotify",
"name": "MySpotify",
"title": false,
"artists": false,
"album": false,
"albumArtists": false,
"meta": true
}
]
}
}
}
]
```
## Logging
If Spotify is not returning matches, or the resulting enhanced Scrobble is not what you expected, enable [**Debug Mode**](/configuration#debug-mode) to help diagnose issues with the Spotify API and Scrobble enhancement.
If you have multiple Modification Stages and need to see the diff for your Play between each Stage, enable `"log": "all"` in the individual [Modification Stage](#rules-and-hooks), as described for the [Musicbrainz Stage](/configuration/transforms/musicbrainz#logging).