···1212A dockerized app that monitors your music listening activity from *everywhere* and scrobbles it *anywhere*.
13131414* Supports scrobbling from many [**Sources**](https://docs.multi-scrobbler.app/configuration/sources)
1515+ * [Apple Music](https://docs.multi-scrobbler.app/configuration/sources/applemusic/?configType=file)
1516 * [Azuracast](https://docs.multi-scrobbler.app/configuration/sources/azuracast)
1617 * [Deezer](https://docs.multi-scrobbler.app/configuration/sources/deezer)
1718 * [Google Cast (Chromecast)](https://docs.multi-scrobbler.app/configuration/sources/google-cast)
+29
config/applemusic.json.example
···11+[
22+ {
33+ "id": "myAppleMusic",
44+ "name": "My Apple Music",
55+ "enable": true,
66+ "clients": [],
77+ "data": {
88+ // either key or token needs to be provided
99+ "key": {
1010+ "id": "2HPSNJZ88N",
1111+ "teamId": "SN6YASW8G4",
1212+ "p8": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEG...-----END PRIVATE KEY-----"
1313+ },
1414+ "token": "your-jwt-token-here",
1515+ "mediaUserToken": "your-media-user-token-here",
1616+1717+ // optional: Origin header for API requests (required when using a browser token)
1818+ "origin": "https://music.apple.com",
1919+2020+ // optional: polling interval in seconds (default 60)
2121+ "interval": 60
2222+ },
2323+ "options": {
2424+ "logDiff": true,
2525+ "recoverUnchangedTopHistory": true,
2626+ "normalizeAlbum": true
2727+ }
2828+ }
2929+]
···11+---
22+title: Apple Music
33+toc_min_heading_level: 2
44+toc_max_heading_level: 5
55+---
66+77+import Tabs from '@theme/Tabs';
88+import TabItem from '@theme/TabItem';
99+import CodeBlock from '@theme/CodeBlock';
1010+import AppleMusicConfig from '!!raw-loader!@site/../config/applemusic.json.example';
1111+1212+This Source **monitors your Apple Music listening history** via the official Apple Music API and scrobbles new activity to your configured [Clients](/configuration/clients).
1313+1414+Because of how the Apple Music API works, Multi-Scrobbler (MS) requires two different tokens to function: a **Media User Token** (identifies your personal account) and an **Authentication Token** (authorizes API access).
1515+1616+### 1. Getting a Media User Token
1717+1818+The `mediaUserToken` is required to access your recently played history. You can extract it directly from the Apple Music web app:
1919+2020+1. Visit [music.apple.com](https://music.apple.com) in your browser and log in.
2121+2. Open your browser's Developer Tools (usually `F12` or <kbd>⌘</kbd> + <kbd>⌥</kbd> + <kbd>I</kbd>).
2222+3. Navigate to the **Application** tab (Chrome/Edge) or **Storage** tab (Safari/Firefox).
2323+4. Expand **Cookies** in the left sidebar and select `https://music.apple.com`.
2424+5. Find the cookie named `media-user-token` and copy its value (it typically starts with `0.`).
2525+2626+:::caution[Token Expiry]
2727+The `mediaUserToken` will eventually expire. If Multi-Scrobbler begins throwing authentication errors in the logs, simply repeat these steps to obtain and configure a fresh token.
2828+:::
2929+3030+### 2. Authentication
3131+3232+To authorize API access, Multi-Scrobbler requires an Authentication Token (JWT). Because the official Apple Music API is designed for developers, there are two ways to provide this token depending on your situation:
3333+3434+* **Browser Token (Free & Most Common):** Best for 99% of users. You can easily extract a temporary token from the Apple Music web player without needing an Apple Developer account.
3535+* **MusicKit Key (Requires Paid Apple Developer Account):** If you happen to be enrolled in the paid Apple Developer Program ($99/year), you can provide a private key to automatically generate permanent tokens.
3636+3737+Choose your preferred method below:
3838+3939+<Tabs groupId="appleMusicAuth" queryString>
4040+ <TabItem value="token" label="Browser Token (Free)">
4141+4242+ Since most users don't have a paid developer account, extracting the token from the web player is the standard approach.
4343+4444+ Behind the scenes, the Apple Music website makes a lot of background requests, which can make finding the right token confusing. To filter out the noise and grab the exact token we need, follow the steps for your browser below:
4545+4646+ <details>
4747+ <summary>Chrome, Edge & Firefox</summary>
4848+4949+ 1. Visit [music.apple.com](https://music.apple.com) and log in.
5050+ 2. Open Developer Tools (`F12` or `Ctrl+Shift+I` / <kbd>⌘</kbd> + <kbd>⌥</kbd> + <kbd>I</kbd>) and navigate to the **Network** tab.
5151+ 3. In the filter box at the top left, paste exactly: `https://amp-api.music.apple.com/v1/me/account` (**Label 1**).
5252+ 4. Click the **Fetch/XHR** filter button to hide irrelevant requests (**Label 2**).
5353+ 5. Reload the page (**Label 3**).
5454+ 6. Under the "Name" column, click the request starting with `account?meta=` (**Label 4**).
5555+ 7. In the panel that opens, scroll down to the **Request Headers** section.
5656+ 8. Find the `authorization` header. Copy the long string of text **after** the `Bearer ` prefix (**Label 5**).
5757+5858+ 
5959+6060+ </details>
6161+6262+ <details>
6363+ <summary>Safari</summary>
6464+6565+ Before you can extract the token in Safari, you must enable developer tools.
6666+6767+ {/* This weird formatting is because ⌘ is usually incredibly tiny */}
6868+ 1. Open Safari Settings (<kbd>⌘</kbd> + <kbd>,</kbd>), navigate to the **Advanced** tab, and check **"Show features for web developers"** at the very bottom.
6969+7070+ 
7171+7272+ 2. Visit [music.apple.com](https://music.apple.com) and log in.
7373+ 3. Open the Web Inspector (<kbd>⌘</kbd>+<kbd>⌥</kbd>+<kbd>I</kbd> or Develop > Show Web Inspector) and navigate to the **Network** tab.
7474+ 4. In the filter bar on the left side of the inspector, paste exactly: `https://amp-api.music.apple.com/v1/me/account` (**Label 1**).
7575+ 5. Reload the page (**Label 2**).
7676+ 6. Click the `account` request that appears in the Name list (**Label 3**).
7777+ 7. In the details sidebar, look under the **Request** section for the `Authorization` header. Copy the long string of text **after** the `Bearer ` prefix (**Label 4**).
7878+7979+ 
8080+8181+ </details>
8282+8383+ <br />
8484+8585+ Once you have your token, add it to your configuration file along with the origin header:
8686+8787+ ```json title="applemusic.json"
8888+ {
8989+ "data": {
9090+ "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsIm...",
9191+ "mediaUserToken": "your-media-user-token-here",
9292+ "origin": "https://music.apple.com"
9393+ }
9494+ }
9595+ ```
9696+9797+ :::info[Origin Header Required]
9898+ When using a JWT extracted from the browser, Apple requires the request to match the domain it was issued to. You **must** include the `origin` field in your config as shown above.
9999+ :::
100100+101101+ *Note: Browser-generated JWTs are valid for a maximum of 35 days and must be updated manually when they expire.*
102102+103103+ </TabItem>
104104+ <TabItem value="key" label="MusicKit Key (Paid Account)">
105105+106106+ :::warning[Apple Developer Account Required]
107107+ To generate JWTs automatically, you must be enrolled in the paid [Apple Developer Program](https://developer.apple.com/programs/) ($99/year) and create a MusicKit key. If you don't have an account, use the **Browser Token (Free)** tab instead.
108108+ :::
109109+110110+ To generate JWTs automatically and avoid manual token refreshes, you need an Apple Music API key:
111111+112112+ 1. Go to the [Apple Developer portal](https://developer.apple.com/account/resources/authkeys/list) and create a **MusicKit** key.
113113+ 2. Download the `.p8` file — this is your private key.
114114+ 3. Note your **Key ID** and **Team ID** (found in your Apple Developer account Membership details).
115115+116116+ Add these details to your config:
117117+118118+ ```json title="applemusic.json"
119119+ {
120120+ "data": {
121121+ "key": {
122122+ "id": "2HPSNJZ88N",
123123+ "teamId": "SN6YASW8G4",
124124+ "p8": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEG...-----END PRIVATE KEY-----"
125125+ },
126126+ "mediaUserToken": "your-media-user-token-here"
127127+ }
128128+ }
129129+ ```
130130+131131+ :::tip
132132+ When pasting the contents of your `.p8` file into JSON, make sure to replace physical line breaks with `\n` so it remains a valid, single-line JSON string.
133133+ :::
134134+135135+ </TabItem>
136136+</Tabs>
137137+138138+---
139139+140140+### How Multi-Scrobbler handles Apple Music quirks
141141+142142+#### Timestamp Estimation
143143+144144+The Apple Music API **does not provide timestamps** for when tracks were played. Multi-Scrobbler estimates play times by taking the current time and subtracting track durations backwards:
145145+* The most recent track is assumed to have finished playing **now**.
146146+* Each older track is estimated to have played `duration` seconds before the previous one.
147147+148148+*For the most accurate scrobble timestamps, it is highly recommended to keep the polling interval (`APPLEMUSIC_INTERVAL`) low (the default is 60 seconds).*
149149+150150+#### Duplicate Play Recovery
151151+152152+The Apple Music history API strictly deduplicates tracks. Instead of showing the same song multiple times in a row, it simply bumps the re-played track back to the #1 spot and removes the older entry.
153153+154154+For example, if you listen to **Song A** → **Song B**, your history is `[Song B, Song A]`. If you then listen to **Song A** again, the API returns `[Song A, Song B]` (Song A was bumped to the top).
155155+156156+If Multi-Scrobbler polled when the top track was Song A, and polls again after it's bumped back to the top, the top track appears functionally unchanged. This usually confuses standard scrobblers into ignoring the update entirely. By default, MS correctly detects this "top-rebound" pattern, extracts the intermediate tracks that were squeezed in (Song B), and scrobbles Song A again as a re-listen.
157157+158158+*Note: Because of this strict deduplication, **"pure loops"** (listening to Song A continuously on repeat without playing anything else in between) cannot be tracked. The history list never changes shape, so MS has no way of knowing it was played again.*
159159+160160+If you ever experience false positives (tracks being scrobbled that you didn't actually re-listen to), you can disable this recovery behavior with `"recoverUnchangedTopHistory": false` in your config options or by setting the `APPLEMUSIC_RECOVER_UNCHANGED_TOP_HISTORY=false` env var.
161161+162162+#### Album name normalization
163163+164164+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.
165165+166166+By default, Multi-Scrobbler strips these suffixes out so only the official name is used.
167167+168168+If you do not want this behavior it can be disabled with ENV `APPLEMUSIC_NORMALIZE_ALBUM=false` or File/AIO option `"normalizeAlbum": false`.
169169+170170+<DetailsAdmo type="tip" summary="Stripping Suffix with User Stage">
171171+172172+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).
173173+174174+Here is an example of creating a User Stage to only strip suffixes for a specific scrobble client:
175175+176176+<details>
177177+178178+<summary>Example</summary>
179179+180180+```json5 title="config.json"
181181+{
182182+ // ...
183183+ "transformers": [
184184+ {
185185+ "type": "user",
186186+ "name": "AppleAlbumStip",
187187+ "defaults": {
188188+ "album": [
189189+ "/ - (EP|Single)$/i"
190190+ ]
191191+ }
192192+ }
193193+ ]
194194+}
195195+```
196196+197197+```json5 title="applemusic.json"
198198+{
199199+ {
200200+ "id": "myAppleMusic",
201201+ "name": "My Apple Music",
202202+ "data": {
203203+ // ...
204204+ },
205205+ // highlight-start
206206+ "options": {
207207+ "normalizeAlbum": false
208208+ }
209209+ // highlight-end
210210+ }
211211+}
212212+```
213213+214214+```json5 title="lastfm.json"
215215+[
216216+ {
217217+ "name": "Foxx LFM Client",
218218+ "id": "myLastFmClient",
219219+ "enable": true,
220220+ "configureAs": "client",
221221+ "data": {
222222+ "apiKey": "a89cba1569901a0671d5a9875fed4be1",
223223+ "secret": "ec42e09d5ae0ee0f0816ca151008412a",
224224+ "redirectUri": "http://localhost:9078/lastfm/callback"
225225+ },
226226+ // highlight-start
227227+ "options": {
228228+ "playTransform": {
229229+ "preCompare": [
230230+ {
231231+ "type": "user",
232232+ "name": "AppleAlbumStip",
233233+ }
234234+ ]
235235+ }
236236+ }
237237+ // highlight-end
238238+ }
239239+]
240240+```
241241+242242+</details>
243243+244244+</DetailsAdmo>
245245+246246+## Configuration Reference
247247+248248+<Config config="AppleMusicSourceConfig" fileContent={AppleMusicConfig} name="applemusic">
249249+ | Environment Variable | Required? | Default | Description |
250250+ | ------------------------------------------ | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
251251+ | `APPLEMUSIC_ID` | Yes | | A unique ID for this source. |
252252+ | `APPLEMUSIC_MEDIA_USER_TOKEN` | Yes | | The media-user-token extracted from the browser. |
253253+ | `APPLEMUSIC_KEY_ID` | No | | Key ID from your MusicKit key. |
254254+ | `APPLEMUSIC_TEAM_ID` | No | | Team ID from your Apple Developer account. |
255255+ | `APPLEMUSIC_KEY_P8` | No | | The contents of your MusicKit `.p8` private key file. |
256256+ | `APPLEMUSIC_TOKEN` | No | | The authentication JWT extracted from the browser. |
257257+ | `APPLEMUSIC_INTERVAL` | No | `60` | Polling interval in seconds. |
258258+ | `APPLEMUSIC_ORIGIN_HEADER` | No | | Origin header for API requests, e.g. `https://music.apple.com`. Required when using a browser token. |
259259+ | `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. |
260260+ | `APPLEMUSIC_NORMALIZE_ALBUM` | No | `true` | Strips extraneous `- EP` and `- Single` suffixes from album names |
261261+ | `APPLEMUSIC_NAME` | No | | A vanity name different than the ID. |
262262+</Config>
···1313A dockerized app that monitors your music listening activity from *everywhere* and scrobbles it *anywhere*.
14141515* Supports monitoring activity from many [**Sources**](/configuration/sources)
1616+ * [Apple Music](/configuration/sources/applemusic)
1617 * [Azuracast](/configuration/sources/azuracast)
1718 * [Deezer](/configuration/sources/deezer)
1819 * [Google Cast (Chromecast)](/configuration/sources/google-cast)
···11+import type {PollingOptions} from "../common.ts";
22+import type {CommonSourceConfig, CommonSourceData, CommonSourceOptions} from "./index.ts";
33+44+export interface AppleMusicKey {
55+ id: string
66+ teamId: string
77+ p8: string
88+}
99+1010+export interface AppleMusicData extends CommonSourceData, PollingOptions {
1111+ key?: AppleMusicKey
1212+ token?: string
1313+ mediaUserToken?: string
1414+ /**
1515+ * Origin header to include in every Apple Music API request.
1616+ * Required when using a browser token (not a MusicKit key).
1717+ *
1818+ * @example "https://music.apple.com"
1919+ */
2020+ origin?: string
2121+}
2222+2323+export interface AppleMusicSourceConfig extends CommonSourceConfig {
2424+ data?: AppleMusicData
2525+ options?: CommonSourceOptions & {
2626+ logAuth?: boolean
2727+ logDiff?: boolean
2828+ /**
2929+ * Fixes a quirk where Apple Music's history API hides duplicate plays.
3030+ * If you listen to A → B → A, the API returns [A, B, X, Y ...] instead of [A, B, A, X, Y ...].
3131+ * This can cause MS to skip the second A play.
3232+ *
3333+ * When enabled (default), MS detects this pattern, keeps the interim tracks (B),
3434+ * and re-scrobbles A as a re-listen. Disable only if you notice false positives.
3535+ *
3636+ * @default true
3737+ * @examples [true, false]
3838+ */
3939+ recoverUnchangedTopHistory?: boolean
4040+ /**
4141+ * Removes extraneous suffixes from album data
4242+ *
4343+ * Apple Music add ' - EP' and ' - Single' to album names for EP's and singles, respectively.
4444+ * These suffixes are not part of the official names for the album and can cause issues in scrobble services
4545+ * or when matching metadata (musicbrainz).
4646+ *
4747+ * When this option is true (default), Multi-scrobbler automatically removes these suffixes.
4848+ *
4949+ * @default true
5050+ * @examples [true, false]
5151+ */
5252+ normalizeAlbum?: boolean
5353+ }
5454+}
5555+5656+export interface AppleMusicSourceAIOConfig extends AppleMusicSourceConfig {
5757+ type: 'applemusic'
5858+}