@opfelmusig/lib 0.0.0-ci.0.sha-3e76884
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 +8 -0
- package/dist/clients/catalog/album.d.ts +12 -0
- package/dist/clients/catalog/album.js +14 -0
- package/dist/clients/catalog/genre.d.ts +13 -0
- package/dist/clients/catalog/genre.js +17 -0
- package/dist/clients/catalog/index.d.ts +71 -0
- package/dist/clients/catalog/index.js +99 -0
- package/dist/clients/catalog/musicVideo.d.ts +12 -0
- package/dist/clients/catalog/musicVideo.js +14 -0
- package/dist/clients/catalog/playlist.d.ts +12 -0
- package/dist/clients/catalog/playlist.js +16 -0
- package/dist/clients/catalog/song.d.ts +12 -0
- package/dist/clients/catalog/song.js +14 -0
- package/dist/clients/catalog/station.d.ts +14 -0
- package/dist/clients/catalog/station.js +34 -0
- package/dist/clients/catalog/stationGenre.d.ts +12 -0
- package/dist/clients/catalog/stationGenre.js +14 -0
- package/dist/clients/library/index.d.ts +39 -0
- package/dist/clients/library/index.js +68 -0
- package/dist/clients/library/playlistFolder.d.ts +12 -0
- package/dist/clients/library/playlistFolder.js +16 -0
- package/dist/clients/me/index.d.ts +25 -0
- package/dist/clients/me/index.js +49 -0
- package/dist/clients/me/musicSummary.d.ts +12 -0
- package/dist/clients/me/musicSummary.js +14 -0
- package/dist/clients/me/ratings.d.ts +15 -0
- package/dist/clients/me/ratings.js +34 -0
- package/dist/clients/me/recent.d.ts +17 -0
- package/dist/clients/me/recent.js +37 -0
- package/dist/clients/me/recommendation.d.ts +12 -0
- package/dist/clients/me/recommendation.js +14 -0
- package/dist/clients/me/station.d.ts +10 -0
- package/dist/clients/me/station.js +13 -0
- package/dist/clients/playback.d.ts +9 -0
- package/dist/clients/playback.js +68 -0
- package/dist/clients/resource.d.ts +34 -0
- package/dist/clients/resource.js +105 -0
- package/dist/clients/shared/equivalent.d.ts +10 -0
- package/dist/clients/shared/equivalent.js +15 -0
- package/dist/clients/social/index.d.ts +7 -0
- package/dist/clients/social/index.js +9 -0
- package/dist/clients/social/profile.d.ts +12 -0
- package/dist/clients/social/profile.js +14 -0
- package/dist/clients/storefront.d.ts +12 -0
- package/dist/clients/storefront.js +14 -0
- package/dist/core/client.d.ts +40 -0
- package/dist/core/client.js +82 -0
- package/dist/core/error.d.ts +23 -0
- package/dist/core/error.js +24 -0
- package/dist/core/http.d.ts +22 -0
- package/dist/core/http.js +232 -0
- package/dist/helpers/language.d.ts +1 -0
- package/dist/helpers/language.js +48 -0
- package/dist/helpers/public.d.ts +19 -0
- package/dist/helpers/public.js +35 -0
- package/dist/helpers/requestQuery.d.ts +17 -0
- package/dist/helpers/requestQuery.js +153 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/schemas/error.d.ts +29 -0
- package/dist/schemas/error.js +16 -0
- package/dist/schemas/request.d.ts +42 -0
- package/dist/schemas/request.js +28 -0
- package/dist/types/combined.d.ts +15 -0
- package/dist/types/combined.js +0 -0
- package/dist/types/config.d.ts +63 -0
- package/dist/types/config.js +0 -0
- package/dist/types/params.d.ts +22 -0
- package/dist/types/params.js +0 -0
- package/dist/types/playback.d.ts +199 -0
- package/dist/types/playback.js +103 -0
- package/dist/types/public.d.ts +56 -0
- package/dist/types/public.js +1 -0
- package/dist/types/query/helpers.d.ts +27 -0
- package/dist/types/query/helpers.js +0 -0
- package/dist/types/query/registry.d.ts +74 -0
- package/dist/types/query/registry.js +0 -0
- package/dist/types/query/resource.d.ts +29 -0
- package/dist/types/query/resource.js +0 -0
- package/dist/types/query/response.d.ts +3 -0
- package/dist/types/query/response.js +0 -0
- package/dist/types/query/results.d.ts +45 -0
- package/dist/types/query/results.js +0 -0
- package/dist/types/query/scopes.d.ts +77 -0
- package/dist/types/query/scopes.js +0 -0
- package/dist/types/request.d.ts +14 -0
- package/dist/types/request.js +0 -0
- package/dist/types/resources/catalog/activity.d.ts +15 -0
- package/dist/types/resources/catalog/activity.js +0 -0
- package/dist/types/resources/catalog/album.d.ts +47 -0
- package/dist/types/resources/catalog/album.js +0 -0
- package/dist/types/resources/catalog/artist.d.ts +57 -0
- package/dist/types/resources/catalog/artist.js +0 -0
- package/dist/types/resources/catalog/concert.d.ts +32 -0
- package/dist/types/resources/catalog/concert.js +0 -0
- package/dist/types/resources/catalog/creditArtist.d.ts +11 -0
- package/dist/types/resources/catalog/creditArtist.js +0 -0
- package/dist/types/resources/catalog/curator.d.ts +27 -0
- package/dist/types/resources/catalog/curator.js +0 -0
- package/dist/types/resources/catalog/genre.d.ts +11 -0
- package/dist/types/resources/catalog/genre.js +0 -0
- package/dist/types/resources/catalog/lyrics.d.ts +8 -0
- package/dist/types/resources/catalog/lyrics.js +0 -0
- package/dist/types/resources/catalog/musicVideo.d.ts +34 -0
- package/dist/types/resources/catalog/musicVideo.js +0 -0
- package/dist/types/resources/catalog/playlist.d.ts +35 -0
- package/dist/types/resources/catalog/playlist.js +0 -0
- package/dist/types/resources/catalog/recordLabel.d.ts +16 -0
- package/dist/types/resources/catalog/recordLabel.js +0 -0
- package/dist/types/resources/catalog/roleCategory.d.ts +12 -0
- package/dist/types/resources/catalog/roleCategory.js +0 -0
- package/dist/types/resources/catalog/song.d.ts +50 -0
- package/dist/types/resources/catalog/song.js +0 -0
- package/dist/types/resources/catalog/station.d.ts +21 -0
- package/dist/types/resources/catalog/station.js +0 -0
- package/dist/types/resources/catalog/stationGenre.d.ts +11 -0
- package/dist/types/resources/catalog/stationGenre.js +0 -0
- package/dist/types/resources/catalog/syllableLyrics.d.ts +9 -0
- package/dist/types/resources/catalog/syllableLyrics.js +0 -0
- package/dist/types/resources/catalog/venue.d.ts +20 -0
- package/dist/types/resources/catalog/venue.js +0 -0
- package/dist/types/resources/library/album.d.ts +17 -0
- package/dist/types/resources/library/album.js +0 -0
- package/dist/types/resources/library/artist.d.ts +13 -0
- package/dist/types/resources/library/artist.js +0 -0
- package/dist/types/resources/library/musicVideo.d.ts +15 -0
- package/dist/types/resources/library/musicVideo.js +0 -0
- package/dist/types/resources/library/playlist.d.ts +24 -0
- package/dist/types/resources/library/playlist.js +0 -0
- package/dist/types/resources/library/playlistFolder.d.ts +13 -0
- package/dist/types/resources/library/playlistFolder.js +0 -0
- package/dist/types/resources/library/song.d.ts +15 -0
- package/dist/types/resources/library/song.js +0 -0
- package/dist/types/resources/me/musicSummary.d.ts +37 -0
- package/dist/types/resources/me/musicSummary.js +0 -0
- package/dist/types/resources/me/profile.d.ts +32 -0
- package/dist/types/resources/me/profile.js +0 -0
- package/dist/types/resources/me/rating.d.ts +11 -0
- package/dist/types/resources/me/rating.js +0 -0
- package/dist/types/resources/me/recommendation.d.ts +27 -0
- package/dist/types/resources/me/recommendation.js +0 -0
- package/dist/types/resources/me/summaries/albumPeriodSummary.d.ts +15 -0
- package/dist/types/resources/me/summaries/albumPeriodSummary.js +0 -0
- package/dist/types/resources/me/summaries/artistPeriodSummary.d.ts +15 -0
- package/dist/types/resources/me/summaries/artistPeriodSummary.js +0 -0
- package/dist/types/resources/me/summaries/genrePeriodSummary.d.ts +12 -0
- package/dist/types/resources/me/summaries/genrePeriodSummary.js +0 -0
- package/dist/types/resources/me/summaries/monthlySongPeriodSummary.d.ts +13 -0
- package/dist/types/resources/me/summaries/monthlySongPeriodSummary.js +0 -0
- package/dist/types/resources/me/summaries/playlistPeriodSummary.d.ts +14 -0
- package/dist/types/resources/me/summaries/playlistPeriodSummary.js +0 -0
- package/dist/types/resources/me/summaries/songPeriodSummary.d.ts +15 -0
- package/dist/types/resources/me/summaries/songPeriodSummary.js +0 -0
- package/dist/types/resources/me/summaries/stationPeriodSummary.d.ts +14 -0
- package/dist/types/resources/me/summaries/stationPeriodSummary.js +0 -0
- package/dist/types/resources/other/storefront.d.ts +8 -0
- package/dist/types/resources/other/storefront.js +0 -0
- package/dist/types/resources/shared/album.d.ts +11 -0
- package/dist/types/resources/shared/album.js +0 -0
- package/dist/types/resources/shared/artist.d.ts +3 -0
- package/dist/types/resources/shared/artist.js +0 -0
- package/dist/types/resources/shared/lyrics.d.ts +5 -0
- package/dist/types/resources/shared/lyrics.js +0 -0
- package/dist/types/resources/shared/musicVideo.d.ts +13 -0
- package/dist/types/resources/shared/musicVideo.js +0 -0
- package/dist/types/resources/shared/playlist.d.ts +7 -0
- package/dist/types/resources/shared/playlist.js +0 -0
- package/dist/types/resources/shared/song.d.ts +15 -0
- package/dist/types/resources/shared/song.js +0 -0
- package/dist/types/results/chart.d.ts +8 -0
- package/dist/types/results/chart.js +0 -0
- package/dist/types/results/search.d.ts +30 -0
- package/dist/types/results/search.js +0 -0
- package/dist/types/shared.d.ts +81 -0
- package/dist/types/shared.js +0 -0
- package/package.json +64 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type z from "zod";
|
|
2
|
+
import { type AppleMusicErrorSchema } from "../schemas/error.js";
|
|
3
|
+
export type AppleMusicErrorKind = "response" | "network";
|
|
4
|
+
export type AppleMusicErrorDetail = z.infer<typeof AppleMusicErrorSchema>;
|
|
5
|
+
export interface AppleMusicErrorOptions {
|
|
6
|
+
kind: AppleMusicErrorKind;
|
|
7
|
+
url: string;
|
|
8
|
+
status?: number;
|
|
9
|
+
statusText?: string;
|
|
10
|
+
retryAfter?: string;
|
|
11
|
+
errors?: readonly AppleMusicErrorDetail[];
|
|
12
|
+
message?: string;
|
|
13
|
+
cause?: unknown;
|
|
14
|
+
}
|
|
15
|
+
export declare class AppleMusicError extends Error {
|
|
16
|
+
readonly kind: AppleMusicErrorKind;
|
|
17
|
+
readonly url: string;
|
|
18
|
+
readonly status?: number;
|
|
19
|
+
readonly statusText?: string;
|
|
20
|
+
readonly retryAfter?: string;
|
|
21
|
+
readonly errors: readonly AppleMusicErrorDetail[];
|
|
22
|
+
constructor(options: AppleMusicErrorOptions);
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {} from "../schemas/error.js";
|
|
2
|
+
function getDefaultMessage(options) {
|
|
3
|
+
if (options.kind === "network")
|
|
4
|
+
return "Apple Music API network error";
|
|
5
|
+
return `Apple Music API error ${options.status ?? "unknown"}${options.statusText ? ` ${options.statusText}` : ""}`;
|
|
6
|
+
}
|
|
7
|
+
export class AppleMusicError extends Error {
|
|
8
|
+
kind;
|
|
9
|
+
url;
|
|
10
|
+
status;
|
|
11
|
+
statusText;
|
|
12
|
+
retryAfter;
|
|
13
|
+
errors;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
super(options.message ?? getDefaultMessage(options), { cause: options.cause });
|
|
16
|
+
this.name = "AppleMusicError";
|
|
17
|
+
this.kind = options.kind;
|
|
18
|
+
this.url = options.url;
|
|
19
|
+
this.status = options.status;
|
|
20
|
+
this.statusText = options.statusText;
|
|
21
|
+
this.retryAfter = options.retryAfter;
|
|
22
|
+
this.errors = options.errors ?? [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { OpfelMusigConfig } from "../types/config.js";
|
|
2
|
+
import type { OpfelMusigHttpRequest } from "../types/request.js";
|
|
3
|
+
export declare const appleMusicApiUrl = "https://amp-api.music.apple.com";
|
|
4
|
+
type DeepPartialNullable<T> = T extends (...args: any[]) => any ? T | null : T extends object ? {
|
|
5
|
+
[K in keyof T]?: DeepPartialNullable<T[K]> | null;
|
|
6
|
+
} : T | null;
|
|
7
|
+
export declare class HttpClient {
|
|
8
|
+
private config;
|
|
9
|
+
constructor(config: OpfelMusigConfig);
|
|
10
|
+
edit: (config: DeepPartialNullable<OpfelMusigConfig>) => void;
|
|
11
|
+
get conf(): {
|
|
12
|
+
storefront: string | undefined;
|
|
13
|
+
language: import("../types/config.js").LanguageConfig | undefined;
|
|
14
|
+
};
|
|
15
|
+
private prepareTokens;
|
|
16
|
+
private get baseUrl();
|
|
17
|
+
private emit;
|
|
18
|
+
private send;
|
|
19
|
+
request<T>(path: string, request?: OpfelMusigHttpRequest): Promise<T>;
|
|
20
|
+
requestUrl<T>(url: string, request?: OpfelMusigHttpRequest): Promise<T>;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { AppleMusicError } from "../core/error.js";
|
|
2
|
+
import { getDefaultHeaders } from "../helpers/public.js";
|
|
3
|
+
import { RequestQueryBuilder } from "../helpers/requestQuery.js";
|
|
4
|
+
import { AppleMusicErrorResponseSchema } from "../schemas/error.js";
|
|
5
|
+
export const appleMusicApiUrl = "https://amp-api.music.apple.com";
|
|
6
|
+
function mergePatch(current, patch) {
|
|
7
|
+
const next = { ...current };
|
|
8
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
9
|
+
if (value === undefined)
|
|
10
|
+
continue;
|
|
11
|
+
if (value === null) {
|
|
12
|
+
delete next[key];
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
16
|
+
next[key] = mergePatch((next[key] ?? {}), value);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
next[key] = value;
|
|
20
|
+
}
|
|
21
|
+
return next;
|
|
22
|
+
}
|
|
23
|
+
async function resolveToken(token) {
|
|
24
|
+
return typeof token === "function" ? token() : token;
|
|
25
|
+
}
|
|
26
|
+
function rethrowAbort(cause, signal) {
|
|
27
|
+
if (signal?.aborted)
|
|
28
|
+
throw signal.reason ?? cause;
|
|
29
|
+
if (cause instanceof Error && cause.name === "AbortError")
|
|
30
|
+
throw cause;
|
|
31
|
+
}
|
|
32
|
+
function headerValue(headers, name) {
|
|
33
|
+
const entry = Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase());
|
|
34
|
+
return entry?.[1];
|
|
35
|
+
}
|
|
36
|
+
function bearerToken(value) {
|
|
37
|
+
return value?.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
38
|
+
}
|
|
39
|
+
function httpEventError(kind, aborted = false) {
|
|
40
|
+
if (aborted) {
|
|
41
|
+
return {
|
|
42
|
+
name: "AbortError",
|
|
43
|
+
message: "Request aborted",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return kind === "network"
|
|
47
|
+
? {
|
|
48
|
+
name: "NetworkError",
|
|
49
|
+
message: "Request failed",
|
|
50
|
+
}
|
|
51
|
+
: {
|
|
52
|
+
name: "ParseError",
|
|
53
|
+
message: "Response parsing failed",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export class HttpClient {
|
|
57
|
+
config;
|
|
58
|
+
constructor(config) {
|
|
59
|
+
this.config = config;
|
|
60
|
+
}
|
|
61
|
+
edit = (config) => {
|
|
62
|
+
this.config = mergePatch(this.config, config);
|
|
63
|
+
};
|
|
64
|
+
get conf() {
|
|
65
|
+
return {
|
|
66
|
+
storefront: this.config.storefront,
|
|
67
|
+
language: this.config.language,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async prepareTokens() {
|
|
71
|
+
return {
|
|
72
|
+
developerToken: await resolveToken(this.config.developerToken),
|
|
73
|
+
userToken: await resolveToken(this.config.userToken),
|
|
74
|
+
excludeOrigin: this.config.excludeOrigin,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
get baseUrl() {
|
|
78
|
+
const baseUrl = this.config.baseUrl ?? appleMusicApiUrl;
|
|
79
|
+
return baseUrl.replace(/\/$/, "");
|
|
80
|
+
}
|
|
81
|
+
emit(event) {
|
|
82
|
+
const observer = this.config.onHttpEvent;
|
|
83
|
+
if (!observer)
|
|
84
|
+
return;
|
|
85
|
+
try {
|
|
86
|
+
Promise.resolve(observer(event)).catch(() => { });
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async send(url, request) {
|
|
93
|
+
const configuredTokens = await this.prepareTokens();
|
|
94
|
+
const headers = {
|
|
95
|
+
...getDefaultHeaders(configuredTokens),
|
|
96
|
+
...this.config.extraHeaders,
|
|
97
|
+
...request.config?.extraHeaders,
|
|
98
|
+
...request.headers,
|
|
99
|
+
};
|
|
100
|
+
const method = request.method ?? "GET";
|
|
101
|
+
const requestTokens = {
|
|
102
|
+
developerToken: bearerToken(headerValue(headers, "Authorization")) ?? configuredTokens.developerToken,
|
|
103
|
+
userToken: headerValue(headers, "Media-User-Token") ??
|
|
104
|
+
headerValue(headers, "Music-User-Token") ??
|
|
105
|
+
configuredTokens.userToken,
|
|
106
|
+
excludeOrigin: configuredTokens.excludeOrigin,
|
|
107
|
+
};
|
|
108
|
+
const body = typeof request.body === "function" ? request.body(requestTokens) : request.body;
|
|
109
|
+
const fetch = this.config.fetch ?? globalThis.fetch;
|
|
110
|
+
const startedAt = performance.now();
|
|
111
|
+
let response;
|
|
112
|
+
try {
|
|
113
|
+
response = await fetch(url, {
|
|
114
|
+
body,
|
|
115
|
+
headers,
|
|
116
|
+
method,
|
|
117
|
+
signal: request.config?.signal,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (cause) {
|
|
121
|
+
const aborted = request.config?.signal?.aborted || (cause instanceof Error && cause.name === "AbortError");
|
|
122
|
+
this.emit({
|
|
123
|
+
method,
|
|
124
|
+
url,
|
|
125
|
+
durationMs: performance.now() - startedAt,
|
|
126
|
+
error: httpEventError("network", aborted),
|
|
127
|
+
});
|
|
128
|
+
rethrowAbort(cause, request.config?.signal);
|
|
129
|
+
throw new AppleMusicError({
|
|
130
|
+
kind: "network",
|
|
131
|
+
url,
|
|
132
|
+
cause,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (this.config.debug) {
|
|
136
|
+
console.log(`RES ${response.status} [${response.statusText}]`);
|
|
137
|
+
}
|
|
138
|
+
const responseContext = {
|
|
139
|
+
url,
|
|
140
|
+
status: response.status,
|
|
141
|
+
statusText: response.statusText,
|
|
142
|
+
retryAfter: response.headers.get("Retry-After") ?? undefined,
|
|
143
|
+
};
|
|
144
|
+
let result;
|
|
145
|
+
try {
|
|
146
|
+
result =
|
|
147
|
+
response.status === 204 || response.status === 205 ? undefined : await response.json();
|
|
148
|
+
}
|
|
149
|
+
catch (cause) {
|
|
150
|
+
const aborted = request.config?.signal?.aborted || (cause instanceof Error && cause.name === "AbortError");
|
|
151
|
+
this.emit({
|
|
152
|
+
method,
|
|
153
|
+
url,
|
|
154
|
+
status: response.status,
|
|
155
|
+
statusText: response.statusText,
|
|
156
|
+
durationMs: performance.now() - startedAt,
|
|
157
|
+
error: httpEventError("parse", aborted),
|
|
158
|
+
});
|
|
159
|
+
throw cause;
|
|
160
|
+
}
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
let errorResponse;
|
|
163
|
+
try {
|
|
164
|
+
errorResponse = AppleMusicErrorResponseSchema.parse(result);
|
|
165
|
+
}
|
|
166
|
+
catch (cause) {
|
|
167
|
+
this.emit({
|
|
168
|
+
method,
|
|
169
|
+
url,
|
|
170
|
+
status: response.status,
|
|
171
|
+
statusText: response.statusText,
|
|
172
|
+
durationMs: performance.now() - startedAt,
|
|
173
|
+
response: result,
|
|
174
|
+
error: httpEventError("parse"),
|
|
175
|
+
});
|
|
176
|
+
throw cause;
|
|
177
|
+
}
|
|
178
|
+
this.emit({
|
|
179
|
+
method,
|
|
180
|
+
url,
|
|
181
|
+
status: response.status,
|
|
182
|
+
statusText: response.statusText,
|
|
183
|
+
durationMs: performance.now() - startedAt,
|
|
184
|
+
response: result,
|
|
185
|
+
});
|
|
186
|
+
throw new AppleMusicError({
|
|
187
|
+
kind: "response",
|
|
188
|
+
...responseContext,
|
|
189
|
+
errors: errorResponse.errors,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
this.emit({
|
|
193
|
+
method,
|
|
194
|
+
url,
|
|
195
|
+
status: response.status,
|
|
196
|
+
statusText: response.statusText,
|
|
197
|
+
durationMs: performance.now() - startedAt,
|
|
198
|
+
response: result,
|
|
199
|
+
});
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
async request(path, request = {}) {
|
|
203
|
+
const questionIndex = path.indexOf("?");
|
|
204
|
+
const url = questionIndex === -1 ? path : path.slice(0, questionIndex);
|
|
205
|
+
const params = questionIndex === -1 ? "" : path.slice(questionIndex + 1);
|
|
206
|
+
const requestPath = url
|
|
207
|
+
.replace("{{storefront}}", request.config?.storefront ?? this.config.storefront ?? "us")
|
|
208
|
+
.replace(/^\/+/, "");
|
|
209
|
+
const requestQuery = RequestQueryBuilder.create(request.query)
|
|
210
|
+
.language(this.config.language)
|
|
211
|
+
.requestConfig(request.config)
|
|
212
|
+
.urlParams(params)
|
|
213
|
+
.set("art[url]", "f")
|
|
214
|
+
.set("omit[resource]", "autos")
|
|
215
|
+
.prepare();
|
|
216
|
+
const query = new URLSearchParams(requestQuery).toString();
|
|
217
|
+
const requestUrl = `${this.baseUrl}/${requestPath}${query ? `?${query}` : ""}`;
|
|
218
|
+
const method = request.method ?? "GET";
|
|
219
|
+
if (this.config.debug) {
|
|
220
|
+
const { ["art[url]"]: artworkUrlFormat, ["omit[resource]"]: omitResource, ...rest } = requestQuery;
|
|
221
|
+
console.log(`REQ ${method} [/${requestPath}] - ${JSON.stringify(rest)}`);
|
|
222
|
+
}
|
|
223
|
+
return this.send(requestUrl, request);
|
|
224
|
+
}
|
|
225
|
+
async requestUrl(url, request = {}) {
|
|
226
|
+
const parsedUrl = new URL(url);
|
|
227
|
+
if (this.config.debug) {
|
|
228
|
+
console.log(`REQ ${request.method ?? "GET"} [${parsedUrl.origin}${parsedUrl.pathname}]`);
|
|
229
|
+
}
|
|
230
|
+
return this.send(url, request);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function lyricScriptFromLanguage(language?: string): string | undefined;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const lyricScripts = {
|
|
2
|
+
am: "Ethi",
|
|
3
|
+
ar: "Arab",
|
|
4
|
+
be: "Cyrl",
|
|
5
|
+
bg: "Cyrl",
|
|
6
|
+
bn: "Beng",
|
|
7
|
+
bo: "Tibt",
|
|
8
|
+
el: "Grek",
|
|
9
|
+
fa: "Arab",
|
|
10
|
+
gu: "Gujr",
|
|
11
|
+
he: "Hebr",
|
|
12
|
+
hi: "Deva",
|
|
13
|
+
hy: "Armn",
|
|
14
|
+
ja: "Kana",
|
|
15
|
+
ka: "Geor",
|
|
16
|
+
kk: "Cyrl",
|
|
17
|
+
km: "Khmr",
|
|
18
|
+
kn: "Knda",
|
|
19
|
+
ko: "Hang",
|
|
20
|
+
ky: "Cyrl",
|
|
21
|
+
lo: "Laoo",
|
|
22
|
+
mk: "Cyrl",
|
|
23
|
+
ml: "Mlym",
|
|
24
|
+
mn: "Cyrl",
|
|
25
|
+
mr: "Deva",
|
|
26
|
+
my: "Mymr",
|
|
27
|
+
ne: "Deva",
|
|
28
|
+
or: "Orya",
|
|
29
|
+
pa: "Guru",
|
|
30
|
+
ps: "Arab",
|
|
31
|
+
ru: "Cyrl",
|
|
32
|
+
sa: "Deva",
|
|
33
|
+
sd: "Arab",
|
|
34
|
+
si: "Sinh",
|
|
35
|
+
sr: "Cyrl",
|
|
36
|
+
ta: "Taml",
|
|
37
|
+
te: "Telu",
|
|
38
|
+
th: "Thai",
|
|
39
|
+
ti: "Ethi",
|
|
40
|
+
uk: "Cyrl",
|
|
41
|
+
ur: "Aran",
|
|
42
|
+
yi: "Hebr",
|
|
43
|
+
zh: "Hans",
|
|
44
|
+
};
|
|
45
|
+
export function lyricScriptFromLanguage(language) {
|
|
46
|
+
const languageCode = language?.split("-")[0]?.toLowerCase();
|
|
47
|
+
return languageCode ? `${languageCode}-${lyricScripts[languageCode] ?? "Latn"}` : undefined;
|
|
48
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ResourceScopes } from "../types/query/scopes.js";
|
|
2
|
+
import type { HeaderConfig } from "../types/request.js";
|
|
3
|
+
import type { Artwork } from "../types/shared.js";
|
|
4
|
+
interface ImageOptions {
|
|
5
|
+
width?: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
crop?: string;
|
|
8
|
+
format?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function defineScopes<const TScopes extends ResourceScopes>(scopes: TScopes): TScopes;
|
|
11
|
+
export declare function parseImageUrl(image: string, options?: ImageOptions): string;
|
|
12
|
+
export declare function getArtworkUrl(artwork?: Artwork | string | null, options?: ImageOptions): string | undefined;
|
|
13
|
+
export declare function getDefaultHeaders(config?: HeaderConfig): {
|
|
14
|
+
Origin?: string | undefined;
|
|
15
|
+
Authorization?: string | undefined;
|
|
16
|
+
"Music-User-Token"?: string | undefined;
|
|
17
|
+
"Media-User-Token"?: string | undefined;
|
|
18
|
+
};
|
|
19
|
+
export { lyricScriptFromLanguage } from "../helpers/language.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function defineScopes(scopes) {
|
|
2
|
+
return scopes;
|
|
3
|
+
}
|
|
4
|
+
export function parseImageUrl(image, options = {}) {
|
|
5
|
+
const width = options?.width ?? options?.height ?? 512;
|
|
6
|
+
const height = options?.height ?? width;
|
|
7
|
+
return image
|
|
8
|
+
.replace("{w}", String(width))
|
|
9
|
+
.replace("{h}", String(height))
|
|
10
|
+
.replace("{c}", options.crop ?? "")
|
|
11
|
+
.replace("{f}", options.format ?? "jpg");
|
|
12
|
+
}
|
|
13
|
+
export function getArtworkUrl(artwork, options = {}) {
|
|
14
|
+
const url = typeof artwork === "string" ? artwork : artwork?.url;
|
|
15
|
+
if (!url)
|
|
16
|
+
return;
|
|
17
|
+
return parseImageUrl(url, options);
|
|
18
|
+
}
|
|
19
|
+
export function getDefaultHeaders(config) {
|
|
20
|
+
return {
|
|
21
|
+
...(config?.excludeOrigin ? {} : { Origin: "https://music.apple.com" }),
|
|
22
|
+
...(config?.developerToken
|
|
23
|
+
? {
|
|
24
|
+
Authorization: `Bearer ${config.developerToken}`,
|
|
25
|
+
}
|
|
26
|
+
: {}),
|
|
27
|
+
...(config?.userToken
|
|
28
|
+
? {
|
|
29
|
+
"Music-User-Token": config.userToken,
|
|
30
|
+
"Media-User-Token": config.userToken,
|
|
31
|
+
}
|
|
32
|
+
: {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export { lyricScriptFromLanguage } from "../helpers/language.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LanguageConfig, RequestConfig } from "../types/config.js";
|
|
2
|
+
export declare class RequestQueryBuilder {
|
|
3
|
+
private query;
|
|
4
|
+
static create(base?: RequestQueryBuilder): RequestQueryBuilder;
|
|
5
|
+
params(params?: Record<string, any>): this;
|
|
6
|
+
private shape;
|
|
7
|
+
private scopes;
|
|
8
|
+
private scope;
|
|
9
|
+
language(language?: LanguageConfig): this;
|
|
10
|
+
requestConfig(config: RequestConfig | undefined): this;
|
|
11
|
+
urlParams(params: string): this;
|
|
12
|
+
set(key?: string, value?: string, scope?: string): this;
|
|
13
|
+
setNumber(key: string, value?: number, scope?: string): this;
|
|
14
|
+
setStringArray(key: string, values?: string | string[], scope?: string): this;
|
|
15
|
+
setString(key: string, value?: string, scope?: string): this;
|
|
16
|
+
prepare(): Record<string, string>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { ScopeSchema, ScopesSchema, ShapeSchema } from "../schemas/request.js";
|
|
2
|
+
export class RequestQueryBuilder {
|
|
3
|
+
query = {};
|
|
4
|
+
static create(base) {
|
|
5
|
+
const requestQuery = new RequestQueryBuilder();
|
|
6
|
+
requestQuery.query = base?.query ? structuredClone(base.query) : {};
|
|
7
|
+
return requestQuery;
|
|
8
|
+
}
|
|
9
|
+
params(params) {
|
|
10
|
+
if (!params)
|
|
11
|
+
return this;
|
|
12
|
+
for (const [key, value] of Object.entries(params)) {
|
|
13
|
+
if (["limit", "offset"].includes(key)) {
|
|
14
|
+
this.setNumber(key, value);
|
|
15
|
+
}
|
|
16
|
+
if (key === "scopes") {
|
|
17
|
+
this.scopes(value);
|
|
18
|
+
}
|
|
19
|
+
if (key === "shape") {
|
|
20
|
+
this.shape(value);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
shape(value) {
|
|
26
|
+
const result = ShapeSchema.safeParse(value);
|
|
27
|
+
if (!result.success) {
|
|
28
|
+
console.warn(`[Opfelmusig/RequestQuery] 'shape' is not in the correct format, skipping.`, result.error.issues[0]?.message);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.setStringArray("types", result.data.types);
|
|
32
|
+
if (result.data.with)
|
|
33
|
+
this.setStringArray("with", result.data.with);
|
|
34
|
+
}
|
|
35
|
+
scopes(value) {
|
|
36
|
+
const result = ScopesSchema.safeParse(value);
|
|
37
|
+
if (!result.success) {
|
|
38
|
+
console.warn(`[Opfelmusig/RequestQuery] 'scopes' is not in the correct format, skipping.`, result.error.issues[0]?.message);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
for (const [key, value] of Object.entries(result.data)) {
|
|
42
|
+
const result = ScopeSchema.safeParse(value);
|
|
43
|
+
if (!result.success) {
|
|
44
|
+
console.warn(`[Opfelmusig/RequestQuery] 'scopes/${key}' is not in the correct format, skipping.`, result.error.issues[0]?.message);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
this.scope(key, result.data);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
scope(scope, value) {
|
|
51
|
+
const fields = value.fields ? [...(value.fields ?? []), ...(value.extend ?? [])] : [];
|
|
52
|
+
if (fields.length)
|
|
53
|
+
this.setStringArray("fields", fields, scope);
|
|
54
|
+
if (value.extend?.length)
|
|
55
|
+
this.setStringArray("extend", value.extend, scope);
|
|
56
|
+
if (value.relationships) {
|
|
57
|
+
const include = [];
|
|
58
|
+
const relate = [];
|
|
59
|
+
for (const [name, options] of Object.entries(value.relationships)) {
|
|
60
|
+
const mode = typeof options === "string" ? options : options.mode;
|
|
61
|
+
if (mode === "include")
|
|
62
|
+
include.push(name);
|
|
63
|
+
if (mode === "relate")
|
|
64
|
+
relate.push(name);
|
|
65
|
+
if (typeof options === "object") {
|
|
66
|
+
this.setNumber("limit", options.limit, `${scope}:${name}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (include.length)
|
|
70
|
+
this.setStringArray("include", include, scope);
|
|
71
|
+
if (relate.length)
|
|
72
|
+
this.setStringArray("relate", relate, scope);
|
|
73
|
+
}
|
|
74
|
+
if (value.views) {
|
|
75
|
+
const views = [];
|
|
76
|
+
for (const [name, options] of Object.entries(value.views)) {
|
|
77
|
+
if (!options)
|
|
78
|
+
continue;
|
|
79
|
+
views.push(name);
|
|
80
|
+
if (typeof options === "object") {
|
|
81
|
+
this.setNumber("limit", options.limit, `${scope}:${name}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (views.length)
|
|
85
|
+
this.setStringArray("views", views, scope);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
language(language) {
|
|
89
|
+
if (language) {
|
|
90
|
+
this.setString("l", language.default);
|
|
91
|
+
this.setString("l[lyrics]", language.lyrics);
|
|
92
|
+
this.setString("l[script]", language.script);
|
|
93
|
+
}
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
requestConfig(config) {
|
|
97
|
+
this.language(config?.language);
|
|
98
|
+
if (config?.extraParams) {
|
|
99
|
+
for (const [key, value] of Object.entries(config.extraParams)) {
|
|
100
|
+
if (key === undefined || value === undefined)
|
|
101
|
+
continue;
|
|
102
|
+
this.set(key, value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
urlParams(params) {
|
|
108
|
+
for (const [key, value] of new URLSearchParams(params).entries()) {
|
|
109
|
+
this.set(key, value);
|
|
110
|
+
}
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
set(key, value, scope) {
|
|
114
|
+
if (!key || value === undefined || key.startsWith("~"))
|
|
115
|
+
return this;
|
|
116
|
+
key = scope ? `${key}[${scope}]` : key;
|
|
117
|
+
this.query[key] = value;
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
setNumber(key, value, scope) {
|
|
121
|
+
if (value === undefined)
|
|
122
|
+
return this;
|
|
123
|
+
if (typeof value !== "number") {
|
|
124
|
+
console.warn(`[Opfelmusig/RequestQuery] '${key}' is not a number. skipping.`);
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
this.set(key, String(value), scope);
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
setStringArray(key, values, scope) {
|
|
131
|
+
if (values === undefined)
|
|
132
|
+
return this;
|
|
133
|
+
values = !Array.isArray(values) ? [values] : values;
|
|
134
|
+
values = values.filter((x) => x !== undefined && !String(x).startsWith("~"));
|
|
135
|
+
if (values.length <= 0)
|
|
136
|
+
return this;
|
|
137
|
+
this.set(key, values.join(","), scope);
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
setString(key, value, scope) {
|
|
141
|
+
if (value === undefined)
|
|
142
|
+
return this;
|
|
143
|
+
if (typeof value !== "string") {
|
|
144
|
+
console.warn(`[Opfelmusig/RequestQuery] '${key}' is not a string. skipping.`);
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
this.set(key, value, scope);
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
prepare() {
|
|
151
|
+
return this.query;
|
|
152
|
+
}
|
|
153
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { OpfelMusigClient } from "./core/client.js";
|
|
2
|
+
export { AppleMusicError, type AppleMusicErrorDetail, type AppleMusicErrorKind, type AppleMusicErrorOptions, } from "./core/error.js";
|
|
3
|
+
export { defineScopes } from "./helpers/public.js";
|
|
4
|
+
export type { HttpEvent, LanguageConfig, OpfelMusigConfig, RequestConfig, TokenProvider, } from "./types/config.js";
|
|
5
|
+
export type { ResourceOf, ResourceTemplate, ResourceTemplateOf, ValidateResourceName, } from "./types/query/helpers.js";
|
|
6
|
+
export type { ScopedResourceOf } from "./types/query/scopes.js";
|
|
7
|
+
export { appleMusicApiUrl } from "./core/http.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
export declare const AppleMusicErrorSourceSchema: z.ZodObject<{
|
|
3
|
+
parameter: z.ZodOptional<z.ZodString>;
|
|
4
|
+
pointer: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export declare const AppleMusicErrorSchema: z.ZodObject<{
|
|
7
|
+
code: z.ZodString;
|
|
8
|
+
detail: z.ZodOptional<z.ZodString>;
|
|
9
|
+
id: z.ZodString;
|
|
10
|
+
source: z.ZodOptional<z.ZodObject<{
|
|
11
|
+
parameter: z.ZodOptional<z.ZodString>;
|
|
12
|
+
pointer: z.ZodOptional<z.ZodString>;
|
|
13
|
+
}, z.core.$strip>>;
|
|
14
|
+
status: z.ZodString;
|
|
15
|
+
title: z.ZodString;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
export declare const AppleMusicErrorResponseSchema: z.ZodObject<{
|
|
18
|
+
errors: z.ZodArray<z.ZodObject<{
|
|
19
|
+
code: z.ZodString;
|
|
20
|
+
detail: z.ZodOptional<z.ZodString>;
|
|
21
|
+
id: z.ZodString;
|
|
22
|
+
source: z.ZodOptional<z.ZodObject<{
|
|
23
|
+
parameter: z.ZodOptional<z.ZodString>;
|
|
24
|
+
pointer: z.ZodOptional<z.ZodString>;
|
|
25
|
+
}, z.core.$strip>>;
|
|
26
|
+
status: z.ZodString;
|
|
27
|
+
title: z.ZodString;
|
|
28
|
+
}, z.core.$strip>>;
|
|
29
|
+
}, z.core.$strip>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
export const AppleMusicErrorSourceSchema = z.object({
|
|
3
|
+
parameter: z.string().optional(),
|
|
4
|
+
pointer: z.string().optional(),
|
|
5
|
+
});
|
|
6
|
+
export const AppleMusicErrorSchema = z.object({
|
|
7
|
+
code: z.string(),
|
|
8
|
+
detail: z.string().optional(),
|
|
9
|
+
id: z.string(),
|
|
10
|
+
source: AppleMusicErrorSourceSchema.optional(),
|
|
11
|
+
status: z.string(),
|
|
12
|
+
title: z.string(),
|
|
13
|
+
});
|
|
14
|
+
export const AppleMusicErrorResponseSchema = z.object({
|
|
15
|
+
errors: z.array(AppleMusicErrorSchema),
|
|
16
|
+
});
|