--- 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).