---
sidebar_position: 4
title: Scrobble Modification
toc_max_heading_level: 4
---

Multi-scrobbler configs support the ability to modify 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?

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.

## Overview

### 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.

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.

:::

### Modification Parts


Each [**hook**](#hook) (`preCompare` etc...) is an object that specifies what part of the **Play** to modify:

```json5
{
    "title": [/* ... */],
    "artists": [/* ... */],
    "album": [/* ... */]
}
```

##### Expression

and then a **list** what pattern/replacements (expressions) to use for the modification by using either simple strings or `search-replace` objects:

```json5
[
  "badTerm", // remove all instances of 'badTerm'
  {
    "search": "anotherBadTerm", // and also match all instances of 'anotherBadTerm'
    "replace": "goodTerm" // replace with the string 'goodTerm'
  }
]
```

Putting it all together:

```json5 title="lastfm.json"
[
  {
    "name": "myLastFm",
    "enable": true,
    "configureAs": "source",
    "data": {
      // ...
    },
    "options": {
      "playTransform": {
        "preCompare": {
          "title": [
            "badTerm",
            {
              "search": "badTerm",
              "replace": "goodTerm"
            }
          ]
        },
      }
    }
  }
]
```

:::note

If the value of the field (title, an artist, album) is an empty string after transforming then the field is **removed.**

:::

:::tip

Modifications can also be applied to **all Sources** or **all Clients** when using the [AIO Config](./configuration.mdx?configType=aio#configuration-types) `config.json` by setting `playTransform` in `sourceDefaults` or `clientDefaults`:

<details>

    <summary>Example</summary>
```json5 title="config.json"
{
  "sourceDefaults": { // will apply playTransform to all sources
    "playTransform": {
      "preCompare": {
        "title": [
          "(Album Version)"
        ]
      }
    }
  },
  "sources": [/* ... */],
  "clients": [/* ... */]
}
```
</details>

:::

#### 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
        },
      }
    }
  }
]
```

#### Regular Expressions

In addition to plain strings [expressions](#expression) that are matched and removed you can also use Regular Expressions. Write your regex like you normally would, but as a string, and it'll automatically be parsed:

```json5
[
  "/^\(\w+.com)/i", // matches any string that starts with '(YourMusic.com)' and removes it
  {
    "search": "/^\(\w+.com)/i", // matches any string that starts with '(YourMusic.com)'
    "replace": "[MySite.com]" // replace with the string '[MySite.com]'
  }
]
```

The `replace` property uses javascript's [`replace()` function and so can use any special string characters.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement)

### Conditional Modification

#### "When" Condition

Top-level hooks **and** individual rules also support a `when` key for testing **if they should be run.**

The `when` key is similar to a normal [modification](#modification-parts) except:

* the keys accept a single string instead of an array
* the `when` key data is an array instead of a single object

All parts of an individual `when` clause must test true to "pass" but if **any** `when` clauses pass the hook/rule is processed. Example `when` data:

```json5
{
  "when": [
    {
      "artist": "Elephant Gym", // both of these must match the Play object (AND)
      "album": "Dreams" // both of these must match the Play object (AND)
    },
    // OR
    {
      "title": "/(Remastered)$/", // both of these must match the Play object (AND)
      "album": "Various Artists" // both of these must match the Play object (AND)
    }
  ]
}
```

More succinctly:

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

<details>

<summary>Example of top-level hook with when condition</summary>

```json5
{
  // IF the artist is Elephant Gym 
  // THEN Run preCompare hook ELSE skip this hook
  //
  //    Run search-replace on album
  //    Run regex title remove
  "sourceDefaults": {
    "playTransform": {
      "preCompare": {
        "when": [
          {
            "artist": "/Elephant Gym/"
          }
        ],
        "album": [
          {
            "search": "Dreams",
            "replace": "夢境"
          }
        ],
        "title": ["/\s\-\s滾石40\s滾石撞樂隊\s40團拚經典(.+)$/i"]
      },
    }
  }
}
```

</details>

<details>

<summary>Example of individual rule with when condition</summary>

```json5
{
  // Always run preCompare
  //
  //   On search-replace in title...
  //     IF artist matches "Elephant Gym"
  //     THEN Run regex search-replace ELSE skip this rule
  //
  //   Run live|remastered regex remove
  "sourceDefaults": {
    "playTransform": {
      "preCompare": {
        "title": [
          {
            "search": "/\\s\\-\\s滾石40\\s滾石撞樂隊\\s40團拚經典(.+)$/i",
            "replace": "",
            "when": [
              {
                "artist": "/Elephant Gym/"
              }
            ]
          },
          "/(\\s\\-\\s|\\s)(feat\\.(.+)|live|remastered(.+))$/i"
        ],
      }
    }
  }
}
```

</details>

#### Top-level Hook array

Top-level hooks can also be an array of hooks. This makes creating multiple scenarios for top-level `when`-gated hooks easier. All hooks in the array will be run (assuming their `when`'s pass, if they exist) and their **input will be the Play object output of the previous hook in the array.**

<details>

<summary>Example</summary>

```json5
{
  "sourceDefaults": {
    "playTransform": {
      "preCompare": [
        // first lifecycle hook of preCompare to run
        {
          "title": [
            {
              "search": "something",
              "replace": "else unique"
            }
          ]
        },
        // second lifecycle hook of preCompare to run
        {
          "title": [
            {
              "search": "else unique",
              "replace": "very demure"
            }
          ]
        },
      ]
    }
  }
}
```

</details>

### Logging

MS can log the output of hook 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 hook in the array
* `"log": "all"` => Output original play + final transformed output of **each** hook in the array

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

## Examples

### Remove phrase from Title in all new Plays

Removes the phrase `(Album Version)` from the Title of a Play


<details>

    <summary>Example</summary>
```json5 title="config.json"
{
  "sourceDefaults": {
    "playTransform": {
      "preCompare": {
        "title": [
          "(Album Version)"
        ]
      }
    }
  }
}

