--- sidebar_position: 4 title: Enhancing Scrobbles toc_max_heading_level: 5 --- 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**): ```json5 title="lastfm.json" {10-14} [ { "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. :::
Using `compare` hook The `compare` [hook](#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. ```json5 title="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 {#stage} Each [**hook**](#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](/configuration/transforms/user) Stage allows a user to define search-and-replace terms for Artist/Title/Album * The [Native](/configuration/transforms/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: ```json5 { "type": "native" // optional, stage specific data here... } ``` ### Configuring Stages {#configuring-stages} Stages may be globally configured using [AIO Config](/configuration?configType=aio#configuration-types) `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](/configuration?configType=aio#configuration-types): ```json5 title="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](/configuration/sources/subsonic) [File Config](/configuration?configType=file#configuration-types): ```json5 title="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](/configuration?configType=aio#configuration-types): ```json5 title="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](/configuration/sources/subsonic) [File Config](/configuration?configType=file#configuration-types): ```json5 title="subsonic.json" [ { "name": "MySubsonic", "data": { /* ... */}, "options": { "playTransform": { "preCompare": [ { "type": "native" "name": "DotTransformer" } ] } } } ] ``` In a [VLC](/configuration/sources/vlc) [File Config](/configuration?configType=file#configuration-types): ```json5 title="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](/configuration?configType=aio#configuration-types): ```json5 title="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](/configuration/sources/subsonic) [File Config](/configuration?configType=file#configuration-types): ```json5 title="subsonic.json" [ { "name": "MySubsonic", "data": { /* ... */}, "options": { "playTransform": { "preCompare": [ { "type": "native" "name": "MyNativeTransformer", // overrides property from "defaults" "artistsParseFrom": ["artists", "title"] } ] } } } ] ```
### Rules for Play Data {#stage-rules} Each [Stage](#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. ```json5 { "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 /* ... */ }, "album": true // will always apply changes to Play album } ``` 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 `title`, `artists`, and `album`. Generically, though, each property may be some value **or** an object combining a [`when` condition](#conditional-modification) 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](#configuring-stages). Rules only determine if the *result* of the transformation should be used (replace) the existing Play Data. ::: ## Conditional Modification [Stages](#stage) within a [Hook](#hook), and [Rules](#stage-rules) within each Stage, support a `when` object for testing **if they should be run.** The `when` object may have propertes for `artist`, `title` and/or `album`. 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. ```json5 { "when": { "artist": "Elephant Gym", // Play must have an artist matching "Elephant Gym" (AND) "album": "Dreams" // Play object must have an album matching "Dreams" (AND) } } ``` ```json5 { "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` `title`) of a `when` are `AND` conditions * All part-objects in the `when` array are `OR` conditions
Example of Stage with `when` condition ```json5 { // 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 ```json5 { // 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 the output 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 ```json5 { "name": "myThing", "data": {/*...*/}, "options": { "playTransform": { "preCompare": {/*...*/}, "log": true } } } ```