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

Lifecyle 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
tip

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"
// 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
  • "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

Examples

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