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 beMy 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):
[
{
"name": "myLastFm",
"enable": true,
"configureAs": "source",
"data": {
// ...
},
"options": {
"playTransform": {
"preCompare": [/* ... */],
"compare": [/* ... */],
"postCompare": [/* ... */]
}
}
}
]
Hook
For Sources:
preCompare- modify Play data immediately when receivedcompare- temporarily modify Play data when it is being compared to see if Play was already discoveredpostCompare- modify Play data before sending to scrobble Clients
For Clients:
preCompare- modify Play data immediately when receivedcompare- temporarily modify Play data when it is being compared to see if it was already scrobbledpostCompare- modify Play data before scrobbling it to downstream service and adding to already seen scrobbles
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.
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.
[
{
"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:
typethe type of Stagenamea unique name for the Stage, to be (potentially) used with hooksdefaults- 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:
{
// ...
"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:
[
{
"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:
{
// ...
"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:
[
{
"name": "MySubsonic",
"data": { /* ... */},
"options": {
"playTransform": {
"preCompare": [
{
"type": "native"
"name": "DotTransformer"
}
]
}
}
}
]
In a VLC File Config:
[
{
"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:
{
// ...
"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:
[
{
"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.
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(defaultcontinue) - If the Stage successfully finishes processingonFailure(defaultstop) - If the Stage encounters an error while processing, or otherwise fails to achieve the transformation resultonSkip(defaultcontinue) - If the Stage does not process the Play data due to stage-levelwhenor other stage-specific skip conditions
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
// ...
"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
// ...
"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 (
artistalbumalbumArtistdurationmetatitle) of awhenareANDconditions - All part-objects in the
whenarray areORconditions
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
- This can also be enabled by using
"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):