@open-kingdom/shared-backend-integration-test-doubles 0.0.2-18 → 0.0.2-21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/dist/apple-music/apple-music-error-mode.d.ts +17 -0
- package/dist/apple-music/apple-music-error-mode.d.ts.map +1 -0
- package/dist/apple-music/apple-music-error-mode.js +61 -0
- package/dist/apple-music/apple-music-error-mode.js.map +1 -0
- package/dist/apple-music/apple-music-twin.config.d.ts +9 -0
- package/dist/apple-music/apple-music-twin.config.d.ts.map +1 -0
- package/dist/apple-music/apple-music-twin.config.js +25 -0
- package/dist/apple-music/apple-music-twin.config.js.map +1 -0
- package/dist/apple-music/apple-music-twin.d.ts +17 -0
- package/dist/apple-music/apple-music-twin.d.ts.map +1 -0
- package/dist/apple-music/apple-music-twin.js +325 -0
- package/dist/apple-music/apple-music-twin.js.map +1 -0
- package/dist/apple-music/catalog-fixtures.d.ts +19 -0
- package/dist/apple-music/catalog-fixtures.d.ts.map +1 -0
- package/dist/apple-music/catalog-fixtures.js +94 -0
- package/dist/apple-music/catalog-fixtures.js.map +1 -0
- package/dist/apple-music/catalog-response.d.ts +59 -0
- package/dist/apple-music/catalog-response.d.ts.map +1 -0
- package/dist/apple-music/catalog-response.js +63 -0
- package/dist/apple-music/catalog-response.js.map +1 -0
- package/dist/apple-music/index.d.ts +8 -0
- package/dist/apple-music/index.d.ts.map +1 -0
- package/dist/apple-music/index.js +6 -0
- package/dist/apple-music/index.js.map +1 -0
- package/dist/apple-music/msw-handlers.d.ts +12 -0
- package/dist/apple-music/msw-handlers.d.ts.map +1 -0
- package/dist/apple-music/msw-handlers.js +18 -0
- package/dist/apple-music/msw-handlers.js.map +1 -0
- package/dist/apple-music/player-shim.d.ts +3 -0
- package/dist/apple-music/player-shim.d.ts.map +1 -0
- package/dist/apple-music/player-shim.js +310 -0
- package/dist/apple-music/player-shim.js.map +1 -0
- package/dist/artwork/playlist-001.svg +13 -0
- package/dist/artwork/playlist-002.svg +13 -0
- package/dist/artwork/track-001.svg +12 -0
- package/dist/artwork/track-002.svg +12 -0
- package/dist/artwork/track-003.svg +13 -0
- package/dist/artwork/track-004.svg +13 -0
- package/dist/artwork/track-005.svg +13 -0
- package/dist/artwork/track-006.svg +13 -0
- package/dist/artwork/track-007.svg +13 -0
- package/dist/audio/mock-1.mp3 +0 -0
- package/dist/audio/mock-2.mp3 +0 -0
- package/dist/audio/mock-3.mp3 +0 -0
- package/dist/gcs/gcs-error-mode.js +1 -1
- package/dist/gcs/gcs-error-mode.js.map +1 -1
- package/dist/gcs/gcs-error-simulation.interceptor.js.map +1 -1
- package/dist/gcs/gcs-twin.config.js.map +1 -1
- package/dist/gcs/gcs-twin.js +1 -1
- package/dist/gcs/gcs-twin.js.map +1 -1
- package/dist/gcs/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptor/http-helper.js.map +1 -1
- package/dist/interceptor/index.js.map +1 -1
- package/dist/interceptor/node-interceptor.js.map +1 -1
- package/dist/interceptor/routing-table.config.d.ts.map +1 -1
- package/dist/interceptor/routing-table.config.js +10 -0
- package/dist/interceptor/routing-table.config.js.map +1 -1
- package/dist/interceptor/routing-table.js +1 -1
- package/dist/interceptor/routing-table.js.map +1 -1
- package/dist/lib/integration-test-doubles.js.map +1 -1
- package/dist/shared/activation.js.map +1 -1
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/constants.d.ts +4 -0
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +4 -2
- package/dist/shared/constants.js.map +1 -1
- package/dist/youtube/index.js.map +1 -1
- package/dist/youtube/msw-handlers.js.map +1 -1
- package/dist/youtube/player-shim.js.map +1 -1
- package/dist/youtube/search-fixtures.js.map +1 -1
- package/dist/youtube/search-response.js.map +1 -1
- package/dist/youtube/youtube-error-mode.js.map +1 -1
- package/dist/youtube/youtube-twin.config.js.map +1 -1
- package/dist/youtube/youtube-twin.js +5 -5
- package/dist/youtube/youtube-twin.js.map +1 -1
- package/package.json +23 -3
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// All fixtures reference the ~15 second clips shipped under ./audio. Keep
|
|
2
|
+
// durationMs in sync with those clips so catalog metadata matches playback.
|
|
3
|
+
const TRACK_DURATION_MS = 15000;
|
|
4
|
+
export const trackFixtures = [
|
|
5
|
+
{
|
|
6
|
+
id: 'mock-track-001',
|
|
7
|
+
name: 'Morning Meditation Calm',
|
|
8
|
+
artistName: 'Zen Nature Sounds',
|
|
9
|
+
albumName: 'Zen Garden',
|
|
10
|
+
durationMs: TRACK_DURATION_MS,
|
|
11
|
+
artworkFile: 'track-001.svg',
|
|
12
|
+
audioFile: 'mock-1.mp3'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'mock-track-002',
|
|
16
|
+
name: 'Deep Sleep Ambient Rain',
|
|
17
|
+
artistName: 'Rainstorm Ambient',
|
|
18
|
+
albumName: 'Relaxing Storms',
|
|
19
|
+
durationMs: TRACK_DURATION_MS,
|
|
20
|
+
artworkFile: 'track-002.svg',
|
|
21
|
+
audioFile: 'mock-2.mp3'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'mock-track-003',
|
|
25
|
+
name: 'Relaxing Acoustic Guitar',
|
|
26
|
+
artistName: 'Guitar Chillout',
|
|
27
|
+
albumName: 'Acoustic Sunday',
|
|
28
|
+
durationMs: TRACK_DURATION_MS,
|
|
29
|
+
artworkFile: 'track-003.svg',
|
|
30
|
+
audioFile: 'mock-3.mp3'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'mock-track-004',
|
|
34
|
+
name: 'Lo-Fi Study Beats',
|
|
35
|
+
artistName: 'Coffee Shop Chill',
|
|
36
|
+
albumName: 'Late Night Chill',
|
|
37
|
+
durationMs: TRACK_DURATION_MS,
|
|
38
|
+
artworkFile: 'track-004.svg',
|
|
39
|
+
audioFile: 'mock-1.mp3'
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'mock-track-005',
|
|
43
|
+
name: 'Classical Serenade',
|
|
44
|
+
artistName: 'Symphony Ensemble',
|
|
45
|
+
albumName: 'Baroque Classics',
|
|
46
|
+
durationMs: TRACK_DURATION_MS,
|
|
47
|
+
artworkFile: 'track-005.svg',
|
|
48
|
+
audioFile: 'mock-2.mp3'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'mock-track-006',
|
|
52
|
+
name: 'Focus Instrumental Piano',
|
|
53
|
+
artistName: 'Keyboard Maestro',
|
|
54
|
+
albumName: 'Focus & Study Piano',
|
|
55
|
+
durationMs: TRACK_DURATION_MS,
|
|
56
|
+
artworkFile: 'track-006.svg',
|
|
57
|
+
audioFile: 'mock-3.mp3'
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'mock-track-007',
|
|
61
|
+
name: 'Birdsong Morning Woods',
|
|
62
|
+
artistName: 'Nature Recording Collective',
|
|
63
|
+
albumName: 'Sounds of the Forest',
|
|
64
|
+
durationMs: TRACK_DURATION_MS,
|
|
65
|
+
artworkFile: 'track-007.svg',
|
|
66
|
+
audioFile: 'mock-1.mp3'
|
|
67
|
+
}
|
|
68
|
+
];
|
|
69
|
+
export const playlistFixtures = [
|
|
70
|
+
{
|
|
71
|
+
id: 'mock-playlist-001',
|
|
72
|
+
name: 'Relaxation Essentials',
|
|
73
|
+
description: 'A curated playlist containing relaxing nature sounds and acoustic guitars.',
|
|
74
|
+
artworkFile: 'playlist-001.svg',
|
|
75
|
+
trackIds: [
|
|
76
|
+
'mock-track-001',
|
|
77
|
+
'mock-track-002',
|
|
78
|
+
'mock-track-003',
|
|
79
|
+
'mock-track-007'
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'mock-playlist-002',
|
|
84
|
+
name: 'Study Focus Beats',
|
|
85
|
+
description: 'Instrumental music and low fidelity tracks to help you concentrate.',
|
|
86
|
+
artworkFile: 'playlist-002.svg',
|
|
87
|
+
trackIds: [
|
|
88
|
+
'mock-track-004',
|
|
89
|
+
'mock-track-006'
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
//# sourceMappingURL=catalog-fixtures.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/apple-music/catalog-fixtures.ts"],"sourcesContent":["export interface AppleMusicTrackFixture {\n id: string;\n name: string;\n artistName: string;\n albumName: string;\n durationMs: number;\n artworkFile: string | null;\n audioFile: string;\n}\n\nexport interface AppleMusicPlaylistFixture {\n id: string;\n name: string;\n description: string | null;\n artworkFile: string | null;\n trackIds: string[];\n}\n\n// All fixtures reference the ~15 second clips shipped under ./audio. Keep\n// durationMs in sync with those clips so catalog metadata matches playback.\nconst TRACK_DURATION_MS = 15000;\n\nexport const trackFixtures: AppleMusicTrackFixture[] = [\n {\n id: 'mock-track-001',\n name: 'Morning Meditation Calm',\n artistName: 'Zen Nature Sounds',\n albumName: 'Zen Garden',\n durationMs: TRACK_DURATION_MS,\n artworkFile: 'track-001.svg',\n audioFile: 'mock-1.mp3',\n },\n {\n id: 'mock-track-002',\n name: 'Deep Sleep Ambient Rain',\n artistName: 'Rainstorm Ambient',\n albumName: 'Relaxing Storms',\n durationMs: TRACK_DURATION_MS,\n artworkFile: 'track-002.svg',\n audioFile: 'mock-2.mp3',\n },\n {\n id: 'mock-track-003',\n name: 'Relaxing Acoustic Guitar',\n artistName: 'Guitar Chillout',\n albumName: 'Acoustic Sunday',\n durationMs: TRACK_DURATION_MS,\n artworkFile: 'track-003.svg',\n audioFile: 'mock-3.mp3',\n },\n {\n id: 'mock-track-004',\n name: 'Lo-Fi Study Beats',\n artistName: 'Coffee Shop Chill',\n albumName: 'Late Night Chill',\n durationMs: TRACK_DURATION_MS,\n artworkFile: 'track-004.svg',\n audioFile: 'mock-1.mp3',\n },\n {\n id: 'mock-track-005',\n name: 'Classical Serenade',\n artistName: 'Symphony Ensemble',\n albumName: 'Baroque Classics',\n durationMs: TRACK_DURATION_MS,\n artworkFile: 'track-005.svg',\n audioFile: 'mock-2.mp3',\n },\n {\n id: 'mock-track-006',\n name: 'Focus Instrumental Piano',\n artistName: 'Keyboard Maestro',\n albumName: 'Focus & Study Piano',\n durationMs: TRACK_DURATION_MS,\n artworkFile: 'track-006.svg',\n audioFile: 'mock-3.mp3',\n },\n {\n id: 'mock-track-007',\n name: 'Birdsong Morning Woods',\n artistName: 'Nature Recording Collective',\n albumName: 'Sounds of the Forest',\n durationMs: TRACK_DURATION_MS,\n artworkFile: 'track-007.svg',\n audioFile: 'mock-1.mp3',\n },\n];\n\nexport const playlistFixtures: AppleMusicPlaylistFixture[] = [\n {\n id: 'mock-playlist-001',\n name: 'Relaxation Essentials',\n description:\n 'A curated playlist containing relaxing nature sounds and acoustic guitars.',\n artworkFile: 'playlist-001.svg',\n trackIds: [\n 'mock-track-001',\n 'mock-track-002',\n 'mock-track-003',\n 'mock-track-007',\n ],\n },\n {\n id: 'mock-playlist-002',\n name: 'Study Focus Beats',\n description:\n 'Instrumental music and low fidelity tracks to help you concentrate.',\n artworkFile: 'playlist-002.svg',\n trackIds: ['mock-track-004', 'mock-track-006'],\n },\n];\n"],"names":["TRACK_DURATION_MS","trackFixtures","id","name","artistName","albumName","durationMs","artworkFile","audioFile","playlistFixtures","description","trackIds"],"mappings":"AAkBA,0EAA0E;AAC1E,4EAA4E;AAC5E,MAAMA,oBAAoB;AAE1B,OAAO,MAAMC,gBAA0C;IACrD;QACEC,IAAI;QACJC,MAAM;QACNC,YAAY;QACZC,WAAW;QACXC,YAAYN;QACZO,aAAa;QACbC,WAAW;IACb;IACA;QACEN,IAAI;QACJC,MAAM;QACNC,YAAY;QACZC,WAAW;QACXC,YAAYN;QACZO,aAAa;QACbC,WAAW;IACb;IACA;QACEN,IAAI;QACJC,MAAM;QACNC,YAAY;QACZC,WAAW;QACXC,YAAYN;QACZO,aAAa;QACbC,WAAW;IACb;IACA;QACEN,IAAI;QACJC,MAAM;QACNC,YAAY;QACZC,WAAW;QACXC,YAAYN;QACZO,aAAa;QACbC,WAAW;IACb;IACA;QACEN,IAAI;QACJC,MAAM;QACNC,YAAY;QACZC,WAAW;QACXC,YAAYN;QACZO,aAAa;QACbC,WAAW;IACb;IACA;QACEN,IAAI;QACJC,MAAM;QACNC,YAAY;QACZC,WAAW;QACXC,YAAYN;QACZO,aAAa;QACbC,WAAW;IACb;IACA;QACEN,IAAI;QACJC,MAAM;QACNC,YAAY;QACZC,WAAW;QACXC,YAAYN;QACZO,aAAa;QACbC,WAAW;IACb;CACD,CAAC;AAEF,OAAO,MAAMC,mBAAgD;IAC3D;QACEP,IAAI;QACJC,MAAM;QACNO,aACE;QACFH,aAAa;QACbI,UAAU;YACR;YACA;YACA;YACA;SACD;IACH;IACA;QACET,IAAI;QACJC,MAAM;QACNO,aACE;QACFH,aAAa;QACbI,UAAU;YAAC;YAAkB;SAAiB;IAChD;CACD,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { AppleMusicTrackFixture, AppleMusicPlaylistFixture } from './catalog-fixtures.js';
|
|
2
|
+
export declare function formatTrackResource(track: AppleMusicTrackFixture, baseUrl?: string): {
|
|
3
|
+
id: string;
|
|
4
|
+
type: string;
|
|
5
|
+
attributes: {
|
|
6
|
+
name: string;
|
|
7
|
+
artistName: string;
|
|
8
|
+
albumName: string;
|
|
9
|
+
durationInMillis: number;
|
|
10
|
+
audioUrl: string | null;
|
|
11
|
+
artwork: {
|
|
12
|
+
url: string;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
} | null;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export declare function formatPlaylistResource(playlist: AppleMusicPlaylistFixture, allTracks: AppleMusicTrackFixture[], baseUrl?: string): {
|
|
19
|
+
id: string;
|
|
20
|
+
type: string;
|
|
21
|
+
attributes: {
|
|
22
|
+
name: string;
|
|
23
|
+
description: {
|
|
24
|
+
standard: string;
|
|
25
|
+
} | null;
|
|
26
|
+
artwork: {
|
|
27
|
+
url: string;
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
} | null;
|
|
31
|
+
trackCount: number;
|
|
32
|
+
};
|
|
33
|
+
relationships: {
|
|
34
|
+
tracks: {
|
|
35
|
+
data: {
|
|
36
|
+
id: string;
|
|
37
|
+
type: string;
|
|
38
|
+
attributes: {
|
|
39
|
+
name: string;
|
|
40
|
+
artistName: string;
|
|
41
|
+
albumName: string;
|
|
42
|
+
durationInMillis: number;
|
|
43
|
+
audioUrl: string | null;
|
|
44
|
+
artwork: {
|
|
45
|
+
url: string;
|
|
46
|
+
width: number;
|
|
47
|
+
height: number;
|
|
48
|
+
} | null;
|
|
49
|
+
};
|
|
50
|
+
}[];
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
export declare function formatSearchResponse(tracks: AppleMusicTrackFixture[], playlists: AppleMusicPlaylistFixture[], allTracks: AppleMusicTrackFixture[], baseUrl?: string): {
|
|
55
|
+
results: Record<string, {
|
|
56
|
+
data: unknown[];
|
|
57
|
+
}>;
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=catalog-response.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-response.d.ts","sourceRoot":"","sources":["../../src/apple-music/catalog-response.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EAC1B,MAAM,uBAAuB,CAAC;AAiB/B,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,sBAAsB,EAC7B,OAAO,CAAC,EAAE,MAAM;;;;;;;;;;iBAdR,MAAM;mBAAS,MAAM;oBAAU,MAAM;;;EA4B9C;AAED,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,yBAAyB,EACnC,SAAS,EAAE,sBAAsB,EAAE,EACnC,OAAO,CAAC,EAAE,MAAM;;;;;;;;;iBAjCR,MAAM;mBAAS,MAAM;oBAAU,MAAM;;;;;;;;;;;;;;;;6BAArC,MAAM;+BAAS,MAAM;gCAAU,MAAM;;;;;;EA0D9C;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,sBAAsB,EAAE,EAChC,SAAS,EAAE,yBAAyB,EAAE,EACtC,SAAS,EAAE,sBAAsB,EAAE,EACnC,OAAO,CAAC,EAAE,MAAM;aAEW,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC;EAiB/D"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
function formatArtwork(artworkFile, baseUrl) {
|
|
2
|
+
if (!artworkFile || !baseUrl) return null;
|
|
3
|
+
// Real MusicKit artwork URLs carry {w}/{h} placeholders that clients
|
|
4
|
+
// substitute with the size they need — keep that shape so consumers (and
|
|
5
|
+
// the demo UI's .replace('{w}', ...) calls) exercise the real contract.
|
|
6
|
+
return {
|
|
7
|
+
url: `${baseUrl}/v1/artwork/{w}x{h}/${artworkFile}`,
|
|
8
|
+
width: 300,
|
|
9
|
+
height: 300
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function formatTrackResource(track, baseUrl) {
|
|
13
|
+
return {
|
|
14
|
+
id: track.id,
|
|
15
|
+
type: 'songs',
|
|
16
|
+
attributes: {
|
|
17
|
+
name: track.name,
|
|
18
|
+
artistName: track.artistName,
|
|
19
|
+
albumName: track.albumName,
|
|
20
|
+
durationInMillis: track.durationMs,
|
|
21
|
+
audioUrl: baseUrl ? `${baseUrl}/v1/audio/${track.audioFile}` : null,
|
|
22
|
+
artwork: formatArtwork(track.artworkFile, baseUrl)
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function formatPlaylistResource(playlist, allTracks, baseUrl) {
|
|
27
|
+
const playlistTracks = playlist.trackIds.map((id)=>allTracks.find((t)=>t.id === id)).filter((t)=>!!t);
|
|
28
|
+
return {
|
|
29
|
+
id: playlist.id,
|
|
30
|
+
type: 'playlists',
|
|
31
|
+
attributes: {
|
|
32
|
+
name: playlist.name,
|
|
33
|
+
description: playlist.description ? {
|
|
34
|
+
standard: playlist.description
|
|
35
|
+
} : null,
|
|
36
|
+
artwork: formatArtwork(playlist.artworkFile, baseUrl),
|
|
37
|
+
trackCount: playlistTracks.length
|
|
38
|
+
},
|
|
39
|
+
relationships: {
|
|
40
|
+
tracks: {
|
|
41
|
+
data: playlistTracks.map((t)=>formatTrackResource(t, baseUrl))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function formatSearchResponse(tracks, playlists, allTracks, baseUrl) {
|
|
47
|
+
const response = {
|
|
48
|
+
results: {}
|
|
49
|
+
};
|
|
50
|
+
if (tracks.length > 0) {
|
|
51
|
+
response.results.songs = {
|
|
52
|
+
data: tracks.map((t)=>formatTrackResource(t, baseUrl))
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (playlists.length > 0) {
|
|
56
|
+
response.results.playlists = {
|
|
57
|
+
data: playlists.map((p)=>formatPlaylistResource(p, allTracks, baseUrl))
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return response;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=catalog-response.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/apple-music/catalog-response.ts"],"sourcesContent":["import {\n AppleMusicTrackFixture,\n AppleMusicPlaylistFixture,\n} from './catalog-fixtures.js';\n\nfunction formatArtwork(\n artworkFile: string | null,\n baseUrl?: string\n): { url: string; width: number; height: number } | null {\n if (!artworkFile || !baseUrl) return null;\n // Real MusicKit artwork URLs carry {w}/{h} placeholders that clients\n // substitute with the size they need — keep that shape so consumers (and\n // the demo UI's .replace('{w}', ...) calls) exercise the real contract.\n return {\n url: `${baseUrl}/v1/artwork/{w}x{h}/${artworkFile}`,\n width: 300,\n height: 300,\n };\n}\n\nexport function formatTrackResource(\n track: AppleMusicTrackFixture,\n baseUrl?: string\n) {\n return {\n id: track.id,\n type: 'songs',\n attributes: {\n name: track.name,\n artistName: track.artistName,\n albumName: track.albumName,\n durationInMillis: track.durationMs,\n audioUrl: baseUrl ? `${baseUrl}/v1/audio/${track.audioFile}` : null,\n artwork: formatArtwork(track.artworkFile, baseUrl),\n },\n };\n}\n\nexport function formatPlaylistResource(\n playlist: AppleMusicPlaylistFixture,\n allTracks: AppleMusicTrackFixture[],\n baseUrl?: string\n) {\n const playlistTracks = playlist.trackIds\n .map((id) => allTracks.find((t) => t.id === id))\n .filter((t): t is AppleMusicTrackFixture => !!t);\n\n return {\n id: playlist.id,\n type: 'playlists',\n attributes: {\n name: playlist.name,\n description: playlist.description\n ? {\n standard: playlist.description,\n }\n : null,\n artwork: formatArtwork(playlist.artworkFile, baseUrl),\n trackCount: playlistTracks.length,\n },\n relationships: {\n tracks: {\n data: playlistTracks.map((t) => formatTrackResource(t, baseUrl)),\n },\n },\n };\n}\n\nexport function formatSearchResponse(\n tracks: AppleMusicTrackFixture[],\n playlists: AppleMusicPlaylistFixture[],\n allTracks: AppleMusicTrackFixture[],\n baseUrl?: string\n) {\n const response: { results: Record<string, { data: unknown[] }> } = {\n results: {},\n };\n\n if (tracks.length > 0) {\n response.results.songs = {\n data: tracks.map((t) => formatTrackResource(t, baseUrl)),\n };\n }\n\n if (playlists.length > 0) {\n response.results.playlists = {\n data: playlists.map((p) => formatPlaylistResource(p, allTracks, baseUrl)),\n };\n }\n\n return response;\n}\n"],"names":["formatArtwork","artworkFile","baseUrl","url","width","height","formatTrackResource","track","id","type","attributes","name","artistName","albumName","durationInMillis","durationMs","audioUrl","audioFile","artwork","formatPlaylistResource","playlist","allTracks","playlistTracks","trackIds","map","find","t","filter","description","standard","trackCount","length","relationships","tracks","data","formatSearchResponse","playlists","response","results","songs","p"],"mappings":"AAKA,SAASA,cACPC,WAA0B,EAC1BC,OAAgB;IAEhB,IAAI,CAACD,eAAe,CAACC,SAAS,OAAO;IACrC,qEAAqE;IACrE,yEAAyE;IACzE,wEAAwE;IACxE,OAAO;QACLC,KAAK,GAAGD,QAAQ,oBAAoB,EAAED,aAAa;QACnDG,OAAO;QACPC,QAAQ;IACV;AACF;AAEA,OAAO,SAASC,oBACdC,KAA6B,EAC7BL,OAAgB;IAEhB,OAAO;QACLM,IAAID,MAAMC,EAAE;QACZC,MAAM;QACNC,YAAY;YACVC,MAAMJ,MAAMI,IAAI;YAChBC,YAAYL,MAAMK,UAAU;YAC5BC,WAAWN,MAAMM,SAAS;YAC1BC,kBAAkBP,MAAMQ,UAAU;YAClCC,UAAUd,UAAU,GAAGA,QAAQ,UAAU,EAAEK,MAAMU,SAAS,EAAE,GAAG;YAC/DC,SAASlB,cAAcO,MAAMN,WAAW,EAAEC;QAC5C;IACF;AACF;AAEA,OAAO,SAASiB,uBACdC,QAAmC,EACnCC,SAAmC,EACnCnB,OAAgB;IAEhB,MAAMoB,iBAAiBF,SAASG,QAAQ,CACrCC,GAAG,CAAC,CAAChB,KAAOa,UAAUI,IAAI,CAAC,CAACC,IAAMA,EAAElB,EAAE,KAAKA,KAC3CmB,MAAM,CAAC,CAACD,IAAmC,CAAC,CAACA;IAEhD,OAAO;QACLlB,IAAIY,SAASZ,EAAE;QACfC,MAAM;QACNC,YAAY;YACVC,MAAMS,SAAST,IAAI;YACnBiB,aAAaR,SAASQ,WAAW,GAC7B;gBACEC,UAAUT,SAASQ,WAAW;YAChC,IACA;YACJV,SAASlB,cAAcoB,SAASnB,WAAW,EAAEC;YAC7C4B,YAAYR,eAAeS,MAAM;QACnC;QACAC,eAAe;YACbC,QAAQ;gBACNC,MAAMZ,eAAeE,GAAG,CAAC,CAACE,IAAMpB,oBAAoBoB,GAAGxB;YACzD;QACF;IACF;AACF;AAEA,OAAO,SAASiC,qBACdF,MAAgC,EAChCG,SAAsC,EACtCf,SAAmC,EACnCnB,OAAgB;IAEhB,MAAMmC,WAA6D;QACjEC,SAAS,CAAC;IACZ;IAEA,IAAIL,OAAOF,MAAM,GAAG,GAAG;QACrBM,SAASC,OAAO,CAACC,KAAK,GAAG;YACvBL,MAAMD,OAAOT,GAAG,CAAC,CAACE,IAAMpB,oBAAoBoB,GAAGxB;QACjD;IACF;IAEA,IAAIkC,UAAUL,MAAM,GAAG,GAAG;QACxBM,SAASC,OAAO,CAACF,SAAS,GAAG;YAC3BF,MAAME,UAAUZ,GAAG,CAAC,CAACgB,IAAMrB,uBAAuBqB,GAAGnB,WAAWnB;QAClE;IACF;IAEA,OAAOmC;AACT"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { AppleMusicTwin } from './apple-music-twin.js';
|
|
2
|
+
export { defaultAppleMusicConfig, createAppleMusicConfig, } from './apple-music-twin.config.js';
|
|
3
|
+
export type { AppleMusicTwinConfig } from './apple-music-twin.config.js';
|
|
4
|
+
export { AppleMusicErrorModeManager } from './apple-music-error-mode.js';
|
|
5
|
+
export type { AppleMusicErrorModeType, AppleMusicErrorModeConfig, AppleMusicErrorSimulationResult, } from './apple-music-error-mode.js';
|
|
6
|
+
export { getAppleMusicMswHandlerConfigs } from './msw-handlers.js';
|
|
7
|
+
export type { AppleMusicTrackFixture, AppleMusicPlaylistFixture, } from './catalog-fixtures.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/apple-music/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EACL,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACzE,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AACzE,YAAY,EACV,uBAAuB,EACvB,yBAAyB,EACzB,+BAA+B,GAChC,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,8BAA8B,EAAE,MAAM,mBAAmB,CAAC;AACnE,YAAY,EACV,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { AppleMusicTwin } from './apple-music-twin.js';
|
|
2
|
+
export { defaultAppleMusicConfig, createAppleMusicConfig } from './apple-music-twin.config.js';
|
|
3
|
+
export { AppleMusicErrorModeManager } from './apple-music-error-mode.js';
|
|
4
|
+
export { getAppleMusicMswHandlerConfigs } from './msw-handlers.js';
|
|
5
|
+
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/apple-music/index.ts"],"sourcesContent":["export { AppleMusicTwin } from './apple-music-twin.js';\nexport {\n defaultAppleMusicConfig,\n createAppleMusicConfig,\n} from './apple-music-twin.config.js';\nexport type { AppleMusicTwinConfig } from './apple-music-twin.config.js';\nexport { AppleMusicErrorModeManager } from './apple-music-error-mode.js';\nexport type {\n AppleMusicErrorModeType,\n AppleMusicErrorModeConfig,\n AppleMusicErrorSimulationResult,\n} from './apple-music-error-mode.js';\nexport { getAppleMusicMswHandlerConfigs } from './msw-handlers.js';\nexport type {\n AppleMusicTrackFixture,\n AppleMusicPlaylistFixture,\n} from './catalog-fixtures.js';\n"],"names":["AppleMusicTwin","defaultAppleMusicConfig","createAppleMusicConfig","AppleMusicErrorModeManager","getAppleMusicMswHandlerConfigs"],"mappings":"AAAA,SAASA,cAAc,QAAQ,wBAAwB;AACvD,SACEC,uBAAuB,EACvBC,sBAAsB,QACjB,+BAA+B;AAEtC,SAASC,0BAA0B,QAAQ,8BAA8B;AAMzE,SAASC,8BAA8B,QAAQ,oBAAoB"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface MswHandlerConfig {
|
|
2
|
+
/** The original URL to intercept */
|
|
3
|
+
url: string;
|
|
4
|
+
/** The local twin URL to proxy to */
|
|
5
|
+
proxyTo: string;
|
|
6
|
+
/** Content-Type header for the response */
|
|
7
|
+
contentType: string;
|
|
8
|
+
/** Description for debugging */
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function getAppleMusicMswHandlerConfigs(twinBaseUrl?: string): MswHandlerConfig[];
|
|
12
|
+
//# sourceMappingURL=msw-handlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"msw-handlers.d.ts","sourceRoot":"","sources":["../../src/apple-music/msw-handlers.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,qCAAqC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,8BAA8B,CAC5C,WAAW,SAA0B,GACpC,gBAAgB,EAAE,CAepB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function getAppleMusicMswHandlerConfigs(twinBaseUrl = 'http://localhost:9019') {
|
|
2
|
+
return [
|
|
3
|
+
{
|
|
4
|
+
url: 'https://js-cdn.music.apple.com/musickit/v3/musickit.js',
|
|
5
|
+
proxyTo: `${twinBaseUrl}/musickit.js`,
|
|
6
|
+
contentType: 'application/javascript',
|
|
7
|
+
description: 'Apple MusicKit JS SDK shim'
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
url: 'https://api.music.apple.com/v1/*',
|
|
11
|
+
proxyTo: `${twinBaseUrl}/v1/*`,
|
|
12
|
+
contentType: 'application/json',
|
|
13
|
+
description: 'Apple Music REST API catalog redirects'
|
|
14
|
+
}
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=msw-handlers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/apple-music/msw-handlers.ts"],"sourcesContent":["export interface MswHandlerConfig {\n /** The original URL to intercept */\n url: string;\n /** The local twin URL to proxy to */\n proxyTo: string;\n /** Content-Type header for the response */\n contentType: string;\n /** Description for debugging */\n description: string;\n}\n\nexport function getAppleMusicMswHandlerConfigs(\n twinBaseUrl = 'http://localhost:9019'\n): MswHandlerConfig[] {\n return [\n {\n url: 'https://js-cdn.music.apple.com/musickit/v3/musickit.js',\n proxyTo: `${twinBaseUrl}/musickit.js`,\n contentType: 'application/javascript',\n description: 'Apple MusicKit JS SDK shim',\n },\n {\n url: 'https://api.music.apple.com/v1/*',\n proxyTo: `${twinBaseUrl}/v1/*`,\n contentType: 'application/json',\n description: 'Apple Music REST API catalog redirects',\n },\n ];\n}\n"],"names":["getAppleMusicMswHandlerConfigs","twinBaseUrl","url","proxyTo","contentType","description"],"mappings":"AAWA,OAAO,SAASA,+BACdC,cAAc,uBAAuB;IAErC,OAAO;QACL;YACEC,KAAK;YACLC,SAAS,GAAGF,YAAY,YAAY,CAAC;YACrCG,aAAa;YACbC,aAAa;QACf;QACA;YACEH,KAAK;YACLC,SAAS,GAAGF,YAAY,KAAK,CAAC;YAC9BG,aAAa;YACbC,aAAa;QACf;KACD;AACH"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const SHIM_TEMPLATE = "(function() {\n 'use strict';\n \n var PlaybackStates = {\n none: 0,\n loading: 1,\n playing: 2,\n paused: 3,\n stopped: 4,\n ended: 5,\n seeking: 6,\n waiting: 7,\n stalled: 8,\n completed: 9\n };\n\n var PlayerShuffleMode = {\n off: 0,\n songs: 1,\n albums: 2\n };\n\n var instance = null;\n\n function MusicKitInstance() {\n this.musicUserToken = null;\n this.developerToken = null;\n this.playbackState = PlaybackStates.stopped;\n this.nowPlayingItem = null;\n this.shuffleMode = PlayerShuffleMode.off;\n this.isAuthorized = true;\n this._listeners = {};\n this._queue = [];\n this._currentIndex = -1;\n this.currentPlaybackTime = 0;\n this.currentPlaybackDuration = 0;\n\n // Add HTML5 audio player\n this._audio = document.createElement('audio');\n this._audio.preload = 'auto';\n document.body.appendChild(this._audio);\n }\n\n MusicKitInstance.prototype.addEventListener = function(event, callback) {\n if (!this._listeners[event]) {\n this._listeners[event] = [];\n }\n this._listeners[event].push(callback);\n };\n\n MusicKitInstance.prototype.removeEventListener = function(event, callback) {\n if (!this._listeners[event]) return;\n this._listeners[event] = this._listeners[event].filter(function(cb) {\n return cb !== callback;\n });\n };\n\n MusicKitInstance.prototype._trigger = function(event) {\n if (this._listeners[event]) {\n this._listeners[event].forEach(function(callback) {\n try { callback(); } catch (e) { console.error(e); }\n });\n }\n };\n\n MusicKitInstance.prototype.setQueue = function(options) {\n var self = this;\n return new Promise(function(resolve) {\n console.log('[MusicKit Shim] setQueue:', options);\n var preloaded = options._preloadedAttributes || {};\n\n if (options.song) {\n self._queue = [{ id: options.song, type: 'songs', attributes: preloaded[options.song] || null }];\n self._currentIndex = 0;\n } else if (options.songs) {\n self._queue = options.songs.map(function(id) {\n return { id: id, type: 'songs', attributes: preloaded[id] || null };\n });\n self._currentIndex = 0;\n } else if (options.playlist) {\n fetch('__TWIN_BASE_URL__/v1/catalog/us/playlists/' + options.playlist)\n .then(function(res) { return res.json(); })\n .then(function(data) {\n if (data && data.data && data.data[0] && data.data[0].relationships && data.data[0].relationships.tracks) {\n self._queue = data.data[0].relationships.tracks.data.map(function(track) {\n return {\n id: track.id,\n type: 'songs',\n attributes: track.attributes\n };\n });\n self._currentIndex = 0;\n self._updateNowPlaying();\n resolve();\n } else {\n self._queue = [{ id: 'mock-track-001', type: 'songs', attributes: null }];\n self._currentIndex = 0;\n self._updateNowPlaying();\n resolve();\n }\n })\n .catch(function() {\n self._queue = [{ id: 'mock-track-001', type: 'songs', attributes: null }];\n self._currentIndex = 0;\n self._updateNowPlaying();\n resolve();\n });\n return;\n }\n self._updateNowPlaying();\n resolve();\n });\n };\n\n MusicKitInstance.prototype._updateNowPlaying = function() {\n var item = this._queue[this._currentIndex];\n var self = this;\n if (item) {\n if (item.attributes) {\n this.nowPlayingItem = {\n id: item.id,\n attributes: item.attributes\n };\n if (item.attributes.audioUrl) {\n this._audio.src = item.attributes.audioUrl;\n this._audio.load();\n this.currentPlaybackTime = 0;\n this.currentPlaybackDuration = 0;\n this._trigger('playbackTimeDidChange');\n this._trigger('playbackDurationDidChange');\n }\n this._trigger('nowPlayingItemDidChange');\n } else {\n fetch('__TWIN_BASE_URL__/v1/catalog/us/songs/' + item.id)\n .then(function(res) { return res.json(); })\n .then(function(data) {\n if (data && data.data && data.data[0]) {\n // Cache attributes back on the queue item so skips don't re-fetch\n self._queue[self._currentIndex].attributes = data.data[0].attributes;\n self.nowPlayingItem = {\n id: item.id,\n attributes: data.data[0].attributes\n };\n if (data.data[0].attributes.audioUrl) {\n self._audio.src = data.data[0].attributes.audioUrl;\n self._audio.load();\n self.currentPlaybackTime = 0;\n self.currentPlaybackDuration = 0;\n self._trigger('playbackTimeDidChange');\n self._trigger('playbackDurationDidChange');\n }\n self._trigger('nowPlayingItemDidChange');\n }\n });\n return;\n }\n } else {\n this.nowPlayingItem = null;\n this._audio.src = '';\n this.currentPlaybackTime = 0;\n this.currentPlaybackDuration = 0;\n this._trigger('playbackTimeDidChange');\n this._trigger('playbackDurationDidChange');\n this._trigger('nowPlayingItemDidChange');\n }\n };\n\n MusicKitInstance.prototype.changeToMediaAtIndex = function(index) {\n var self = this;\n return new Promise(function(resolve, reject) {\n if (index >= 0 && index < self._queue.length) {\n self._currentIndex = index;\n self._updateNowPlaying();\n resolve();\n } else {\n reject(new Error('Index out of bounds'));\n }\n });\n };\n\n MusicKitInstance.prototype.changeToMediaItem = function(id) {\n var self = this;\n return new Promise(function(resolve, reject) {\n var index = self._queue.findIndex(function(item) { return item.id === id; });\n if (index !== -1) {\n self._currentIndex = index;\n self._updateNowPlaying();\n resolve();\n } else {\n reject(new Error('Item not in queue'));\n }\n });\n };\n\n MusicKitInstance.prototype.play = function() {\n var self = this;\n return new Promise(function(resolve) {\n self.playbackState = PlaybackStates.loading;\n self._trigger('playbackStateDidChange');\n self._audio.play()\n .then(function() { resolve(); })\n .catch(function(e) {\n console.error('[MusicKit Shim] play() failed:', e);\n resolve();\n });\n });\n };\n\n MusicKitInstance.prototype.pause = function() {\n var self = this;\n return new Promise(function(resolve) {\n self._audio.pause();\n resolve();\n });\n };\n\n MusicKitInstance.prototype.stop = function() {\n var self = this;\n return new Promise(function(resolve) {\n self._audio.pause();\n self._audio.currentTime = 0;\n self.playbackState = PlaybackStates.stopped;\n self._trigger('playbackStateDidChange');\n resolve();\n });\n };\n\n // Bug 5 fix: auto-play after skipping\n MusicKitInstance.prototype.skipToNextItem = function() {\n var self = this;\n return new Promise(function(resolve) {\n if (self._queue.length > 0) {\n if (self.shuffleMode === PlayerShuffleMode.songs) {\n self._currentIndex = Math.floor(Math.random() * self._queue.length);\n } else {\n self._currentIndex = (self._currentIndex + 1) % self._queue.length;\n }\n self._updateNowPlaying();\n // Auto-play after skip\n setTimeout(function() { self._audio.play().catch(function(){}); }, 150);\n }\n resolve();\n });\n };\n\n MusicKitInstance.prototype.skipToPreviousItem = function() {\n var self = this;\n return new Promise(function(resolve) {\n if (self._queue.length > 0) {\n self._currentIndex = (self._currentIndex - 1 + self._queue.length) % self._queue.length;\n self._updateNowPlaying();\n // Auto-play after skip\n setTimeout(function() { self._audio.play().catch(function(){}); }, 150);\n }\n resolve();\n });\n };\n\n window.MusicKit = {\n PlaybackStates: PlaybackStates,\n PlayerShuffleMode: PlayerShuffleMode,\n configure: function(config) {\n return new Promise(function(resolve) {\n instance = new MusicKitInstance();\n instance.developerToken = config.developerToken;\n\n // Wire audio events\n instance._audio.addEventListener('playing', function() {\n instance.playbackState = PlaybackStates.playing;\n instance._trigger('playbackStateDidChange');\n });\n instance._audio.addEventListener('pause', function() {\n if (instance.playbackState !== PlaybackStates.stopped) {\n instance.playbackState = PlaybackStates.paused;\n instance._trigger('playbackStateDidChange');\n }\n });\n instance._audio.addEventListener('ended', function() {\n instance.playbackState = PlaybackStates.ended;\n instance._trigger('playbackStateDidChange');\n });\n instance._audio.addEventListener('waiting', function() {\n instance.playbackState = PlaybackStates.waiting;\n instance._trigger('playbackStateDidChange');\n });\n instance._audio.addEventListener('timeupdate', function() {\n instance.currentPlaybackTime = instance._audio.currentTime;\n instance._trigger('playbackTimeDidChange');\n });\n instance._audio.addEventListener('durationchange', function() {\n instance.currentPlaybackDuration = instance._audio.duration;\n instance._trigger('playbackDurationDidChange');\n });\n\n resolve(instance);\n });\n },\n getInstance: function() {\n return instance;\n }\n };\n\n document.dispatchEvent(new Event('musickitloaded'));\n})();\n";
|
|
2
|
+
export declare function generatePlayerShim(baseUrl: string): string;
|
|
3
|
+
//# sourceMappingURL=player-shim.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"player-shim.d.ts","sourceRoot":"","sources":["../../src/apple-music/player-shim.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,m9TAgTzB,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1D"}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
export const SHIM_TEMPLATE = `(function() {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var PlaybackStates = {
|
|
5
|
+
none: 0,
|
|
6
|
+
loading: 1,
|
|
7
|
+
playing: 2,
|
|
8
|
+
paused: 3,
|
|
9
|
+
stopped: 4,
|
|
10
|
+
ended: 5,
|
|
11
|
+
seeking: 6,
|
|
12
|
+
waiting: 7,
|
|
13
|
+
stalled: 8,
|
|
14
|
+
completed: 9
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
var PlayerShuffleMode = {
|
|
18
|
+
off: 0,
|
|
19
|
+
songs: 1,
|
|
20
|
+
albums: 2
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
var instance = null;
|
|
24
|
+
|
|
25
|
+
function MusicKitInstance() {
|
|
26
|
+
this.musicUserToken = null;
|
|
27
|
+
this.developerToken = null;
|
|
28
|
+
this.playbackState = PlaybackStates.stopped;
|
|
29
|
+
this.nowPlayingItem = null;
|
|
30
|
+
this.shuffleMode = PlayerShuffleMode.off;
|
|
31
|
+
this.isAuthorized = true;
|
|
32
|
+
this._listeners = {};
|
|
33
|
+
this._queue = [];
|
|
34
|
+
this._currentIndex = -1;
|
|
35
|
+
this.currentPlaybackTime = 0;
|
|
36
|
+
this.currentPlaybackDuration = 0;
|
|
37
|
+
|
|
38
|
+
// Add HTML5 audio player
|
|
39
|
+
this._audio = document.createElement('audio');
|
|
40
|
+
this._audio.preload = 'auto';
|
|
41
|
+
document.body.appendChild(this._audio);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
MusicKitInstance.prototype.addEventListener = function(event, callback) {
|
|
45
|
+
if (!this._listeners[event]) {
|
|
46
|
+
this._listeners[event] = [];
|
|
47
|
+
}
|
|
48
|
+
this._listeners[event].push(callback);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
MusicKitInstance.prototype.removeEventListener = function(event, callback) {
|
|
52
|
+
if (!this._listeners[event]) return;
|
|
53
|
+
this._listeners[event] = this._listeners[event].filter(function(cb) {
|
|
54
|
+
return cb !== callback;
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
MusicKitInstance.prototype._trigger = function(event) {
|
|
59
|
+
if (this._listeners[event]) {
|
|
60
|
+
this._listeners[event].forEach(function(callback) {
|
|
61
|
+
try { callback(); } catch (e) { console.error(e); }
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
MusicKitInstance.prototype.setQueue = function(options) {
|
|
67
|
+
var self = this;
|
|
68
|
+
return new Promise(function(resolve) {
|
|
69
|
+
console.log('[MusicKit Shim] setQueue:', options);
|
|
70
|
+
var preloaded = options._preloadedAttributes || {};
|
|
71
|
+
|
|
72
|
+
if (options.song) {
|
|
73
|
+
self._queue = [{ id: options.song, type: 'songs', attributes: preloaded[options.song] || null }];
|
|
74
|
+
self._currentIndex = 0;
|
|
75
|
+
} else if (options.songs) {
|
|
76
|
+
self._queue = options.songs.map(function(id) {
|
|
77
|
+
return { id: id, type: 'songs', attributes: preloaded[id] || null };
|
|
78
|
+
});
|
|
79
|
+
self._currentIndex = 0;
|
|
80
|
+
} else if (options.playlist) {
|
|
81
|
+
fetch('__TWIN_BASE_URL__/v1/catalog/us/playlists/' + options.playlist)
|
|
82
|
+
.then(function(res) { return res.json(); })
|
|
83
|
+
.then(function(data) {
|
|
84
|
+
if (data && data.data && data.data[0] && data.data[0].relationships && data.data[0].relationships.tracks) {
|
|
85
|
+
self._queue = data.data[0].relationships.tracks.data.map(function(track) {
|
|
86
|
+
return {
|
|
87
|
+
id: track.id,
|
|
88
|
+
type: 'songs',
|
|
89
|
+
attributes: track.attributes
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
self._currentIndex = 0;
|
|
93
|
+
self._updateNowPlaying();
|
|
94
|
+
resolve();
|
|
95
|
+
} else {
|
|
96
|
+
self._queue = [{ id: 'mock-track-001', type: 'songs', attributes: null }];
|
|
97
|
+
self._currentIndex = 0;
|
|
98
|
+
self._updateNowPlaying();
|
|
99
|
+
resolve();
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
.catch(function() {
|
|
103
|
+
self._queue = [{ id: 'mock-track-001', type: 'songs', attributes: null }];
|
|
104
|
+
self._currentIndex = 0;
|
|
105
|
+
self._updateNowPlaying();
|
|
106
|
+
resolve();
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
self._updateNowPlaying();
|
|
111
|
+
resolve();
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
MusicKitInstance.prototype._updateNowPlaying = function() {
|
|
116
|
+
var item = this._queue[this._currentIndex];
|
|
117
|
+
var self = this;
|
|
118
|
+
if (item) {
|
|
119
|
+
if (item.attributes) {
|
|
120
|
+
this.nowPlayingItem = {
|
|
121
|
+
id: item.id,
|
|
122
|
+
attributes: item.attributes
|
|
123
|
+
};
|
|
124
|
+
if (item.attributes.audioUrl) {
|
|
125
|
+
this._audio.src = item.attributes.audioUrl;
|
|
126
|
+
this._audio.load();
|
|
127
|
+
this.currentPlaybackTime = 0;
|
|
128
|
+
this.currentPlaybackDuration = 0;
|
|
129
|
+
this._trigger('playbackTimeDidChange');
|
|
130
|
+
this._trigger('playbackDurationDidChange');
|
|
131
|
+
}
|
|
132
|
+
this._trigger('nowPlayingItemDidChange');
|
|
133
|
+
} else {
|
|
134
|
+
fetch('__TWIN_BASE_URL__/v1/catalog/us/songs/' + item.id)
|
|
135
|
+
.then(function(res) { return res.json(); })
|
|
136
|
+
.then(function(data) {
|
|
137
|
+
if (data && data.data && data.data[0]) {
|
|
138
|
+
// Cache attributes back on the queue item so skips don't re-fetch
|
|
139
|
+
self._queue[self._currentIndex].attributes = data.data[0].attributes;
|
|
140
|
+
self.nowPlayingItem = {
|
|
141
|
+
id: item.id,
|
|
142
|
+
attributes: data.data[0].attributes
|
|
143
|
+
};
|
|
144
|
+
if (data.data[0].attributes.audioUrl) {
|
|
145
|
+
self._audio.src = data.data[0].attributes.audioUrl;
|
|
146
|
+
self._audio.load();
|
|
147
|
+
self.currentPlaybackTime = 0;
|
|
148
|
+
self.currentPlaybackDuration = 0;
|
|
149
|
+
self._trigger('playbackTimeDidChange');
|
|
150
|
+
self._trigger('playbackDurationDidChange');
|
|
151
|
+
}
|
|
152
|
+
self._trigger('nowPlayingItemDidChange');
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
this.nowPlayingItem = null;
|
|
159
|
+
this._audio.src = '';
|
|
160
|
+
this.currentPlaybackTime = 0;
|
|
161
|
+
this.currentPlaybackDuration = 0;
|
|
162
|
+
this._trigger('playbackTimeDidChange');
|
|
163
|
+
this._trigger('playbackDurationDidChange');
|
|
164
|
+
this._trigger('nowPlayingItemDidChange');
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
MusicKitInstance.prototype.changeToMediaAtIndex = function(index) {
|
|
169
|
+
var self = this;
|
|
170
|
+
return new Promise(function(resolve, reject) {
|
|
171
|
+
if (index >= 0 && index < self._queue.length) {
|
|
172
|
+
self._currentIndex = index;
|
|
173
|
+
self._updateNowPlaying();
|
|
174
|
+
resolve();
|
|
175
|
+
} else {
|
|
176
|
+
reject(new Error('Index out of bounds'));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
MusicKitInstance.prototype.changeToMediaItem = function(id) {
|
|
182
|
+
var self = this;
|
|
183
|
+
return new Promise(function(resolve, reject) {
|
|
184
|
+
var index = self._queue.findIndex(function(item) { return item.id === id; });
|
|
185
|
+
if (index !== -1) {
|
|
186
|
+
self._currentIndex = index;
|
|
187
|
+
self._updateNowPlaying();
|
|
188
|
+
resolve();
|
|
189
|
+
} else {
|
|
190
|
+
reject(new Error('Item not in queue'));
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
MusicKitInstance.prototype.play = function() {
|
|
196
|
+
var self = this;
|
|
197
|
+
return new Promise(function(resolve) {
|
|
198
|
+
self.playbackState = PlaybackStates.loading;
|
|
199
|
+
self._trigger('playbackStateDidChange');
|
|
200
|
+
self._audio.play()
|
|
201
|
+
.then(function() { resolve(); })
|
|
202
|
+
.catch(function(e) {
|
|
203
|
+
console.error('[MusicKit Shim] play() failed:', e);
|
|
204
|
+
resolve();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
MusicKitInstance.prototype.pause = function() {
|
|
210
|
+
var self = this;
|
|
211
|
+
return new Promise(function(resolve) {
|
|
212
|
+
self._audio.pause();
|
|
213
|
+
resolve();
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
MusicKitInstance.prototype.stop = function() {
|
|
218
|
+
var self = this;
|
|
219
|
+
return new Promise(function(resolve) {
|
|
220
|
+
self._audio.pause();
|
|
221
|
+
self._audio.currentTime = 0;
|
|
222
|
+
self.playbackState = PlaybackStates.stopped;
|
|
223
|
+
self._trigger('playbackStateDidChange');
|
|
224
|
+
resolve();
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Bug 5 fix: auto-play after skipping
|
|
229
|
+
MusicKitInstance.prototype.skipToNextItem = function() {
|
|
230
|
+
var self = this;
|
|
231
|
+
return new Promise(function(resolve) {
|
|
232
|
+
if (self._queue.length > 0) {
|
|
233
|
+
if (self.shuffleMode === PlayerShuffleMode.songs) {
|
|
234
|
+
self._currentIndex = Math.floor(Math.random() * self._queue.length);
|
|
235
|
+
} else {
|
|
236
|
+
self._currentIndex = (self._currentIndex + 1) % self._queue.length;
|
|
237
|
+
}
|
|
238
|
+
self._updateNowPlaying();
|
|
239
|
+
// Auto-play after skip
|
|
240
|
+
setTimeout(function() { self._audio.play().catch(function(){}); }, 150);
|
|
241
|
+
}
|
|
242
|
+
resolve();
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
MusicKitInstance.prototype.skipToPreviousItem = function() {
|
|
247
|
+
var self = this;
|
|
248
|
+
return new Promise(function(resolve) {
|
|
249
|
+
if (self._queue.length > 0) {
|
|
250
|
+
self._currentIndex = (self._currentIndex - 1 + self._queue.length) % self._queue.length;
|
|
251
|
+
self._updateNowPlaying();
|
|
252
|
+
// Auto-play after skip
|
|
253
|
+
setTimeout(function() { self._audio.play().catch(function(){}); }, 150);
|
|
254
|
+
}
|
|
255
|
+
resolve();
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
window.MusicKit = {
|
|
260
|
+
PlaybackStates: PlaybackStates,
|
|
261
|
+
PlayerShuffleMode: PlayerShuffleMode,
|
|
262
|
+
configure: function(config) {
|
|
263
|
+
return new Promise(function(resolve) {
|
|
264
|
+
instance = new MusicKitInstance();
|
|
265
|
+
instance.developerToken = config.developerToken;
|
|
266
|
+
|
|
267
|
+
// Wire audio events
|
|
268
|
+
instance._audio.addEventListener('playing', function() {
|
|
269
|
+
instance.playbackState = PlaybackStates.playing;
|
|
270
|
+
instance._trigger('playbackStateDidChange');
|
|
271
|
+
});
|
|
272
|
+
instance._audio.addEventListener('pause', function() {
|
|
273
|
+
if (instance.playbackState !== PlaybackStates.stopped) {
|
|
274
|
+
instance.playbackState = PlaybackStates.paused;
|
|
275
|
+
instance._trigger('playbackStateDidChange');
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
instance._audio.addEventListener('ended', function() {
|
|
279
|
+
instance.playbackState = PlaybackStates.ended;
|
|
280
|
+
instance._trigger('playbackStateDidChange');
|
|
281
|
+
});
|
|
282
|
+
instance._audio.addEventListener('waiting', function() {
|
|
283
|
+
instance.playbackState = PlaybackStates.waiting;
|
|
284
|
+
instance._trigger('playbackStateDidChange');
|
|
285
|
+
});
|
|
286
|
+
instance._audio.addEventListener('timeupdate', function() {
|
|
287
|
+
instance.currentPlaybackTime = instance._audio.currentTime;
|
|
288
|
+
instance._trigger('playbackTimeDidChange');
|
|
289
|
+
});
|
|
290
|
+
instance._audio.addEventListener('durationchange', function() {
|
|
291
|
+
instance.currentPlaybackDuration = instance._audio.duration;
|
|
292
|
+
instance._trigger('playbackDurationDidChange');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
resolve(instance);
|
|
296
|
+
});
|
|
297
|
+
},
|
|
298
|
+
getInstance: function() {
|
|
299
|
+
return instance;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
document.dispatchEvent(new Event('musickitloaded'));
|
|
304
|
+
})();
|
|
305
|
+
`;
|
|
306
|
+
export function generatePlayerShim(baseUrl) {
|
|
307
|
+
return SHIM_TEMPLATE.replace(/__TWIN_BASE_URL__/g, baseUrl);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
//# sourceMappingURL=player-shim.js.map
|