@open-kingdom/shared-backend-integration-test-doubles 0.0.2-19 → 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
package/README.md
CHANGED
|
@@ -163,6 +163,45 @@ const handlers = configs.map((config) =>
|
|
|
163
163
|
);
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
+
### AppleMusicTwin
|
|
167
|
+
|
|
168
|
+
#### AppleMusicTwin Constructor
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
constructor(overrides?: Partial<AppleMusicTwinConfig>)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- `overrides.port`: Port to run the twin on (default: 9019, or from `APPLE_MUSIC_TWIN_PORT` env var; must be within 9010-9020)
|
|
175
|
+
- `overrides.externalUrl`: External URL for the twin (default: `http://localhost:{port}`)
|
|
176
|
+
|
|
177
|
+
#### AppleMusicTwin Methods
|
|
178
|
+
|
|
179
|
+
- `start()`: Start the Express server
|
|
180
|
+
- `stop()`: Stop the Express server
|
|
181
|
+
- `reset()`: Reset catalog fixtures, auth state, and clear any active error mode
|
|
182
|
+
- `getEmulatorHost()`: Get the twin URL (e.g., `http://localhost:9019`)
|
|
183
|
+
- `isHealthy()`: Check if the twin is running and responding
|
|
184
|
+
|
|
185
|
+
#### Browser-Side MSW Interception
|
|
186
|
+
|
|
187
|
+
Use `getAppleMusicMswHandlerConfigs(twinBaseUrl)` to export mock route definitions for browser-side testing frameworks like MSW:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import { http, HttpResponse } from 'msw';
|
|
191
|
+
import { getAppleMusicMswHandlerConfigs } from '@open-kingdom/shared-backend-integration-test-doubles';
|
|
192
|
+
|
|
193
|
+
const configs = getAppleMusicMswHandlerConfigs('http://localhost:9019');
|
|
194
|
+
const handlers = configs.map((config) =>
|
|
195
|
+
http.get(config.url, async () => {
|
|
196
|
+
const res = await fetch(config.proxyTo);
|
|
197
|
+
const body = await res.text();
|
|
198
|
+
return new HttpResponse(body, {
|
|
199
|
+
headers: { 'Content-Type': config.contentType },
|
|
200
|
+
});
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
```
|
|
204
|
+
|
|
166
205
|
### Exports
|
|
167
206
|
|
|
168
207
|
```typescript
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type AppleMusicErrorModeType = 'unauthorized' | 'expired-token' | 'rate-limited' | 'empty-results' | 'not-found';
|
|
2
|
+
export interface AppleMusicErrorModeConfig {
|
|
3
|
+
type: AppleMusicErrorModeType;
|
|
4
|
+
}
|
|
5
|
+
export interface AppleMusicErrorSimulationResult {
|
|
6
|
+
status: number;
|
|
7
|
+
body: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export declare class AppleMusicErrorModeManager {
|
|
10
|
+
private activeMode;
|
|
11
|
+
setMode(config: AppleMusicErrorModeConfig): void;
|
|
12
|
+
clearMode(): void;
|
|
13
|
+
getMode(): AppleMusicErrorModeConfig | null;
|
|
14
|
+
matchRequest(path: string): AppleMusicErrorSimulationResult | null;
|
|
15
|
+
reset(): void;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=apple-music-error-mode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apple-music-error-mode.d.ts","sourceRoot":"","sources":["../../src/apple-music/apple-music-error-mode.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,uBAAuB,GAC/B,cAAc,GACd,eAAe,GACf,cAAc,GACd,eAAe,GACf,WAAW,CAAC;AAEhB,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,uBAAuB,CAAC;CAC/B;AAED,MAAM,WAAW,+BAA+B;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAwBD,qBAAa,0BAA0B;IACrC,OAAO,CAAC,UAAU,CAA0C;IAE5D,OAAO,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI;IAIhD,SAAS,IAAI,IAAI;IAIjB,OAAO,IAAI,yBAAyB,GAAG,IAAI;IAI3C,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,+BAA+B,GAAG,IAAI;IA4ClE,KAAK,IAAI,IAAI;CAGd"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
function appleMusicError(status, title, detail, code = status.toString()) {
|
|
2
|
+
return {
|
|
3
|
+
status,
|
|
4
|
+
body: {
|
|
5
|
+
errors: [
|
|
6
|
+
{
|
|
7
|
+
id: `mock-error-${Date.now()}`,
|
|
8
|
+
status: status.toString(),
|
|
9
|
+
code,
|
|
10
|
+
title,
|
|
11
|
+
detail
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export class AppleMusicErrorModeManager {
|
|
18
|
+
setMode(config) {
|
|
19
|
+
this.activeMode = config;
|
|
20
|
+
}
|
|
21
|
+
clearMode() {
|
|
22
|
+
this.activeMode = null;
|
|
23
|
+
}
|
|
24
|
+
getMode() {
|
|
25
|
+
return this.activeMode;
|
|
26
|
+
}
|
|
27
|
+
matchRequest(path) {
|
|
28
|
+
if (!this.activeMode) return null;
|
|
29
|
+
if (path.startsWith('/test/')) return null;
|
|
30
|
+
switch(this.activeMode.type){
|
|
31
|
+
case 'unauthorized':
|
|
32
|
+
return appleMusicError(401, 'Unauthorized', 'The developer token is missing or invalid.');
|
|
33
|
+
case 'expired-token':
|
|
34
|
+
return appleMusicError(403, 'Forbidden', 'The user token has expired or is invalid.');
|
|
35
|
+
case 'rate-limited':
|
|
36
|
+
return appleMusicError(429, 'Too Many Requests', 'The request was rate limited.');
|
|
37
|
+
case 'empty-results':
|
|
38
|
+
if (path.includes('/search')) {
|
|
39
|
+
return {
|
|
40
|
+
status: 200,
|
|
41
|
+
body: {
|
|
42
|
+
results: {}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
case 'not-found':
|
|
48
|
+
return appleMusicError(404, 'Not Found', 'The requested resource was not found.');
|
|
49
|
+
default:
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
reset() {
|
|
54
|
+
this.activeMode = null;
|
|
55
|
+
}
|
|
56
|
+
constructor(){
|
|
57
|
+
this.activeMode = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=apple-music-error-mode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/apple-music/apple-music-error-mode.ts"],"sourcesContent":["export type AppleMusicErrorModeType =\n | 'unauthorized'\n | 'expired-token'\n | 'rate-limited'\n | 'empty-results'\n | 'not-found';\n\nexport interface AppleMusicErrorModeConfig {\n type: AppleMusicErrorModeType;\n}\n\nexport interface AppleMusicErrorSimulationResult {\n status: number;\n body: Record<string, unknown>;\n}\n\nfunction appleMusicError(\n status: number,\n title: string,\n detail: string,\n code = status.toString()\n): AppleMusicErrorSimulationResult {\n return {\n status,\n body: {\n errors: [\n {\n id: `mock-error-${Date.now()}`,\n status: status.toString(),\n code,\n title,\n detail,\n },\n ],\n },\n };\n}\n\nexport class AppleMusicErrorModeManager {\n private activeMode: AppleMusicErrorModeConfig | null = null;\n\n setMode(config: AppleMusicErrorModeConfig): void {\n this.activeMode = config;\n }\n\n clearMode(): void {\n this.activeMode = null;\n }\n\n getMode(): AppleMusicErrorModeConfig | null {\n return this.activeMode;\n }\n\n matchRequest(path: string): AppleMusicErrorSimulationResult | null {\n if (!this.activeMode) return null;\n if (path.startsWith('/test/')) return null;\n\n switch (this.activeMode.type) {\n case 'unauthorized':\n return appleMusicError(\n 401,\n 'Unauthorized',\n 'The developer token is missing or invalid.'\n );\n case 'expired-token':\n return appleMusicError(\n 403,\n 'Forbidden',\n 'The user token has expired or is invalid.'\n );\n case 'rate-limited':\n return appleMusicError(\n 429,\n 'Too Many Requests',\n 'The request was rate limited.'\n );\n case 'empty-results':\n if (path.includes('/search')) {\n return {\n status: 200,\n body: {\n results: {},\n },\n };\n }\n return null;\n case 'not-found':\n return appleMusicError(\n 404,\n 'Not Found',\n 'The requested resource was not found.'\n );\n default:\n return null;\n }\n }\n\n reset(): void {\n this.activeMode = null;\n }\n}\n"],"names":["appleMusicError","status","title","detail","code","toString","body","errors","id","Date","now","AppleMusicErrorModeManager","setMode","config","activeMode","clearMode","getMode","matchRequest","path","startsWith","type","includes","results","reset"],"mappings":"AAgBA,SAASA,gBACPC,MAAc,EACdC,KAAa,EACbC,MAAc,EACdC,OAAOH,OAAOI,QAAQ,EAAE;IAExB,OAAO;QACLJ;QACAK,MAAM;YACJC,QAAQ;gBACN;oBACEC,IAAI,CAAC,WAAW,EAAEC,KAAKC,GAAG,IAAI;oBAC9BT,QAAQA,OAAOI,QAAQ;oBACvBD;oBACAF;oBACAC;gBACF;aACD;QACH;IACF;AACF;AAEA,OAAO,MAAMQ;IAGXC,QAAQC,MAAiC,EAAQ;QAC/C,IAAI,CAACC,UAAU,GAAGD;IACpB;IAEAE,YAAkB;QAChB,IAAI,CAACD,UAAU,GAAG;IACpB;IAEAE,UAA4C;QAC1C,OAAO,IAAI,CAACF,UAAU;IACxB;IAEAG,aAAaC,IAAY,EAA0C;QACjE,IAAI,CAAC,IAAI,CAACJ,UAAU,EAAE,OAAO;QAC7B,IAAII,KAAKC,UAAU,CAAC,WAAW,OAAO;QAEtC,OAAQ,IAAI,CAACL,UAAU,CAACM,IAAI;YAC1B,KAAK;gBACH,OAAOpB,gBACL,KACA,gBACA;YAEJ,KAAK;gBACH,OAAOA,gBACL,KACA,aACA;YAEJ,KAAK;gBACH,OAAOA,gBACL,KACA,qBACA;YAEJ,KAAK;gBACH,IAAIkB,KAAKG,QAAQ,CAAC,YAAY;oBAC5B,OAAO;wBACLpB,QAAQ;wBACRK,MAAM;4BACJgB,SAAS,CAAC;wBACZ;oBACF;gBACF;gBACA,OAAO;YACT,KAAK;gBACH,OAAOtB,gBACL,KACA,aACA;YAEJ;gBACE,OAAO;QACX;IACF;IAEAuB,QAAc;QACZ,IAAI,CAACT,UAAU,GAAG;IACpB;;aA5DQA,aAA+C;;AA6DzD"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface AppleMusicTwinConfig {
|
|
2
|
+
/** Port the Apple Music twin Express server listens on */
|
|
3
|
+
port: number;
|
|
4
|
+
/** External URL consumers use to reach the twin */
|
|
5
|
+
externalUrl: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const defaultAppleMusicConfig: AppleMusicTwinConfig;
|
|
8
|
+
export declare function createAppleMusicConfig(overrides?: Partial<AppleMusicTwinConfig>): AppleMusicTwinConfig;
|
|
9
|
+
//# sourceMappingURL=apple-music-twin.config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apple-music-twin.config.d.ts","sourceRoot":"","sources":["../../src/apple-music/apple-music-twin.config.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,uBAAuB,EAAE,oBAGrC,CAAC;AAEF,wBAAgB,sBAAsB,CACpC,SAAS,GAAE,OAAO,CAAC,oBAAoB,CAAM,GAC5C,oBAAoB,CA8BtB"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
import { ConfigService, nodeEnvAdapter } from '@open-kingdom/shared-poly-util-env-config';
|
|
3
|
+
import { DEFAULT_PORTS, ENV_VARS, PORT_RANGE } from '../shared/constants.js';
|
|
4
|
+
export const defaultAppleMusicConfig = {
|
|
5
|
+
port: DEFAULT_PORTS.APPLE_MUSIC,
|
|
6
|
+
externalUrl: `http://localhost:${DEFAULT_PORTS.APPLE_MUSIC}`
|
|
7
|
+
};
|
|
8
|
+
export function createAppleMusicConfig(overrides = {}) {
|
|
9
|
+
const env = new ConfigService(nodeEnvAdapter);
|
|
10
|
+
const envPortVal = env.get(ENV_VARS.APPLE_MUSIC_TWIN_PORT);
|
|
11
|
+
const envPort = envPortVal ? Number.parseInt(envPortVal, 10) : undefined;
|
|
12
|
+
const baseConfig = _extends({}, defaultAppleMusicConfig, envPort !== undefined && !Number.isNaN(envPort) && {
|
|
13
|
+
port: envPort
|
|
14
|
+
});
|
|
15
|
+
const finalConfig = _extends({}, baseConfig, overrides);
|
|
16
|
+
if (finalConfig.port < PORT_RANGE.min || finalConfig.port > PORT_RANGE.max) {
|
|
17
|
+
throw new Error(`Apple Music twin port ${finalConfig.port} is outside the reserved range ` + `${PORT_RANGE.min}–${PORT_RANGE.max}.`);
|
|
18
|
+
}
|
|
19
|
+
if (!overrides.externalUrl && finalConfig.externalUrl === defaultAppleMusicConfig.externalUrl) {
|
|
20
|
+
finalConfig.externalUrl = `http://localhost:${finalConfig.port}`;
|
|
21
|
+
}
|
|
22
|
+
return finalConfig;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=apple-music-twin.config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/apple-music/apple-music-twin.config.ts"],"sourcesContent":["import {\n ConfigService,\n nodeEnvAdapter,\n} from '@open-kingdom/shared-poly-util-env-config';\nimport { DEFAULT_PORTS, ENV_VARS, PORT_RANGE } from '../shared/constants.js';\n\nexport interface AppleMusicTwinConfig {\n /** Port the Apple Music twin Express server listens on */\n port: number;\n /** External URL consumers use to reach the twin */\n externalUrl: string;\n}\n\nexport const defaultAppleMusicConfig: AppleMusicTwinConfig = {\n port: DEFAULT_PORTS.APPLE_MUSIC,\n externalUrl: `http://localhost:${DEFAULT_PORTS.APPLE_MUSIC}`,\n};\n\nexport function createAppleMusicConfig(\n overrides: Partial<AppleMusicTwinConfig> = {}\n): AppleMusicTwinConfig {\n const env = new ConfigService(nodeEnvAdapter);\n const envPortVal = env.get(ENV_VARS.APPLE_MUSIC_TWIN_PORT);\n const envPort = envPortVal ? Number.parseInt(envPortVal, 10) : undefined;\n\n const baseConfig: AppleMusicTwinConfig = {\n ...defaultAppleMusicConfig,\n ...(envPort !== undefined && !Number.isNaN(envPort) && { port: envPort }),\n };\n\n const finalConfig: AppleMusicTwinConfig = {\n ...baseConfig,\n ...overrides,\n };\n\n if (finalConfig.port < PORT_RANGE.min || finalConfig.port > PORT_RANGE.max) {\n throw new Error(\n `Apple Music twin port ${finalConfig.port} is outside the reserved range ` +\n `${PORT_RANGE.min}–${PORT_RANGE.max}.`\n );\n }\n\n if (\n !overrides.externalUrl &&\n finalConfig.externalUrl === defaultAppleMusicConfig.externalUrl\n ) {\n finalConfig.externalUrl = `http://localhost:${finalConfig.port}`;\n }\n\n return finalConfig;\n}\n"],"names":["ConfigService","nodeEnvAdapter","DEFAULT_PORTS","ENV_VARS","PORT_RANGE","defaultAppleMusicConfig","port","APPLE_MUSIC","externalUrl","createAppleMusicConfig","overrides","env","envPortVal","get","APPLE_MUSIC_TWIN_PORT","envPort","Number","parseInt","undefined","baseConfig","isNaN","finalConfig","min","max","Error"],"mappings":";AAAA,SACEA,aAAa,EACbC,cAAc,QACT,4CAA4C;AACnD,SAASC,aAAa,EAAEC,QAAQ,EAAEC,UAAU,QAAQ,yBAAyB;AAS7E,OAAO,MAAMC,0BAAgD;IAC3DC,MAAMJ,cAAcK,WAAW;IAC/BC,aAAa,CAAC,iBAAiB,EAAEN,cAAcK,WAAW,EAAE;AAC9D,EAAE;AAEF,OAAO,SAASE,uBACdC,YAA2C,CAAC,CAAC;IAE7C,MAAMC,MAAM,IAAIX,cAAcC;IAC9B,MAAMW,aAAaD,IAAIE,GAAG,CAACV,SAASW,qBAAqB;IACzD,MAAMC,UAAUH,aAAaI,OAAOC,QAAQ,CAACL,YAAY,MAAMM;IAE/D,MAAMC,aAAmC,aACpCd,yBACCU,YAAYG,aAAa,CAACF,OAAOI,KAAK,CAACL,YAAY;QAAET,MAAMS;IAAQ;IAGzE,MAAMM,cAAoC,aACrCF,YACAT;IAGL,IAAIW,YAAYf,IAAI,GAAGF,WAAWkB,GAAG,IAAID,YAAYf,IAAI,GAAGF,WAAWmB,GAAG,EAAE;QAC1E,MAAM,IAAIC,MACR,CAAC,sBAAsB,EAAEH,YAAYf,IAAI,CAAC,+BAA+B,CAAC,GACxE,GAAGF,WAAWkB,GAAG,CAAC,CAAC,EAAElB,WAAWmB,GAAG,CAAC,CAAC,CAAC;IAE5C;IAEA,IACE,CAACb,UAAUF,WAAW,IACtBa,YAAYb,WAAW,KAAKH,wBAAwBG,WAAW,EAC/D;QACAa,YAAYb,WAAW,GAAG,CAAC,iBAAiB,EAAEa,YAAYf,IAAI,EAAE;IAClE;IAEA,OAAOe;AACT"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AppleMusicTwinConfig } from './apple-music-twin.config.js';
|
|
2
|
+
export declare class AppleMusicTwin {
|
|
3
|
+
private readonly config;
|
|
4
|
+
private server;
|
|
5
|
+
private currentTracks;
|
|
6
|
+
private currentPlaylists;
|
|
7
|
+
private readonly errorModeManager;
|
|
8
|
+
private deviceSession;
|
|
9
|
+
private currentPlayback;
|
|
10
|
+
constructor(overrides?: Partial<AppleMusicTwinConfig>);
|
|
11
|
+
start(): Promise<void>;
|
|
12
|
+
stop(): Promise<void>;
|
|
13
|
+
reset(): Promise<void>;
|
|
14
|
+
isHealthy(): Promise<boolean>;
|
|
15
|
+
getEmulatorHost(): string;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=apple-music-twin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apple-music-twin.d.ts","sourceRoot":"","sources":["../../src/apple-music/apple-music-twin.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,oBAAoB,EACrB,MAAM,8BAA8B,CAAC;AAetC,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,aAAa,CAAgC;IACrD,OAAO,CAAC,gBAAgB,CAAmC;IAC3D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAoC;IAGrE,OAAO,CAAC,aAAa,CAMnB;IAEF,OAAO,CAAC,eAAe,CAAiB;gBAE5B,SAAS,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC;IAM/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAoStB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAarB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IActB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IAenC,eAAe,IAAI,MAAM;CAG1B"}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import express from 'express';
|
|
5
|
+
import cors from 'cors';
|
|
6
|
+
import { createAppleMusicConfig } from './apple-music-twin.config.js';
|
|
7
|
+
import { trackFixtures, playlistFixtures } from './catalog-fixtures.js';
|
|
8
|
+
import { formatSearchResponse, formatTrackResource, formatPlaylistResource } from './catalog-response.js';
|
|
9
|
+
import { generatePlayerShim } from './player-shim.js';
|
|
10
|
+
import { AppleMusicErrorModeManager } from './apple-music-error-mode.js';
|
|
11
|
+
export class AppleMusicTwin {
|
|
12
|
+
async start() {
|
|
13
|
+
if (this.server) return;
|
|
14
|
+
const app = express();
|
|
15
|
+
app.use(cors());
|
|
16
|
+
app.use(express.json());
|
|
17
|
+
// Middleware to apply simulated errors
|
|
18
|
+
app.use((req, res, next)=>{
|
|
19
|
+
const errorResult = this.errorModeManager.matchRequest(req.path);
|
|
20
|
+
if (errorResult) {
|
|
21
|
+
res.status(errorResult.status).json(errorResult.body);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
next();
|
|
25
|
+
});
|
|
26
|
+
// Browser-side script shim
|
|
27
|
+
const shimJs = generatePlayerShim(this.config.externalUrl);
|
|
28
|
+
app.get('/musickit.js', (_req, res)=>{
|
|
29
|
+
res.type('application/javascript').send(shimJs);
|
|
30
|
+
});
|
|
31
|
+
// Audio static files
|
|
32
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
let audioPath = path.join(__dirname, 'audio');
|
|
34
|
+
if (!fs.existsSync(audioPath)) {
|
|
35
|
+
audioPath = path.join(__dirname, '../audio');
|
|
36
|
+
}
|
|
37
|
+
app.use('/v1/audio', express.static(audioPath, {
|
|
38
|
+
setHeaders: (res)=>{
|
|
39
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
40
|
+
res.setHeader('Accept-Ranges', 'bytes');
|
|
41
|
+
}
|
|
42
|
+
}));
|
|
43
|
+
// Artwork static files. Catalog URLs carry Apple-style {w}x{h} placeholders
|
|
44
|
+
// (e.g. /v1/artwork/300x300/track-001.svg) that clients substitute with the
|
|
45
|
+
// size they need; the :size segment is cosmetic, files live flat on disk.
|
|
46
|
+
let artworkPath = path.join(__dirname, 'artwork');
|
|
47
|
+
if (!fs.existsSync(artworkPath)) {
|
|
48
|
+
artworkPath = path.join(__dirname, '../artwork');
|
|
49
|
+
}
|
|
50
|
+
app.get('/v1/artwork/:size/:file', (req, res)=>{
|
|
51
|
+
const file = path.basename(req.params.file);
|
|
52
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
53
|
+
res.sendFile(path.join(artworkPath, file), (err)=>{
|
|
54
|
+
if (err && !res.headersSent) {
|
|
55
|
+
res.status(404).json({
|
|
56
|
+
errors: [
|
|
57
|
+
{
|
|
58
|
+
title: 'Resource Not Found',
|
|
59
|
+
detail: `Artwork ${file} not found.`
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
// --- Apple Music Catalog API ---
|
|
67
|
+
// Search — empty term returns all tracks/playlists
|
|
68
|
+
app.get('/v1/catalog/:storefront/search', (req, res)=>{
|
|
69
|
+
const { term = '', types = 'songs' } = req.query;
|
|
70
|
+
const requestedTypes = types.split(',');
|
|
71
|
+
const matchesTerm = (val)=>term.trim() === '' || val.toLowerCase().includes(term.toLowerCase());
|
|
72
|
+
const matchedTracks = requestedTypes.includes('songs') ? this.currentTracks.filter((t)=>matchesTerm(t.name) || matchesTerm(t.artistName)) : [];
|
|
73
|
+
const matchedPlaylists = requestedTypes.includes('playlists') ? this.currentPlaylists.filter((p)=>matchesTerm(p.name)) : [];
|
|
74
|
+
const response = formatSearchResponse(matchedTracks, matchedPlaylists, this.currentTracks, this.config.externalUrl);
|
|
75
|
+
res.json(response);
|
|
76
|
+
});
|
|
77
|
+
// Get Single Song
|
|
78
|
+
app.get('/v1/catalog/:storefront/songs/:id', (req, res)=>{
|
|
79
|
+
const { id } = req.params;
|
|
80
|
+
const track = this.currentTracks.find((t)=>t.id === id);
|
|
81
|
+
if (!track) {
|
|
82
|
+
res.status(404).json({
|
|
83
|
+
errors: [
|
|
84
|
+
{
|
|
85
|
+
title: 'Resource Not Found',
|
|
86
|
+
detail: `Song with ID ${id} not found.`
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
res.json({
|
|
93
|
+
data: [
|
|
94
|
+
formatTrackResource(track, this.config.externalUrl)
|
|
95
|
+
]
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
// Get Single Playlist
|
|
99
|
+
app.get('/v1/catalog/:storefront/playlists/:id', (req, res)=>{
|
|
100
|
+
const { id } = req.params;
|
|
101
|
+
const playlist = this.currentPlaylists.find((p)=>p.id === id);
|
|
102
|
+
if (!playlist) {
|
|
103
|
+
res.status(404).json({
|
|
104
|
+
errors: [
|
|
105
|
+
{
|
|
106
|
+
title: 'Resource Not Found',
|
|
107
|
+
detail: `Playlist with ID ${id} not found.`
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
res.json({
|
|
114
|
+
data: [
|
|
115
|
+
formatPlaylistResource(playlist, this.currentTracks, this.config.externalUrl)
|
|
116
|
+
]
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
// Get Playlist Tracks
|
|
120
|
+
app.get('/v1/catalog/:storefront/playlists/:id/tracks', (req, res)=>{
|
|
121
|
+
const { id } = req.params;
|
|
122
|
+
const playlist = this.currentPlaylists.find((p)=>p.id === id);
|
|
123
|
+
if (!playlist) {
|
|
124
|
+
res.status(404).json({
|
|
125
|
+
errors: [
|
|
126
|
+
{
|
|
127
|
+
title: 'Resource Not Found',
|
|
128
|
+
detail: `Playlist with ID ${id} not found.`
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const playlistTracks = playlist.trackIds.map((tid)=>this.currentTracks.find((t)=>t.id === tid)).filter((t)=>!!t);
|
|
135
|
+
res.json({
|
|
136
|
+
data: playlistTracks.map((t)=>formatTrackResource(t, this.config.externalUrl))
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
// --- Custom Auth / Playback endpoints ---
|
|
140
|
+
// Developer Token
|
|
141
|
+
app.get('/v1/developer-token', (_req, res)=>{
|
|
142
|
+
res.json({
|
|
143
|
+
developerToken: this.deviceSession.developerToken
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
// Device Session
|
|
147
|
+
app.get('/v1/device-session', (_req, res)=>{
|
|
148
|
+
res.json(this.deviceSession);
|
|
149
|
+
});
|
|
150
|
+
// Connect User Apple Music
|
|
151
|
+
app.post('/v1/connect', (req, res)=>{
|
|
152
|
+
const { musicUserToken, storefront, mutExpires } = req.body || {};
|
|
153
|
+
if (!musicUserToken) {
|
|
154
|
+
res.status(400).json({
|
|
155
|
+
error: 'Missing musicUserToken'
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
this.deviceSession.musicUserToken = musicUserToken;
|
|
160
|
+
this.deviceSession.storefront = storefront || 'us';
|
|
161
|
+
this.deviceSession.mutExpires = mutExpires || '2027-01-01T00:00:00Z';
|
|
162
|
+
this.deviceSession.authStatus = 'authorized';
|
|
163
|
+
res.json({
|
|
164
|
+
success: true
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
// Auth Status GET
|
|
168
|
+
app.get('/v1/auth-status', (_req, res)=>{
|
|
169
|
+
res.json({
|
|
170
|
+
status: this.deviceSession.authStatus
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
// Auth Status POST
|
|
174
|
+
app.post('/v1/auth-status', (req, res)=>{
|
|
175
|
+
const { status } = req.body || {};
|
|
176
|
+
if (status) {
|
|
177
|
+
this.deviceSession.authStatus = status;
|
|
178
|
+
}
|
|
179
|
+
res.json({
|
|
180
|
+
success: true
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
// Playback Telemetry Report
|
|
184
|
+
app.post('/v1/playback', (req, res)=>{
|
|
185
|
+
this.currentPlayback = req.body;
|
|
186
|
+
console.log('[AppleMusicTwin] Playback telemetry received:', this.currentPlayback);
|
|
187
|
+
res.json({
|
|
188
|
+
success: true
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
// --- Test Control Endpoints ---
|
|
192
|
+
// Health
|
|
193
|
+
app.get('/test/apple-music/health', (_req, res)=>{
|
|
194
|
+
res.json({
|
|
195
|
+
status: 'ok'
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
// Reset
|
|
199
|
+
app.post('/test/apple-music/reset', async (_req, res)=>{
|
|
200
|
+
await this.reset();
|
|
201
|
+
res.json({
|
|
202
|
+
success: true
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
// Runtime fixtures override
|
|
206
|
+
app.put('/test/apple-music/fixtures', (req, res)=>{
|
|
207
|
+
const { tracks, playlists } = req.body || {};
|
|
208
|
+
if (tracks) this.currentTracks = tracks;
|
|
209
|
+
if (playlists) this.currentPlaylists = playlists;
|
|
210
|
+
res.json({
|
|
211
|
+
success: true,
|
|
212
|
+
trackCount: this.currentTracks.length,
|
|
213
|
+
playlistCount: this.currentPlaylists.length
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
// Error control
|
|
217
|
+
app.post('/test/apple-music/error-mode', (req, res)=>{
|
|
218
|
+
const { mode } = req.body || {};
|
|
219
|
+
if (!mode) {
|
|
220
|
+
res.status(400).json({
|
|
221
|
+
error: 'Missing "mode" field'
|
|
222
|
+
});
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
this.errorModeManager.setMode({
|
|
226
|
+
type: mode
|
|
227
|
+
});
|
|
228
|
+
res.json({
|
|
229
|
+
success: true,
|
|
230
|
+
mode
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
app.delete('/test/apple-music/error-mode', (_req, res)=>{
|
|
234
|
+
this.errorModeManager.clearMode();
|
|
235
|
+
res.json({
|
|
236
|
+
success: true,
|
|
237
|
+
mode: null
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
app.get('/test/apple-music/error-mode', (_req, res)=>{
|
|
241
|
+
var _ref;
|
|
242
|
+
const mode = this.errorModeManager.getMode();
|
|
243
|
+
res.json({
|
|
244
|
+
active: mode !== null,
|
|
245
|
+
mode: (_ref = mode == null ? void 0 : mode.type) != null ? _ref : null
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
return new Promise((resolve, reject)=>{
|
|
249
|
+
this.server = app.listen(this.config.port, ()=>{
|
|
250
|
+
console.log(`[AppleMusicTwin] Express server listening on port ${this.config.port}`);
|
|
251
|
+
resolve();
|
|
252
|
+
});
|
|
253
|
+
this.server.on('error', reject);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
async stop() {
|
|
257
|
+
const server = this.server;
|
|
258
|
+
if (!server) return;
|
|
259
|
+
return new Promise((resolve, reject)=>{
|
|
260
|
+
server.close((err)=>{
|
|
261
|
+
if (err) return reject(err);
|
|
262
|
+
this.server = null;
|
|
263
|
+
console.log('[AppleMusicTwin] Express server stopped');
|
|
264
|
+
resolve();
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
async reset() {
|
|
269
|
+
this.errorModeManager.reset();
|
|
270
|
+
this.currentTracks = [
|
|
271
|
+
...trackFixtures
|
|
272
|
+
];
|
|
273
|
+
this.currentPlaylists = [
|
|
274
|
+
...playlistFixtures
|
|
275
|
+
];
|
|
276
|
+
this.deviceSession = {
|
|
277
|
+
developerToken: 'mock-developer-token-jwt',
|
|
278
|
+
musicUserToken: 'mock-music-user-token',
|
|
279
|
+
storefront: 'us',
|
|
280
|
+
mutExpires: '2027-01-01T00:00:00Z',
|
|
281
|
+
authStatus: 'authorized'
|
|
282
|
+
};
|
|
283
|
+
this.currentPlayback = null;
|
|
284
|
+
}
|
|
285
|
+
async isHealthy() {
|
|
286
|
+
try {
|
|
287
|
+
const res = await fetch(`${this.config.externalUrl}/test/apple-music/health`, {
|
|
288
|
+
headers: {
|
|
289
|
+
Connection: 'close'
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
await res.text();
|
|
293
|
+
return res.ok;
|
|
294
|
+
} catch (unused) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
getEmulatorHost() {
|
|
299
|
+
return this.config.externalUrl;
|
|
300
|
+
}
|
|
301
|
+
constructor(overrides){
|
|
302
|
+
this.server = null;
|
|
303
|
+
this.currentTracks = [];
|
|
304
|
+
this.currentPlaylists = [];
|
|
305
|
+
this.errorModeManager = new AppleMusicErrorModeManager();
|
|
306
|
+
// Stateful sessions
|
|
307
|
+
this.deviceSession = {
|
|
308
|
+
developerToken: 'mock-developer-token-jwt',
|
|
309
|
+
musicUserToken: 'mock-music-user-token',
|
|
310
|
+
storefront: 'us',
|
|
311
|
+
mutExpires: '2027-01-01T00:00:00Z',
|
|
312
|
+
authStatus: 'authorized'
|
|
313
|
+
};
|
|
314
|
+
this.currentPlayback = null;
|
|
315
|
+
this.config = createAppleMusicConfig(overrides);
|
|
316
|
+
this.currentTracks = [
|
|
317
|
+
...trackFixtures
|
|
318
|
+
];
|
|
319
|
+
this.currentPlaylists = [
|
|
320
|
+
...playlistFixtures
|
|
321
|
+
];
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
//# sourceMappingURL=apple-music-twin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/apple-music/apple-music-twin.ts"],"sourcesContent":["import http from 'node:http';\nimport path from 'node:path';\nimport fs from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport express, { Request, Response } from 'express';\nimport cors from 'cors';\nimport {\n createAppleMusicConfig,\n AppleMusicTwinConfig,\n} from './apple-music-twin.config.js';\nimport {\n trackFixtures,\n playlistFixtures,\n AppleMusicTrackFixture,\n AppleMusicPlaylistFixture,\n} from './catalog-fixtures.js';\nimport {\n formatSearchResponse,\n formatTrackResource,\n formatPlaylistResource,\n} from './catalog-response.js';\nimport { generatePlayerShim } from './player-shim.js';\nimport { AppleMusicErrorModeManager } from './apple-music-error-mode.js';\n\nexport class AppleMusicTwin {\n private readonly config: AppleMusicTwinConfig;\n private server: http.Server | null = null;\n private currentTracks: AppleMusicTrackFixture[] = [];\n private currentPlaylists: AppleMusicPlaylistFixture[] = [];\n private readonly errorModeManager = new AppleMusicErrorModeManager();\n\n // Stateful sessions\n private deviceSession = {\n developerToken: 'mock-developer-token-jwt',\n musicUserToken: 'mock-music-user-token',\n storefront: 'us',\n mutExpires: '2027-01-01T00:00:00Z',\n authStatus: 'authorized' as 'authorized' | 'expiring-soon' | 'expired',\n };\n\n private currentPlayback: unknown = null;\n\n constructor(overrides?: Partial<AppleMusicTwinConfig>) {\n this.config = createAppleMusicConfig(overrides);\n this.currentTracks = [...trackFixtures];\n this.currentPlaylists = [...playlistFixtures];\n }\n\n async start(): Promise<void> {\n if (this.server) return;\n\n const app = express();\n app.use(cors());\n app.use(express.json());\n\n // Middleware to apply simulated errors\n app.use((req, res, next) => {\n const errorResult = this.errorModeManager.matchRequest(req.path);\n if (errorResult) {\n res.status(errorResult.status).json(errorResult.body);\n return;\n }\n next();\n });\n\n // Browser-side script shim\n const shimJs = generatePlayerShim(this.config.externalUrl);\n app.get('/musickit.js', (_req: Request, res: Response) => {\n res.type('application/javascript').send(shimJs);\n });\n\n // Audio static files\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n let audioPath = path.join(__dirname, 'audio');\n if (!fs.existsSync(audioPath)) {\n audioPath = path.join(__dirname, '../audio');\n }\n app.use(\n '/v1/audio',\n express.static(audioPath, {\n setHeaders: (res) => {\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Accept-Ranges', 'bytes');\n },\n })\n );\n\n // Artwork static files. Catalog URLs carry Apple-style {w}x{h} placeholders\n // (e.g. /v1/artwork/300x300/track-001.svg) that clients substitute with the\n // size they need; the :size segment is cosmetic, files live flat on disk.\n let artworkPath = path.join(__dirname, 'artwork');\n if (!fs.existsSync(artworkPath)) {\n artworkPath = path.join(__dirname, '../artwork');\n }\n app.get(\n '/v1/artwork/:size/:file',\n (req: Request<{ size: string; file: string }>, res: Response) => {\n const file = path.basename(req.params.file);\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.sendFile(path.join(artworkPath, file), (err) => {\n if (err && !res.headersSent) {\n res.status(404).json({\n errors: [\n {\n title: 'Resource Not Found',\n detail: `Artwork ${file} not found.`,\n },\n ],\n });\n }\n });\n }\n );\n\n // --- Apple Music Catalog API ---\n\n // Search — empty term returns all tracks/playlists\n app.get('/v1/catalog/:storefront/search', (req: Request, res: Response) => {\n const { term = '', types = 'songs' } = req.query as Record<\n string,\n string\n >;\n const requestedTypes = types.split(',');\n\n const matchesTerm = (val: string) =>\n term.trim() === '' || val.toLowerCase().includes(term.toLowerCase());\n\n const matchedTracks = requestedTypes.includes('songs')\n ? this.currentTracks.filter(\n (t) => matchesTerm(t.name) || matchesTerm(t.artistName)\n )\n : [];\n\n const matchedPlaylists = requestedTypes.includes('playlists')\n ? this.currentPlaylists.filter((p) => matchesTerm(p.name))\n : [];\n\n const response = formatSearchResponse(\n matchedTracks,\n matchedPlaylists,\n this.currentTracks,\n this.config.externalUrl\n );\n res.json(response);\n });\n\n // Get Single Song\n app.get(\n '/v1/catalog/:storefront/songs/:id',\n (req: Request, res: Response) => {\n const { id } = req.params;\n const track = this.currentTracks.find((t) => t.id === id);\n if (!track) {\n res.status(404).json({\n errors: [\n {\n title: 'Resource Not Found',\n detail: `Song with ID ${id} not found.`,\n },\n ],\n });\n return;\n }\n res.json({\n data: [formatTrackResource(track, this.config.externalUrl)],\n });\n }\n );\n\n // Get Single Playlist\n app.get(\n '/v1/catalog/:storefront/playlists/:id',\n (req: Request, res: Response) => {\n const { id } = req.params;\n const playlist = this.currentPlaylists.find((p) => p.id === id);\n if (!playlist) {\n res.status(404).json({\n errors: [\n {\n title: 'Resource Not Found',\n detail: `Playlist with ID ${id} not found.`,\n },\n ],\n });\n return;\n }\n res.json({\n data: [\n formatPlaylistResource(\n playlist,\n this.currentTracks,\n this.config.externalUrl\n ),\n ],\n });\n }\n );\n\n // Get Playlist Tracks\n app.get(\n '/v1/catalog/:storefront/playlists/:id/tracks',\n (req: Request, res: Response) => {\n const { id } = req.params;\n const playlist = this.currentPlaylists.find((p) => p.id === id);\n if (!playlist) {\n res.status(404).json({\n errors: [\n {\n title: 'Resource Not Found',\n detail: `Playlist with ID ${id} not found.`,\n },\n ],\n });\n return;\n }\n const playlistTracks = playlist.trackIds\n .map((tid) => this.currentTracks.find((t) => t.id === tid))\n .filter((t): t is AppleMusicTrackFixture => !!t);\n\n res.json({\n data: playlistTracks.map((t) =>\n formatTrackResource(t, this.config.externalUrl)\n ),\n });\n }\n );\n\n // --- Custom Auth / Playback endpoints ---\n\n // Developer Token\n app.get('/v1/developer-token', (_req: Request, res: Response) => {\n res.json({ developerToken: this.deviceSession.developerToken });\n });\n\n // Device Session\n app.get('/v1/device-session', (_req: Request, res: Response) => {\n res.json(this.deviceSession);\n });\n\n // Connect User Apple Music\n app.post('/v1/connect', (req: Request, res: Response) => {\n const { musicUserToken, storefront, mutExpires } = req.body || {};\n if (!musicUserToken) {\n res.status(400).json({ error: 'Missing musicUserToken' });\n return;\n }\n this.deviceSession.musicUserToken = musicUserToken;\n this.deviceSession.storefront = storefront || 'us';\n this.deviceSession.mutExpires = mutExpires || '2027-01-01T00:00:00Z';\n this.deviceSession.authStatus = 'authorized';\n res.json({ success: true });\n });\n\n // Auth Status GET\n app.get('/v1/auth-status', (_req: Request, res: Response) => {\n res.json({ status: this.deviceSession.authStatus });\n });\n\n // Auth Status POST\n app.post('/v1/auth-status', (req: Request, res: Response) => {\n const { status } = req.body || {};\n if (status) {\n this.deviceSession.authStatus = status;\n }\n res.json({ success: true });\n });\n\n // Playback Telemetry Report\n app.post('/v1/playback', (req: Request, res: Response) => {\n this.currentPlayback = req.body;\n console.log(\n '[AppleMusicTwin] Playback telemetry received:',\n this.currentPlayback\n );\n res.json({ success: true });\n });\n\n // --- Test Control Endpoints ---\n\n // Health\n app.get('/test/apple-music/health', (_req: Request, res: Response) => {\n res.json({ status: 'ok' });\n });\n\n // Reset\n app.post(\n '/test/apple-music/reset',\n async (_req: Request, res: Response) => {\n await this.reset();\n res.json({ success: true });\n }\n );\n\n // Runtime fixtures override\n app.put('/test/apple-music/fixtures', (req: Request, res: Response) => {\n const { tracks, playlists } = req.body || {};\n if (tracks) this.currentTracks = tracks;\n if (playlists) this.currentPlaylists = playlists;\n res.json({\n success: true,\n trackCount: this.currentTracks.length,\n playlistCount: this.currentPlaylists.length,\n });\n });\n\n // Error control\n app.post('/test/apple-music/error-mode', (req: Request, res: Response) => {\n const { mode } = req.body || {};\n if (!mode) {\n res.status(400).json({ error: 'Missing \"mode\" field' });\n return;\n }\n this.errorModeManager.setMode({ type: mode });\n res.json({ success: true, mode });\n });\n\n app.delete(\n '/test/apple-music/error-mode',\n (_req: Request, res: Response) => {\n this.errorModeManager.clearMode();\n res.json({ success: true, mode: null });\n }\n );\n\n app.get('/test/apple-music/error-mode', (_req: Request, res: Response) => {\n const mode = this.errorModeManager.getMode();\n res.json({ active: mode !== null, mode: mode?.type ?? null });\n });\n\n return new Promise<void>((resolve, reject) => {\n this.server = app.listen(this.config.port, () => {\n console.log(\n `[AppleMusicTwin] Express server listening on port ${this.config.port}`\n );\n resolve();\n });\n this.server.on('error', reject);\n });\n }\n\n async stop(): Promise<void> {\n const server = this.server;\n if (!server) return;\n return new Promise<void>((resolve, reject) => {\n server.close((err) => {\n if (err) return reject(err);\n this.server = null;\n console.log('[AppleMusicTwin] Express server stopped');\n resolve();\n });\n });\n }\n\n async reset(): Promise<void> {\n this.errorModeManager.reset();\n this.currentTracks = [...trackFixtures];\n this.currentPlaylists = [...playlistFixtures];\n this.deviceSession = {\n developerToken: 'mock-developer-token-jwt',\n musicUserToken: 'mock-music-user-token',\n storefront: 'us',\n mutExpires: '2027-01-01T00:00:00Z',\n authStatus: 'authorized',\n };\n this.currentPlayback = null;\n }\n\n async isHealthy(): Promise<boolean> {\n try {\n const res = await fetch(\n `${this.config.externalUrl}/test/apple-music/health`,\n {\n headers: { Connection: 'close' },\n }\n );\n await res.text();\n return res.ok;\n } catch {\n return false;\n }\n }\n\n getEmulatorHost(): string {\n return this.config.externalUrl;\n }\n}\n"],"names":["path","fs","fileURLToPath","express","cors","createAppleMusicConfig","trackFixtures","playlistFixtures","formatSearchResponse","formatTrackResource","formatPlaylistResource","generatePlayerShim","AppleMusicErrorModeManager","AppleMusicTwin","start","server","app","use","json","req","res","next","errorResult","errorModeManager","matchRequest","status","body","shimJs","config","externalUrl","get","_req","type","send","__dirname","dirname","url","audioPath","join","existsSync","static","setHeaders","setHeader","artworkPath","file","basename","params","sendFile","err","headersSent","errors","title","detail","term","types","query","requestedTypes","split","matchesTerm","val","trim","toLowerCase","includes","matchedTracks","currentTracks","filter","t","name","artistName","matchedPlaylists","currentPlaylists","p","response","id","track","find","data","playlist","playlistTracks","trackIds","map","tid","developerToken","deviceSession","post","musicUserToken","storefront","mutExpires","error","authStatus","success","currentPlayback","console","log","reset","put","tracks","playlists","trackCount","length","playlistCount","mode","setMode","delete","clearMode","getMode","active","Promise","resolve","reject","listen","port","on","stop","close","isHealthy","fetch","headers","Connection","text","ok","getEmulatorHost","overrides"],"mappings":"AACA,OAAOA,UAAU,YAAY;AAC7B,OAAOC,QAAQ,UAAU;AACzB,SAASC,aAAa,QAAQ,WAAW;AACzC,OAAOC,aAAoC,UAAU;AACrD,OAAOC,UAAU,OAAO;AACxB,SACEC,sBAAsB,QAEjB,+BAA+B;AACtC,SACEC,aAAa,EACbC,gBAAgB,QAGX,wBAAwB;AAC/B,SACEC,oBAAoB,EACpBC,mBAAmB,EACnBC,sBAAsB,QACjB,wBAAwB;AAC/B,SAASC,kBAAkB,QAAQ,mBAAmB;AACtD,SAASC,0BAA0B,QAAQ,8BAA8B;AAEzE,OAAO,MAAMC;IAwBX,MAAMC,QAAuB;QAC3B,IAAI,IAAI,CAACC,MAAM,EAAE;QAEjB,MAAMC,MAAMb;QACZa,IAAIC,GAAG,CAACb;QACRY,IAAIC,GAAG,CAACd,QAAQe,IAAI;QAEpB,uCAAuC;QACvCF,IAAIC,GAAG,CAAC,CAACE,KAAKC,KAAKC;YACjB,MAAMC,cAAc,IAAI,CAACC,gBAAgB,CAACC,YAAY,CAACL,IAAInB,IAAI;YAC/D,IAAIsB,aAAa;gBACfF,IAAIK,MAAM,CAACH,YAAYG,MAAM,EAAEP,IAAI,CAACI,YAAYI,IAAI;gBACpD;YACF;YACAL;QACF;QAEA,2BAA2B;QAC3B,MAAMM,SAAShB,mBAAmB,IAAI,CAACiB,MAAM,CAACC,WAAW;QACzDb,IAAIc,GAAG,CAAC,gBAAgB,CAACC,MAAeX;YACtCA,IAAIY,IAAI,CAAC,0BAA0BC,IAAI,CAACN;QAC1C;QAEA,qBAAqB;QACrB,MAAMO,YAAYlC,KAAKmC,OAAO,CAACjC,cAAc,YAAYkC,GAAG;QAC5D,IAAIC,YAAYrC,KAAKsC,IAAI,CAACJ,WAAW;QACrC,IAAI,CAACjC,GAAGsC,UAAU,CAACF,YAAY;YAC7BA,YAAYrC,KAAKsC,IAAI,CAACJ,WAAW;QACnC;QACAlB,IAAIC,GAAG,CACL,aACAd,QAAQqC,MAAM,CAACH,WAAW;YACxBI,YAAY,CAACrB;gBACXA,IAAIsB,SAAS,CAAC,+BAA+B;gBAC7CtB,IAAIsB,SAAS,CAAC,iBAAiB;YACjC;QACF;QAGF,4EAA4E;QAC5E,4EAA4E;QAC5E,0EAA0E;QAC1E,IAAIC,cAAc3C,KAAKsC,IAAI,CAACJ,WAAW;QACvC,IAAI,CAACjC,GAAGsC,UAAU,CAACI,cAAc;YAC/BA,cAAc3C,KAAKsC,IAAI,CAACJ,WAAW;QACrC;QACAlB,IAAIc,GAAG,CACL,2BACA,CAACX,KAA8CC;YAC7C,MAAMwB,OAAO5C,KAAK6C,QAAQ,CAAC1B,IAAI2B,MAAM,CAACF,IAAI;YAC1CxB,IAAIsB,SAAS,CAAC,+BAA+B;YAC7CtB,IAAI2B,QAAQ,CAAC/C,KAAKsC,IAAI,CAACK,aAAaC,OAAO,CAACI;gBAC1C,IAAIA,OAAO,CAAC5B,IAAI6B,WAAW,EAAE;oBAC3B7B,IAAIK,MAAM,CAAC,KAAKP,IAAI,CAAC;wBACnBgC,QAAQ;4BACN;gCACEC,OAAO;gCACPC,QAAQ,CAAC,QAAQ,EAAER,KAAK,WAAW,CAAC;4BACtC;yBACD;oBACH;gBACF;YACF;QACF;QAGF,kCAAkC;QAElC,mDAAmD;QACnD5B,IAAIc,GAAG,CAAC,kCAAkC,CAACX,KAAcC;YACvD,MAAM,EAAEiC,OAAO,EAAE,EAAEC,QAAQ,OAAO,EAAE,GAAGnC,IAAIoC,KAAK;YAIhD,MAAMC,iBAAiBF,MAAMG,KAAK,CAAC;YAEnC,MAAMC,cAAc,CAACC,MACnBN,KAAKO,IAAI,OAAO,MAAMD,IAAIE,WAAW,GAAGC,QAAQ,CAACT,KAAKQ,WAAW;YAEnE,MAAME,gBAAgBP,eAAeM,QAAQ,CAAC,WAC1C,IAAI,CAACE,aAAa,CAACC,MAAM,CACvB,CAACC,IAAMR,YAAYQ,EAAEC,IAAI,KAAKT,YAAYQ,EAAEE,UAAU,KAExD,EAAE;YAEN,MAAMC,mBAAmBb,eAAeM,QAAQ,CAAC,eAC7C,IAAI,CAACQ,gBAAgB,CAACL,MAAM,CAAC,CAACM,IAAMb,YAAYa,EAAEJ,IAAI,KACtD,EAAE;YAEN,MAAMK,WAAWhE,qBACfuD,eACAM,kBACA,IAAI,CAACL,aAAa,EAClB,IAAI,CAACpC,MAAM,CAACC,WAAW;YAEzBT,IAAIF,IAAI,CAACsD;QACX;QAEA,kBAAkB;QAClBxD,IAAIc,GAAG,CACL,qCACA,CAACX,KAAcC;YACb,MAAM,EAAEqD,EAAE,EAAE,GAAGtD,IAAI2B,MAAM;YACzB,MAAM4B,QAAQ,IAAI,CAACV,aAAa,CAACW,IAAI,CAAC,CAACT,IAAMA,EAAEO,EAAE,KAAKA;YACtD,IAAI,CAACC,OAAO;gBACVtD,IAAIK,MAAM,CAAC,KAAKP,IAAI,CAAC;oBACnBgC,QAAQ;wBACN;4BACEC,OAAO;4BACPC,QAAQ,CAAC,aAAa,EAAEqB,GAAG,WAAW,CAAC;wBACzC;qBACD;gBACH;gBACA;YACF;YACArD,IAAIF,IAAI,CAAC;gBACP0D,MAAM;oBAACnE,oBAAoBiE,OAAO,IAAI,CAAC9C,MAAM,CAACC,WAAW;iBAAE;YAC7D;QACF;QAGF,sBAAsB;QACtBb,IAAIc,GAAG,CACL,yCACA,CAACX,KAAcC;YACb,MAAM,EAAEqD,EAAE,EAAE,GAAGtD,IAAI2B,MAAM;YACzB,MAAM+B,WAAW,IAAI,CAACP,gBAAgB,CAACK,IAAI,CAAC,CAACJ,IAAMA,EAAEE,EAAE,KAAKA;YAC5D,IAAI,CAACI,UAAU;gBACbzD,IAAIK,MAAM,CAAC,KAAKP,IAAI,CAAC;oBACnBgC,QAAQ;wBACN;4BACEC,OAAO;4BACPC,QAAQ,CAAC,iBAAiB,EAAEqB,GAAG,WAAW,CAAC;wBAC7C;qBACD;gBACH;gBACA;YACF;YACArD,IAAIF,IAAI,CAAC;gBACP0D,MAAM;oBACJlE,uBACEmE,UACA,IAAI,CAACb,aAAa,EAClB,IAAI,CAACpC,MAAM,CAACC,WAAW;iBAE1B;YACH;QACF;QAGF,sBAAsB;QACtBb,IAAIc,GAAG,CACL,gDACA,CAACX,KAAcC;YACb,MAAM,EAAEqD,EAAE,EAAE,GAAGtD,IAAI2B,MAAM;YACzB,MAAM+B,WAAW,IAAI,CAACP,gBAAgB,CAACK,IAAI,CAAC,CAACJ,IAAMA,EAAEE,EAAE,KAAKA;YAC5D,IAAI,CAACI,UAAU;gBACbzD,IAAIK,MAAM,CAAC,KAAKP,IAAI,CAAC;oBACnBgC,QAAQ;wBACN;4BACEC,OAAO;4BACPC,QAAQ,CAAC,iBAAiB,EAAEqB,GAAG,WAAW,CAAC;wBAC7C;qBACD;gBACH;gBACA;YACF;YACA,MAAMK,iBAAiBD,SAASE,QAAQ,CACrCC,GAAG,CAAC,CAACC,MAAQ,IAAI,CAACjB,aAAa,CAACW,IAAI,CAAC,CAACT,IAAMA,EAAEO,EAAE,KAAKQ,MACrDhB,MAAM,CAAC,CAACC,IAAmC,CAAC,CAACA;YAEhD9C,IAAIF,IAAI,CAAC;gBACP0D,MAAME,eAAeE,GAAG,CAAC,CAACd,IACxBzD,oBAAoByD,GAAG,IAAI,CAACtC,MAAM,CAACC,WAAW;YAElD;QACF;QAGF,2CAA2C;QAE3C,kBAAkB;QAClBb,IAAIc,GAAG,CAAC,uBAAuB,CAACC,MAAeX;YAC7CA,IAAIF,IAAI,CAAC;gBAAEgE,gBAAgB,IAAI,CAACC,aAAa,CAACD,cAAc;YAAC;QAC/D;QAEA,iBAAiB;QACjBlE,IAAIc,GAAG,CAAC,sBAAsB,CAACC,MAAeX;YAC5CA,IAAIF,IAAI,CAAC,IAAI,CAACiE,aAAa;QAC7B;QAEA,2BAA2B;QAC3BnE,IAAIoE,IAAI,CAAC,eAAe,CAACjE,KAAcC;YACrC,MAAM,EAAEiE,cAAc,EAAEC,UAAU,EAAEC,UAAU,EAAE,GAAGpE,IAAIO,IAAI,IAAI,CAAC;YAChE,IAAI,CAAC2D,gBAAgB;gBACnBjE,IAAIK,MAAM,CAAC,KAAKP,IAAI,CAAC;oBAAEsE,OAAO;gBAAyB;gBACvD;YACF;YACA,IAAI,CAACL,aAAa,CAACE,cAAc,GAAGA;YACpC,IAAI,CAACF,aAAa,CAACG,UAAU,GAAGA,cAAc;YAC9C,IAAI,CAACH,aAAa,CAACI,UAAU,GAAGA,cAAc;YAC9C,IAAI,CAACJ,aAAa,CAACM,UAAU,GAAG;YAChCrE,IAAIF,IAAI,CAAC;gBAAEwE,SAAS;YAAK;QAC3B;QAEA,kBAAkB;QAClB1E,IAAIc,GAAG,CAAC,mBAAmB,CAACC,MAAeX;YACzCA,IAAIF,IAAI,CAAC;gBAAEO,QAAQ,IAAI,CAAC0D,aAAa,CAACM,UAAU;YAAC;QACnD;QAEA,mBAAmB;QACnBzE,IAAIoE,IAAI,CAAC,mBAAmB,CAACjE,KAAcC;YACzC,MAAM,EAAEK,MAAM,EAAE,GAAGN,IAAIO,IAAI,IAAI,CAAC;YAChC,IAAID,QAAQ;gBACV,IAAI,CAAC0D,aAAa,CAACM,UAAU,GAAGhE;YAClC;YACAL,IAAIF,IAAI,CAAC;gBAAEwE,SAAS;YAAK;QAC3B;QAEA,4BAA4B;QAC5B1E,IAAIoE,IAAI,CAAC,gBAAgB,CAACjE,KAAcC;YACtC,IAAI,CAACuE,eAAe,GAAGxE,IAAIO,IAAI;YAC/BkE,QAAQC,GAAG,CACT,iDACA,IAAI,CAACF,eAAe;YAEtBvE,IAAIF,IAAI,CAAC;gBAAEwE,SAAS;YAAK;QAC3B;QAEA,iCAAiC;QAEjC,SAAS;QACT1E,IAAIc,GAAG,CAAC,4BAA4B,CAACC,MAAeX;YAClDA,IAAIF,IAAI,CAAC;gBAAEO,QAAQ;YAAK;QAC1B;QAEA,QAAQ;QACRT,IAAIoE,IAAI,CACN,2BACA,OAAOrD,MAAeX;YACpB,MAAM,IAAI,CAAC0E,KAAK;YAChB1E,IAAIF,IAAI,CAAC;gBAAEwE,SAAS;YAAK;QAC3B;QAGF,4BAA4B;QAC5B1E,IAAI+E,GAAG,CAAC,8BAA8B,CAAC5E,KAAcC;YACnD,MAAM,EAAE4E,MAAM,EAAEC,SAAS,EAAE,GAAG9E,IAAIO,IAAI,IAAI,CAAC;YAC3C,IAAIsE,QAAQ,IAAI,CAAChC,aAAa,GAAGgC;YACjC,IAAIC,WAAW,IAAI,CAAC3B,gBAAgB,GAAG2B;YACvC7E,IAAIF,IAAI,CAAC;gBACPwE,SAAS;gBACTQ,YAAY,IAAI,CAAClC,aAAa,CAACmC,MAAM;gBACrCC,eAAe,IAAI,CAAC9B,gBAAgB,CAAC6B,MAAM;YAC7C;QACF;QAEA,gBAAgB;QAChBnF,IAAIoE,IAAI,CAAC,gCAAgC,CAACjE,KAAcC;YACtD,MAAM,EAAEiF,IAAI,EAAE,GAAGlF,IAAIO,IAAI,IAAI,CAAC;YAC9B,IAAI,CAAC2E,MAAM;gBACTjF,IAAIK,MAAM,CAAC,KAAKP,IAAI,CAAC;oBAAEsE,OAAO;gBAAuB;gBACrD;YACF;YACA,IAAI,CAACjE,gBAAgB,CAAC+E,OAAO,CAAC;gBAAEtE,MAAMqE;YAAK;YAC3CjF,IAAIF,IAAI,CAAC;gBAAEwE,SAAS;gBAAMW;YAAK;QACjC;QAEArF,IAAIuF,MAAM,CACR,gCACA,CAACxE,MAAeX;YACd,IAAI,CAACG,gBAAgB,CAACiF,SAAS;YAC/BpF,IAAIF,IAAI,CAAC;gBAAEwE,SAAS;gBAAMW,MAAM;YAAK;QACvC;QAGFrF,IAAIc,GAAG,CAAC,gCAAgC,CAACC,MAAeX;;YACtD,MAAMiF,OAAO,IAAI,CAAC9E,gBAAgB,CAACkF,OAAO;YAC1CrF,IAAIF,IAAI,CAAC;gBAAEwF,QAAQL,SAAS;gBAAMA,IAAI,UAAEA,wBAAAA,KAAMrE,IAAI,mBAAI;YAAK;QAC7D;QAEA,OAAO,IAAI2E,QAAc,CAACC,SAASC;YACjC,IAAI,CAAC9F,MAAM,GAAGC,IAAI8F,MAAM,CAAC,IAAI,CAAClF,MAAM,CAACmF,IAAI,EAAE;gBACzCnB,QAAQC,GAAG,CACT,CAAC,kDAAkD,EAAE,IAAI,CAACjE,MAAM,CAACmF,IAAI,EAAE;gBAEzEH;YACF;YACA,IAAI,CAAC7F,MAAM,CAACiG,EAAE,CAAC,SAASH;QAC1B;IACF;IAEA,MAAMI,OAAsB;QAC1B,MAAMlG,SAAS,IAAI,CAACA,MAAM;QAC1B,IAAI,CAACA,QAAQ;QACb,OAAO,IAAI4F,QAAc,CAACC,SAASC;YACjC9F,OAAOmG,KAAK,CAAC,CAAClE;gBACZ,IAAIA,KAAK,OAAO6D,OAAO7D;gBACvB,IAAI,CAACjC,MAAM,GAAG;gBACd6E,QAAQC,GAAG,CAAC;gBACZe;YACF;QACF;IACF;IAEA,MAAMd,QAAuB;QAC3B,IAAI,CAACvE,gBAAgB,CAACuE,KAAK;QAC3B,IAAI,CAAC9B,aAAa,GAAG;eAAI1D;SAAc;QACvC,IAAI,CAACgE,gBAAgB,GAAG;eAAI/D;SAAiB;QAC7C,IAAI,CAAC4E,aAAa,GAAG;YACnBD,gBAAgB;YAChBG,gBAAgB;YAChBC,YAAY;YACZC,YAAY;YACZE,YAAY;QACd;QACA,IAAI,CAACE,eAAe,GAAG;IACzB;IAEA,MAAMwB,YAA8B;QAClC,IAAI;YACF,MAAM/F,MAAM,MAAMgG,MAChB,GAAG,IAAI,CAACxF,MAAM,CAACC,WAAW,CAAC,wBAAwB,CAAC,EACpD;gBACEwF,SAAS;oBAAEC,YAAY;gBAAQ;YACjC;YAEF,MAAMlG,IAAImG,IAAI;YACd,OAAOnG,IAAIoG,EAAE;QACf,EAAE,eAAM;YACN,OAAO;QACT;IACF;IAEAC,kBAA0B;QACxB,OAAO,IAAI,CAAC7F,MAAM,CAACC,WAAW;IAChC;IAtVA,YAAY6F,SAAyC,CAAE;aAhB/C3G,SAA6B;aAC7BiD,gBAA0C,EAAE;aAC5CM,mBAAgD,EAAE;aACzC/C,mBAAmB,IAAIX;QAExC,oBAAoB;aACZuE,gBAAgB;YACtBD,gBAAgB;YAChBG,gBAAgB;YAChBC,YAAY;YACZC,YAAY;YACZE,YAAY;QACd;aAEQE,kBAA2B;QAGjC,IAAI,CAAC/D,MAAM,GAAGvB,uBAAuBqH;QACrC,IAAI,CAAC1D,aAAa,GAAG;eAAI1D;SAAc;QACvC,IAAI,CAACgE,gBAAgB,GAAG;eAAI/D;SAAiB;IAC/C;AAmVF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface AppleMusicTrackFixture {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
artistName: string;
|
|
5
|
+
albumName: string;
|
|
6
|
+
durationMs: number;
|
|
7
|
+
artworkFile: string | null;
|
|
8
|
+
audioFile: string;
|
|
9
|
+
}
|
|
10
|
+
export interface AppleMusicPlaylistFixture {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
description: string | null;
|
|
14
|
+
artworkFile: string | null;
|
|
15
|
+
trackIds: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare const trackFixtures: AppleMusicTrackFixture[];
|
|
18
|
+
export declare const playlistFixtures: AppleMusicPlaylistFixture[];
|
|
19
|
+
//# sourceMappingURL=catalog-fixtures.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-fixtures.d.ts","sourceRoot":"","sources":["../../src/apple-music/catalog-fixtures.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAMD,eAAO,MAAM,aAAa,EAAE,sBAAsB,EAgEjD,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,yBAAyB,EAsBvD,CAAC"}
|