```
</details>

### Remove all parenthesized content from the end of a title

<details>

    <summary>Example</summary>
```json5 title="lastfm.json"
[
  {
    "name": "myLastFm",
    // ...
    "options": {
      "playTransform": {
        "compare": {
          "candidate": {
          "title": [
              "/(\(.+\))\s*$/"
            ]
          },
          "existing": {
          "title": [
              "/(\(.+\))\s*$/"
            ]
          },
        },
      }
    }
  }
]
```
</details>

### Rename misspelled artist in all new Plays

<details>

    <summary>Example</summary>
```json5 title="config.json"
{
  "sourceDefaults": {
    "playTransform": {
      "preCompare": {
        "artists": [
          {
            "search": "Boz Skaggs",
            "replace": "Boz Scaggs"
          }
        ]
      }
    }
  }
}
```
</details>

### Remove "Various Artists" albums in all new Plays

<details>

    <summary>Example</summary>
```json5 title="config.json"
{
  "sourceDefaults": {
    "playTransform": {
      "preCompare": {
        "album": [
          {
            "search": "Various Artists",
            "replace": ""
          }
        ]
      }
    }
  }
}
```
</details>

### Extract primary Artist from delimited, multi-Artist string

<details>

  When the Artist string is actually a multi-artist, delimited string, this search-and-replace will replace the string with just the first artist found.

  Ex

  ```
  My Artist One / My Artist Two / Another Guy
  My Artist One
  ```

  Artists are delimited with a spaced forward slash (`/`) in the regex below. Replace the contents of the `delim` capture group with the delimiter for your use case. Some more common scenarios:

  * `(?<delim>\\/)` No spaces between slash IE `My Artist One/My Artist Two/Another Guy`
  * `(?<delim>\\s*\\\\\s*)` Backslash instead of forward slash IE `My Artist One \ My Artist Two \ Another Guy`
  * `(?<delim>,)` Comma IE `My Artist One, My Artist Two, Another Guy`

  <details>

      <summary>Example</summary>
  ```json5 title="config.json"
  {
    "sourceDefaults": {
      "playTransform": {
        "preCompare": {
          "artists": [
            {
              "search": "(.*?)(?<delim>\\s*\\/\\s*)(.*$)",
              "replace": "$1"
            }
          ]
        }
      }
    }
  }
  ```
  </details>

</details>