Skip to main content

# Enhancing Scrobbles

Multi-scrobbler configs support the ability to enhance scrobble data in an automated fashion by matching and replacing strings in title, artists, and album at many different times in multi-scrobbler's lifecycle.

Why Would I Do This?

You may need to "clean up" data from a Source or before sending to a scrobble Client due to any number of reasons:

- ID3 tags in your music collection are dirty or have repeating garbage IE `[YourMusicSource.com] My Artist - My Title`
- A Source's service often incorrectly adds data to some field IE `My Artist - My Title (Album Version)` when the title should just be `My Title`
- An Artist you listen to often is spelled different between a Source and a Client which causes duplicate scrobbles

In any scenario where a repeating pattern can be found in the data it would be nice to be able to fix it before the data gets downstream or to help prevent duplicate scrobbling. Multi-scrobbler can help you do this.

## Journey of a Scrobble​

First, let's recap the lifecycle of a scrobble in multi-scrobbler:

Sources are the beginning of the journey for a Play (song you've listened to long enough to be scrobblable)

- A Source finds a new valid Play
- The Source compares this new Play to all the other Plays it has already seen, if the Play is unique (title/artist/album/listened datetime) then...
- The Source discovers the Play, adds it to Plays it has seen already, and broadcasts the Play should be scrobbled to all Clients

Scrobble Clients listen for discovered Plays from Sources, then...

- A Client receives a Play from a Source
- The Client compares this Play to all the other scrobbles it has already seen, if the Play is unique (title/artist/album/listened datetime) then...
- The Client scrobbles the Play downstream to the scrobble service and adds it as a Scrobble it has seen already

## Lifecycle Hooks​

You'll notice there is a pattern above that looks like this:

- Before data is compared
- Data is compared
- After data is compared

These points, during both Source and Client processes, are when you can hook into the scrobble lifecycle and modify it.

##### TLDR​

In more concrete terms this is the structure of hooks within a configuration (can be used in any Source or Client):

lastfm.json

    [
      {
        "name": "myLastFm",
        "enable": true,
        "configureAs": "source",
        "data": {
          // ...
        },
        "options": {
          "playTransform": {
            "preCompare": [/* ... */],
            "compare": [/* ... */],
            "postCompare": [/* ... */]
          }
        }
      }
    ]

### Hook​

For Sources:

- `preCompare` - modify Play data immediately when received
- `compare` - temporarily modify Play data when it is being compared to see if Play was already discovered
- `postCompare` - modify Play data before sending to scrobble Clients

For Clients:

- `preCompare` - modify Play data immediately when received
- `compare` - temporarily modify Play data when it is being compared to see if it was already scrobbled
- `postCompare` - modify Play data before scrobbling it to downstream service and adding to already seen scrobbles

Transformed Scrobbles Are Permanently Modified

Keep in mind that modifying Scrobble/Play data earlier in the lifecycle will affect that data at all times later in the lifecycle (except when using the compare hook).

For example, to modify the track so it's the same anywhere it is processed in multi-scrobbler you only need to modify it in the Source's `preCompare` hook because all later processes will receive the data with the modified track.

TLDR

To modify a scrobble coming from one of your Sources use the `preCompare` hook.

Using `compare` hook

The `compare` hook is slightly different than `preCompare` and `postCompare`. It consists of an object where you define which side(s) of the comparison should be modified. It also does not modify downstream data! Instead, the modifications are made only for use in the comparison.

lastfm.json

    [
      {
        "name": "myLastFm",
        // ...
        "options": {
          "playTransform": {
            "compare": [
              {
                "candidate": {/* ... */}, // modify the "new" Play being compared
                "existing": {/* ... */}, // modify all "existing" Play/Scrobbles the new Play is being compared against
              }
            ],
          }
        }
      }
    ]

## Modification Stage​

Each hook is made up of one or more Stages. A Stage is a self-contained, unique way of enhancing or modifying the Play data. Some examples of a Stage:

- The User Stage allows a user to define search-and-replace terms for Artist/Title/Album
- The Native Stage uses MS's built-in heuristics to extract Artists from a single Artist string
- The Musicbrainz Stage tries to match Play data with the Musicbrainz database and to standardize the Artist/Title/Album data

Each Stage in a Hook receives Play data from the previous Stage.

Within a hook, each Stage minimally consists of a `type` to identify what Stage it is along with any other data specific to that stage:

    {
      "type": "native"
      // optional, stage specific data here...
    }

### Configuring Stages​

Stages may be globally configured using AIO Config `config.json` file in the top-level `transformers` block.

Each Stage consists of:

- `type` the type of Stage
- `name` a unique name for the Stage, to be (potentially) used with hooks
- `defaults` - An object defining default configuration for this stage, when used in a Hook.
- `data` - An object containing any data required to initially configure the stage itself (Example: API URL, username, password, etc...)

Example

Your AIO Config:

config.json

    {
      // ...
      "transformers": [
        {
          "type": "native",
          "name": "MyNativeTransformer",
          "defaults": {
            // default delimiters when this Stage is used in a hook
            "delimiters": [
              "•"
            ],
            // default delimiters when this Stage is used in a hook
            "artistsParseFrom": ["artists"]
          }
        }
      ]
    }

In a Subsonic File Config:

subsonic.json

    [
      {
        "name": "MySubsonic",
        "data": { /* ... */},
        "options": {
          "playTransform": {
            "preCompare": [
              {
                "type": "native"
                "name": "MyNativeTransformer" // when "name" is not defined, uses first found "native" transformer
              }
            ]
          }
        }
      }
    ]

Multiple stages of the same type may be configured, allowing you to define several sets of default behavior.

Example

Your AIO Config:

config.json

    {
      // ...
      "transformers": [
        {
          "type": "native",
          "name": "DotTransformer",
          "defaults": {
            "delimiters": [
              "•"
            ],
            "artistsParseFrom": ["artists"]
          }
        },
        {
          "type": "native",
          "name": "TitleOnly",
          "defaults": {
            // extracts and uses *only* artists found in title string
            "artistsParseFrom": ["title"]
          }
        }
      ]
    }

In a Subsonic File Config:

subsonic.json

    [
      {
        "name": "MySubsonic",
        "data": { /* ... */},
        "options": {
          "playTransform": {
            "preCompare": [
              {
                "type": "native"
                "name": "DotTransformer"
              }
            ]
          }
        }
      }
    ]

In a VLC File Config:

vlc.json

    [
      {
        "name": "MyVLC",
        "data": { /* ... */},
        "options": {
          "playTransform": {
            "preCompare": [
              {
                "type": "native"
                "name": "TitleOnly"
              }
            ]
          }
        }
      }
    ]

#### Overriding Configuration​

The default configuration you set for your Stage may be overridden in any usage of the Stage within a Hook.

Example

Your AIO Config:

config.json

    {
      // ...
      "transformers": [
        {
          "type": "native",
          "name": "MyNativeTransformer",
          "defaults": {
            // default delimiters when this Stage is used in a hook
            "delimiters": [
              "•"
            ],
            // default delimiters when this Stage is used in a hook
            "artistsParseFrom": ["artists"]
          }
        }
      ]
    }

In a Subsonic File Config:

subsonic.json

    [
      {
        "name": "MySubsonic",
        "data": { /* ... */},
        "options": {
          "playTransform": {
            "preCompare": [
              {
                "type": "native"
                "name": "MyNativeTransformer",
                // overrides property from "defaults"
                "artistsParseFrom": ["artists", "title"]
              }
            ]
          }
        }
      }
    ]

### Rules for Play Data​

Each Stage may specify whether it should apply the resulting transformation to different parts of the Play data by specifying `title`, `artists` and/or `album` in the Stage object.

    {
        "type": "native",
        // ...
        "title": false, // will not apply any changes to Play title
        "artists": {
          "when": {/* ... */}, // will only apply changes to Play artists if "when" is satisfied
        /* ... */
        },
        "albumArtists": true, // will always apply changes to Play album artists
        "album": true, // will always apply changes to Play album
        "duration": true, // will always apply changes to Play duration (length of track)
        "meta": true, // will always apply changes to Play meta (MBIDs, spotify links, etc...)
    }

The actual value of each property may be different for each Stage. Check the docs for the Stage you want to use to see its usage of each.

Generically, though, each property may be some value or an object combining a `when` condition and that value.

If none of the properties are specified in the stage then it's assumed all transformed data should be used.

note

Specifying these Rules is not the same as configuring the Stage. Rules only determine if the result of the transformation should be used (replace) the existing Play Data.

## Stage Flow Control​

Stages may be optionally configured to continue to the next Stage or stop (end early) all subsequent Stages, based on the outcome of the currently running Stage.

These three properties can be added to the Modification Stage data, alongside Rules:

- `onSuccess` (default `continue`) - If the Stage successfully finishes processing
- `onFailure` (default `stop`) - If the Stage encounters an error while processing, or otherwise fails to achieve the transformation result
- `onSkip` (default `continue`) - If the Stage does not process the Play data due to stage-level `when` or other stage-specific skip conditions

tip

The default behaviors for Flow Control are the same as you would intuitively think Stages should run IE the next Stage you have defined runs if the current Stage does not fail.

Specifying Flow Control is only necessary if the above assumptions are not true for your scenario.

Default Flow Control Example
subsonic.json

        // ...
        "options": {
          "playTransform": {
            "preCompare": [
              {
                // if this stage is successful, native is run
                // if this stage fails, native does not run
                "type": "musicbrainz",
                "name": "MyMB"
              },
              {
                "type": "native",
                "name": "MyNative"
              }
            ]
          }
        }

Customized Flow Control Example
subsonic.json

        // ...
        "options": {
          "playTransform": {
            "preCompare": [
              {
                // if musicbrainz is successful then do NOT run native,
                // only run native if musicbrainz fails to find a match (onFailure)
                "type": "musicbrainz",
                "name": "MyMB"
                "onSuccess": "stop",
                "onFailure": "continue"
              },
              {
                "type": "native",
                "name": "MyNative"
              }
            ]
          }
        }

### Conditionally Run​

Stages within a Hook, and Rules within each Stage, support a `when` object for testing if they should be run.

The `when` object may have propertes for rule. Each property may be a string or regular expression. The value of the property is used to match the pre-transformation values from Play data.

All parts of an individual `when` clause must test true to "pass" but if any `when` clauses pass the Stage/Rule is processed.

If the `when` test does not pass then the Stage is skipped (`onSkip`).

    {
      "when":
        {
          "artist": "Elephant Gym", // Play must have an artist matching "Elephant Gym" (AND)
          "album": "Dreams" // Play object must have an album  matching "Dreams" (AND)
        }
    }

    {
      "when": [
        {
          "artist": "Elephant Gym", // Play must have an artist matching "Elephant Gym" (AND)
          "album": "Dreams" // Play object must have an album  matching "Dreams" (AND)
        },
        // OR
        {
          "title": "/(Remastered)$/", // Play title must match regular expression (AND)
          "album": "Various Artists" // Play album must match "Various Artists" (AND)
        }
      ]
    }

More succinctly:

- All parts (`artist` `album` `albumArtist` `duration` `meta` `title`) of a `when` are `AND` conditions
- All part-objects in the `when` array are `OR` conditions

Example of Stage with `when` condition

    {
      // IF the artist is Elephant Gym
      // THEN Run native stage
      "playTransform": {
        "preCompare": [
          {
            "type": "native",
            "when": [
              {
                "artist": "/Elephant Gym/"
              }
            ]
          }
        ],
      }
    }

Example of individual rule with when condition

    {
      // Always run native Stage
      //
      //     IF artist matches "Elephant Gym"
      //     THEN use result of native stage for "artists" of Play data
      "playTransform": {
        "preCompare": {
          "type": "native",
          "artists":
            {
              "when": [
                {
                  "artist": "/Elephant Gym/"
                }
              ]
            },
        }
      }
    }

## Logging​

MS can log a diff of Stage transformations if/when they occur. In the `playTransform` object of a Source/Client config use `log`:

- `"log": true` => Output original play + final transformed output of last Stage in the array
  - This can also be enabled by using `DEBUG_MODE=true`
- `"log": "all"` => Output original play + final transformed output of each Stage in the array

    {
      "name": "myThing",
      "data": {/*...*/},
      "options": {
        "playTransform": {
          "preCompare": {/*...*/},
          "log": true
        }
      }
    }

Example

The output shows the diff between the previous stage (or original Play) and the current stage. In docker logs this is highlighted with diff syntax:

    [2025-12-17 08:53:10.467 -0500] DEBUG  : [App] [Sources] [Lastfm - mylfm] [Play Transform] [preCompare] [VLDJJo] Transform Diff
    - Original
    + musicbrainz-MyMB

      Title        : Demons Theme Part II (original 12" mix)
      Artists      : LTJ Bukem
      Album Artists: (None)
    - Album        : Producer 05: Rarities (Original 12" Version)
    + Album        : Producer 05: Rarities (original 12" version)
      Meta         :
      * brainz-album: 36759a8a-d3df-47da-a236-60f84fdc0cab
    + * brainz-artist: 28c1b7b7-355a-48b1-b2c4-75b8eb8080ef
    + * brainz-track: a0d51240-7ef1-4676-882e-be9f354075cb
    + * brainz-releaseGroup: e0cce74f-abcc-343b-9390-1673e4d57ce7

## Best Practices​

### Caching​

MS uses caching to reduce the number of API calls needed for stages like Musicbrainz and to speed up all transforms by caching steps and results. However, the default caching strategy uses a small cache size and very short TTLs because it is in-memory.

If you are using any Transform stages you should configure secondary caching to increase the cache size and lifetime of cached items. This will also reduce memory usage in MS.

## Examples​

See Examples sections in specific Stage docs (also in the sidebar):

- User Stage
- Native Stage
- Musicbrainz Stage