diff --git a/config/applemusic.json.example b/config/applemusic.json.example
index 2ed1a0f7..33b21cc4 100644
--- a/config/applemusic.json.example
+++ b/config/applemusic.json.example
@@ -22,7 +22,8 @@
},
"options": {
"logDiff": true,
- "recoverUnchangedTopHistory": true
+ "recoverUnchangedTopHistory": true,
+ "normalizeAlbum": true
}
}
]
diff --git a/docsite/docs/configuration/sources/applemusic.mdx b/docsite/docs/configuration/sources/applemusic.mdx
index 165af538..e3758c13 100644
--- a/docsite/docs/configuration/sources/applemusic.mdx
+++ b/docsite/docs/configuration/sources/applemusic.mdx
@@ -161,21 +161,102 @@ If you ever experience false positives (tracks being scrobbled that you didn't a
#### Album name normalization
-The Apple Music API appends ` - EP` or ` - Single` to the album name for EPs and singles. Multi-Scrobbler strips this out so that the album name matches other sources.
+The Apple Music API appends ` - EP` or ` - Single` to the album name for EPs and singles. These suffixes are *not* part of the official names of the album and can cause issues when trying to scrobble or match metadata.
+
+By default, Multi-Scrobbler strips these suffixes out so only the official name is used.
+
+If you do not want this behavior it can be disabled with ENV `APPLEMUSIC_NORMALIZE_ALBUM=false` or File/AIO option `"normalizeAlbum": false`.
+
+
+
+If you still want this stripping functionality but with more control over how it is applied you can use a [User Stage](/configuration/transforms/user).
+
+Here is an example of creating a User Stage to only strip suffixes for a specific scrobble client:
+
+
+
+Example
+
+```json5 title="config.json"
+{
+ // ...
+ "transformers": [
+ {
+ "type": "user",
+ "name": "AppleAlbumStip",
+ "defaults": {
+ "album": [
+ "/ - (EP|Single)$/i"
+ ]
+ }
+ }
+ ]
+}
+```
+
+```json5 title="applemusic.json"
+{
+ {
+ "id": "myAppleMusic",
+ "name": "My Apple Music",
+ "data": {
+ // ...
+ },
+ // highlight-start
+ "options": {
+ "normalizeAlbum": false
+ }
+ // highlight-end
+ }
+}
+```
+
+```json5 title="lastfm.json"
+[
+ {
+ "name": "Foxx LFM Client",
+ "id": "myLastFmClient",
+ "enable": true,
+ "configureAs": "client",
+ "data": {
+ "apiKey": "a89cba1569901a0671d5a9875fed4be1",
+ "secret": "ec42e09d5ae0ee0f0816ca151008412a",
+ "redirectUri": "http://localhost:9078/lastfm/callback"
+ },
+ // highlight-start
+ "options": {
+ "playTransform": {
+ "preCompare": [
+ {
+ "type": "user",
+ "name": "AppleAlbumStip",
+ }
+ ]
+ }
+ }
+ // highlight-end
+ }
+]
+```
+
+
+
+
## Configuration Reference
- | Environment Variable | Required? | Default | Description |
- | ----------------------------- | --------- | ------- | ---------------------------------------------------- |
- | `APPLEMUSIC_ID` | Yes | | A unique ID for this source. |
- | `APPLEMUSIC_MEDIA_USER_TOKEN` | Yes | | The media-user-token extracted from the browser. |
- | `APPLEMUSIC_KEY_ID` | No | | Key ID from your MusicKit key. |
- | `APPLEMUSIC_TEAM_ID` | No | | Team ID from your Apple Developer account. |
- | `APPLEMUSIC_KEY_P8` | No | | The contents of your MusicKit `.p8` private key file.|
- | `APPLEMUSIC_TOKEN` | No | | The authentication JWT extracted from the browser. |
- | `APPLEMUSIC_INTERVAL` | No | `60` | Polling interval in seconds. |
- | `APPLEMUSIC_ORIGIN_HEADER` | No | | Origin header for API requests, e.g. `https://music.apple.com`. Required when using a browser token. |
- | `APPLEMUSIC_RECOVER_UNCHANGED_TOP_HISTORY` | No | `true` | Apple Music deduplicates its history by bumping re-played tracks to the top. If you listen to Song A → Song B → Song A, the history changes from `[A]` to `[A, B]` (bumping A). MS detects this "top-rebound" to recover the intermediate play (B) and the re-listen (A). Disable this only if you see incorrect duplicate scrobbles. |
- | `APPLEMUSIC_NAME` | No | | A vanity name different than the ID. |
+ | Environment Variable | Required? | Default | Description |
+ | ------------------------------------------ | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+ | `APPLEMUSIC_ID` | Yes | | A unique ID for this source. |
+ | `APPLEMUSIC_MEDIA_USER_TOKEN` | Yes | | The media-user-token extracted from the browser. |
+ | `APPLEMUSIC_KEY_ID` | No | | Key ID from your MusicKit key. |
+ | `APPLEMUSIC_TEAM_ID` | No | | Team ID from your Apple Developer account. |
+ | `APPLEMUSIC_KEY_P8` | No | | The contents of your MusicKit `.p8` private key file. |
+ | `APPLEMUSIC_TOKEN` | No | | The authentication JWT extracted from the browser. |
+ | `APPLEMUSIC_INTERVAL` | No | `60` | Polling interval in seconds. |
+ | `APPLEMUSIC_ORIGIN_HEADER` | No | | Origin header for API requests, e.g. `https://music.apple.com`. Required when using a browser token. |
+ | `APPLEMUSIC_RECOVER_UNCHANGED_TOP_HISTORY` | No | `true` | Apple Music deduplicates its history by bumping re-played tracks to the top. If you listen to Song A → Song B → Song A, the history changes from `[A]` to `[A, B]` (bumping A). MS detects this "top-rebound" to recover the intermediate play (B) and the re-listen (A). Disable this only if you see incorrect duplicate scrobbles. |
+ | `APPLEMUSIC_NORMALIZE_ALBUM` | No | `true` | Strips extraneous `- EP` and `- Single` suffixes from album names |
+ | `APPLEMUSIC_NAME` | No | | A vanity name different than the ID. |
\ No newline at end of file
diff --git a/src/backend/common/infrastructure/config/source/applemusic.ts b/src/backend/common/infrastructure/config/source/applemusic.ts
index 29643e30..b5acba9e 100644
--- a/src/backend/common/infrastructure/config/source/applemusic.ts
+++ b/src/backend/common/infrastructure/config/source/applemusic.ts
@@ -37,6 +37,19 @@ export interface AppleMusicSourceConfig extends CommonSourceConfig {
* @examples [true, false]
*/
recoverUnchangedTopHistory?: boolean
+ /**
+ * Removes extraneous suffixes from album data
+ *
+ * Apple Music add ' - EP' and ' - Single' to album names for EP's and singles, respectively.
+ * These suffixes are not part of the official names for the album and can cause issues in scrobble services
+ * or when matching metadata (musicbrainz).
+ *
+ * When this option is true (default), Multi-scrobbler automatically removes these suffixes.
+ *
+ * @default true
+ * @examples [true, false]
+ */
+ normalizeAlbum?: boolean
}
}
diff --git a/src/backend/sources/AppleMusicSource.ts b/src/backend/sources/AppleMusicSource.ts
index 7db38ebb..36099ce3 100644
--- a/src/backend/sources/AppleMusicSource.ts
+++ b/src/backend/sources/AppleMusicSource.ts
@@ -115,13 +115,15 @@ export default class AppleMusicSource extends AbstractSource {
return `Apple Music API is reachable (status ${status})`;
}
- static formatPlayObj(track: Song, options: {newFromSource?: boolean} = {}): PlayObject {
- const {newFromSource = false} = options;
+ static formatPlayObj(track: Song, options: {newFromSource?: boolean, normalizeAlbum?: boolean} = {}): PlayObject {
+ const {newFromSource = false, normalizeAlbum = true} = options;
- // Apple Music appends " - EP" or " - Single" to the album name for EPs and singles
- // We strip this out so it matches other sources
let albumName = track.albumName
- albumName = albumName.replace(/ - (EP|Single)$/i, '');
+ if(normalizeAlbum) {
+ // Apple Music appends " - EP" or " - Single" to the album name for EPs and singles
+ // We strip this out so it matches other sources
+ albumName = albumName.replace(/ - (EP|Single)$/i, '');
+ }
const play: PlayObjectMinimal = {
data: {
@@ -164,7 +166,7 @@ export default class AppleMusicSource extends AbstractSource {
if (!result.data) {
return [];
}
- return (result.data as Song[]).map(track => AppleMusicSource.formatPlayObj(track));
+ return (result.data as Song[]).map(track => AppleMusicSource.formatPlayObj(track, {normalizeAlbum: this.config?.options?.normalizeAlbum}));
}
getIncomingHistoryConsistencyResult = (plays: PlayObject[]): HistoryConsistencyResult => {
diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts
index 7cc56704..fbe32458 100644
--- a/src/backend/sources/ScrobbleSources.ts
+++ b/src/backend/sources/ScrobbleSources.ts
@@ -34,7 +34,7 @@ import type {YandexMusicBridgeData, YandexMusicBridgeSourceConfig} from "../comm
import type {SonosData, SonosSourceConfig} from "../common/infrastructure/config/source/sonos.ts";
import type {AppleMusicSourceConfig} from "../common/infrastructure/config/source/applemusic.ts";
import type { WildcardEmitter } from "../common/WildcardEmitter.ts";
-import { nonEmptyObj, parseBool } from "../utils.ts";
+import { nonEmptyObj, parseBool, parseBoolStrict } from "../utils.ts";
import { removeUndefinedKeys } from '../../core/DataUtils.ts';
import { getCommonComponentEnvConfig, readJson } from '../utils/DataUtils.ts';
import { validateJson } from "../utils/ValidationUtils.ts";
@@ -848,10 +848,10 @@ export default class ScrobbleSources {
key,
origin: nonEmptyStringOrDefault(process.env.APPLEMUSIC_ORIGIN_HEADER, undefined),
}, false);
- const recoverEnv = process.env.APPLEMUSIC_RECOVER_UNCHANGED_TOP_HISTORY;
- const recoverUnchangedTopHistory = recoverEnv !== undefined && recoverEnv.trim() !== ''
- ? parseBool(recoverEnv)
- : undefined;
+ const recoverEnv = nonEmptyStringOrDefault(process.env.APPLEMUSIC_RECOVER_UNCHANGED_TOP_HISTORY);
+ const recoverUnchangedTopHistory = recoverEnv !== undefined ? parseBoolStrict(recoverEnv) : undefined;
+ const albumNormalizeEnv = nonEmptyStringOrDefault(process.env.APPLEMUSIC_NORMALIZE_ALBUM);
+ const normalizeAlbumName = albumNormalizeEnv !== undefined ? parseBoolStrict(albumNormalizeEnv) : undefined;
const p = getCommonComponentEnvConfig('APPLEMUSIC');
if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
@@ -864,6 +864,7 @@ export default class ScrobbleSources {
...p,
options: transformPresetEnv('APPLEMUSIC', {
recoverUnchangedTopHistory,
+ normalizeAlbumName
} as AppleMusicSourceConfig['options'])
});
}
diff --git a/src/backend/tests/applemusic/applemusic.test.ts b/src/backend/tests/applemusic/applemusic.test.ts
index 4679f065..86ab67d2 100644
--- a/src/backend/tests/applemusic/applemusic.test.ts
+++ b/src/backend/tests/applemusic/applemusic.test.ts
@@ -287,11 +287,13 @@ describe('Apple Music - Format Play Object', function () {
durationInMillis: 200000
} as any;
+ const formatOptions = {normalizeAlbum: true};
+
// Run them through the formatter
- const playEP = AppleMusicSource.formatPlayObj(trackEP);
- const playSingle = AppleMusicSource.formatPlayObj(trackSingle);
- const playNormal = AppleMusicSource.formatPlayObj(trackNormal);
- const playLowercase = AppleMusicSource.formatPlayObj(trackLowercase);
+ const playEP = AppleMusicSource.formatPlayObj(trackEP, formatOptions);
+ const playSingle = AppleMusicSource.formatPlayObj(trackSingle, formatOptions);
+ const playNormal = AppleMusicSource.formatPlayObj(trackNormal, formatOptions);
+ const playLowercase = AppleMusicSource.formatPlayObj(trackLowercase, formatOptions);
// Assert the suffixes are removed
expect(playEP.data.album).to.equal('ALBUM A');