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
Section titled “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
Section titled “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.
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": [/* ... */] } } }]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
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.
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": [ {modify the "new" Play being compared "candidate": {/* ... */},modify all "existing" Play/Scrobbles the new Play is being compared against "existing": {/* ... */}, } ], } } }]Modification Stage
Section titled “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
- The Rocksky Stage tries to match Play data with the Rocksky metadata API to enrich meta IDs (like MBID, ISRC), duration, and artwork
- The Spotify Stage tries to match Play data with the Spotify catalog (prioritizing ISRC lookups) to standardize the Artist/Title/Album data
- The Cover Art Archive Stage adds album art to your scrobble based on Musicbrainz Relase/Release-Group MBID
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
Section titled “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:
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": { } } ]}- 1.extracts and uses *only* artists found in title string
[ { "name": "MySubsonic", "data": { /* ... */}, "options": { "playTransform": { "preCompare": [ { "type": "native", "name": "DotTransformer" } ] } } }][ { "name": "MyVLC", "data": { /* ... */}, "options": { "playTransform": { "preCompare": [ { "type": "native", "name": "TitleOnly" } ] } } }]Stage Names and Default Stages
Section titled “Stage Names and Default Stages”When specifying a Stage within a Hook (like in preCompare), the name for a Stage is potentially optional.
If Stages exists for the type you want to use in the hook and you do not specify a name…
Example
[ { "name": "MyVLC", "data": { /* ... */}, "options": { "playTransform": { "preCompare": [ {no "name" is specified "type": "rocksky" } ] } } }]Multi-Scrobbler looks for configured Stages in this order:
- The first non-default, non-ENV configured Stage IE a Stage you configured with an explicit
name - An ENV configured stage, like Rocksky’s ENV Configuration
- ENV configured stages are always named
MSEnv
- ENV configured stages are always named
- The default, built-in Stage from Multi-Scrobbler. This is always named
MSDefault
All of the Modification Stages have an MSDefault configured Stage except for Spotify.
Overriding Configuration
Section titled “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": { "•" ], } } ]}- 1.default delimiters when this Stage is used in a hook
- 2.default artistsParseFrom when this Stage is used in a hook
[ { "name": "MySubsonic", "data": { /* ... */}, "options": { "playTransform": { "preCompare": [ { "type": "native", "name": "MyNativeTransformer", } ] } } }]- 1.overrides
"artistsParseFrom"from"defaults"
Rules for Play Data
Section titled “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", // ... "artists": { /* ... */ },}- 1.will not apply any changes to Play title
- 2.will only apply changes to Play artists if
"when"is satisfied - 3.will always apply changes to Play album artists
- 4.will always apply changes to Play album
- 5.will always apply changes to Play duration (length of track)
- 6.will always apply changes to Play meta (MBIDs, spotify links, etc...)
- 7.will always apply art to Play (if stage provides art)
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.
How meta and art Combine With Other Rules
IDs (MBIDs, Spotify IDs…) and art belong to a specific part of your Play: the title, the album, or an individual artist. So what meta and art do depends on whether the Stage is also allowed to change that part.
Rule for a part (EX title) |
meta |
What happens to that part |
|---|---|---|
true |
true |
Name is replaced and IDs from the Stage are added. IDs the part already has from other services/stages are kept. |
true |
false |
Name is replaced and no IDs are added. If the name actually changed, the IDs the part already had are removed. |
false |
true |
Name is not changed. IDs from the Stage are added to the existing part. |
false |
false |
Nothing is changed. |
Stage Flow Control
Section titled “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
Conditionally Run
Section titled “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": { }}- 1.Play must have an artist matching "Elephant Gym" (AND)
- 2.Play object must have an album matching "Dreams" (AND)
Checking for Art
To test if your Play/scrobble has no art associated with the data use an empty string for art:
{ "when": [ {run the stage when play has no artwork (album, track, or artist) "art": "", }, ]}Otherwise, using a string/regex pattern for art will test if any art url includes that pattern.
EX: only run stage if art does not come from deezer, use deezer’s cdn in a not regex statement:
{ "when": [ {run the stage when no art url comes from deezer (using this regex) "art": "/^((?!(dzcdn)).)*$/", }, ]}More succinctly:
- All parts (
artistalbumalbumArtistdurationmetatitleart) of awhenareANDconditions - All part-objects in the
whenarray areORconditions
{ "playTransform": { "preCompare": [ { "type": "native", { } ] } ], }}Logging
Section titled “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-1673e4d57ce7Best Practices
Section titled “Best Practices”Caching
Section titled “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
Section titled “Examples”See Examples sections in specific Stage docs (also in the sidebar):
These examples use all of the features described above. They walk you through the entire process of building configuration to transform your scrobble data, from start to end, in one usecase.
Walkthrough - Remove Something from Title
Section titled “Walkthrough - Remove Something from Title”You want to use the User Stage to remove instances of (Album Version) in track titles from Navidrome.
{}- 1.Set the Stage type
{ "type": "user",
]}- 1.Define which Rule this Stage will apply. This equates to the part of the scrobble data you want to modify
{ "type": "user", "title": [ "search": "(Album Version)", } ]}- 1.Add the behavior for the chosen Stage to that Rule
- 2.Replacing with an empty string ("") removes the value found in
search
{ { "type": "user", "title": [ { "search": "(Album Version)", "replace": "" } ] } ]}- 1.Run this behavior when scrobble data is first seen
{ "sources": [ { "type": "subsonic", "id": "myNavidrome", "data": {}, "options": { "preCompare": [ { "type": "user", "title": [ { "search": "(Album Version)", "replace": "" } ] } ] } } } ]}- 1.Defined on the Navidrome source scrobble data comes from
Walkthrough - Using Musicbrainz on Many Sources/Clients
Section titled “Walkthrough - Using Musicbrainz on Many Sources/Clients”You want to use the Musicbrainz Stage, with specific behavior, to correct/enrich your scrobble data. It should do this for both Navidrome and Jellyfin.
{ { } ]}- 1.Add to
transformersin AIO config. This is where Stages with reusable configuration are defined - 2.Set the Musicbrainz Stage
- 3.Give it a unique name so we can reference it in Client/Sources later
{ "transformers": [ { "type": "musicbrainz", "name": "myMB" //"artists": true, //"album": true, //... } ]}- 1.You want all Rules from this Stage to apply. All rules default to
truewhen not defined, so defining nothing here is the same as "apply everything".
{ "transformers": [ { "type": "musicbrainz", "name": "myMB", } ]}- 1.Unlike the User Stage Walkthrough which sets behavior directly in the Rule, we want the behavior we set to apply to *all* Sources/Clients that use it, so we set behavior in
defaults
{ "transformers": [ { "type": "musicbrainz", "name": "myMB", "releaseCountryPriority": ["US"], } ]}- 1.Add our stage behvior to
defaults - 2.Prioritize releases and country for releases
- 3.Now our stage is ready to be used in Sources/Clients
{ "transformers": [...], "sources": [ { "type": "subsonic", "id": "myNavidrome", "data": {}, "options": { { "type": "musicbrainz", } ] } } }, { "type": "jellyfin", "id": "myJellyfin", "data": {}, "options": { "preCompare": [ { "type": "musicbrainz", "name": "myMB", } ] } } }, ], "clients": [...]}- 1.Defined on the Navidrome source scrobble data comes from
- 2.Run this behavior [when scrobble data is first seen](#lifecycle-hooks
- 3.Referencing Stage by name means "use the
defaultsin the Stage intransformerswith thisname" - 4.Defined on the jellyfin source scrobble data comes from