axios->undici, reducing overhead and vuln
This commit is contained in:
parent
a3cefee49a
commit
3a179e0c69
12 changed files with 160 additions and 232 deletions
142
package-lock.json
generated
142
package-lock.json
generated
|
|
@ -11,7 +11,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@libsql/client": "^0.15.12",
|
"@libsql/client": "^0.15.12",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"axios": "^1.11.0",
|
|
||||||
"callsites": "^4.2.0",
|
"callsites": "^4.2.0",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"data-uri-to-buffer": "^6.0.2",
|
"data-uri-to-buffer": "^6.0.2",
|
||||||
|
|
@ -28,6 +27,7 @@
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"timeago.js": "^4.0.2",
|
"timeago.js": "^4.0.2",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
|
"undici": "^7.16.0",
|
||||||
"zod": "^4.0.14",
|
"zod": "^4.0.14",
|
||||||
"zod-openapi": "^5.3.0",
|
"zod-openapi": "^5.3.0",
|
||||||
"zod-validation-error": "^4.0.1"
|
"zod-validation-error": "^4.0.1"
|
||||||
|
|
@ -2326,23 +2326,6 @@
|
||||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/asynckit": {
|
|
||||||
"version": "0.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/axios": {
|
|
||||||
"version": "1.11.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
|
||||||
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"follow-redirects": "^1.15.6",
|
|
||||||
"form-data": "^4.0.4",
|
|
||||||
"proxy-from-env": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/b4a": {
|
"node_modules/b4a": {
|
||||||
"version": "1.6.7",
|
"version": "1.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
||||||
|
|
@ -2558,18 +2541,6 @@
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/combined-stream": {
|
|
||||||
"version": "1.0.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
|
||||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"delayed-stream": "~1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/compress-commons": {
|
"node_modules/compress-commons": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
|
||||||
|
|
@ -2782,15 +2753,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/delayed-stream": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
|
|
@ -3059,21 +3021,6 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/es-set-tostringtag": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"es-errors": "^1.3.0",
|
|
||||||
"get-intrinsic": "^1.2.6",
|
|
||||||
"has-tostringtag": "^1.0.2",
|
|
||||||
"hasown": "^2.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.9",
|
"version": "0.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
|
||||||
|
|
@ -3664,26 +3611,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
|
||||||
"version": "1.15.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
|
||||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"debug": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/foreground-child": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
|
|
@ -3700,22 +3627,6 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
|
||||||
"version": "4.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
|
||||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"asynckit": "^0.4.0",
|
|
||||||
"combined-stream": "^1.0.8",
|
|
||||||
"es-set-tostringtag": "^2.1.0",
|
|
||||||
"hasown": "^2.0.2",
|
|
||||||
"mime-types": "^2.1.12"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/format-duration": {
|
"node_modules/format-duration": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/format-duration/-/format-duration-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/format-duration/-/format-duration-3.0.2.tgz",
|
||||||
|
|
@ -3992,21 +3903,6 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/has-tostringtag": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"has-symbols": "^1.0.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
|
@ -4473,27 +4369,6 @@
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mime-db": {
|
|
||||||
"version": "1.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mime-types": {
|
|
||||||
"version": "2.1.35",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "1.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "9.0.5",
|
"version": "9.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||||
|
|
@ -4911,12 +4786,6 @@
|
||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/proxy-from-env": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/pssh-tools": {
|
"node_modules/pssh-tools": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/pssh-tools/-/pssh-tools-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/pssh-tools/-/pssh-tools-1.2.0.tgz",
|
||||||
|
|
@ -5899,6 +5768,15 @@
|
||||||
"node": ">=0.8.0"
|
"node": ">=0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "7.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
|
||||||
|
"integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.20.0",
|
"version": "6.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@libsql/client": "^0.15.12",
|
"@libsql/client": "^0.15.12",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"axios": "^1.11.0",
|
|
||||||
"callsites": "^4.2.0",
|
"callsites": "^4.2.0",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"data-uri-to-buffer": "^6.0.2",
|
"data-uri-to-buffer": "^6.0.2",
|
||||||
|
|
@ -34,6 +33,7 @@
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"timeago.js": "^4.0.2",
|
"timeago.js": "^4.0.2",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
|
"undici": "^7.16.0",
|
||||||
"zod": "^4.0.14",
|
"zod": "^4.0.14",
|
||||||
"zod-openapi": "^5.3.0",
|
"zod-openapi": "^5.3.0",
|
||||||
"zod-validation-error": "^4.0.1"
|
"zod-validation-error": "^4.0.1"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import axios, { type AxiosInstance } from "axios";
|
|
||||||
import { ampApiUrl, appleMusicHomepageUrl, licenseApiUrl, webplaybackApiUrl } from "../constants/urls.js";
|
import { ampApiUrl, appleMusicHomepageUrl, licenseApiUrl, webplaybackApiUrl } from "../constants/urls.js";
|
||||||
import type { GetAlbumResponse, GetPlaylistResponse, GetSongResponse, SearchResponse } from "./types/responses.js";
|
import type { GetAlbumResponse, GetPlaylistResponse, GetSongResponse, SearchResponse } from "./types/responses.js";
|
||||||
import type { AlbumAttributesExtensionTypes, AnyAttributesExtensionTypes, SongAttributesExtensionTypes } from "./types/extensions.js";
|
import type { AlbumAttributesExtensionTypes, AnyAttributesExtensionTypes, SongAttributesExtensionTypes } from "./types/extensions.js";
|
||||||
|
|
@ -6,10 +5,12 @@ import { getToken } from "./token.js";
|
||||||
import { config, env } from "../config.js";
|
import { config, env } from "../config.js";
|
||||||
import { HttpException } from "../web/index.js";
|
import { HttpException } from "../web/index.js";
|
||||||
import type { RelationshipTypes } from "./types/relationships.js";
|
import type { RelationshipTypes } from "./types/relationships.js";
|
||||||
|
import { fetch, request } from "undici";
|
||||||
|
|
||||||
export default class AppleMusicApi {
|
export default class AppleMusicApi {
|
||||||
private storefront: string;
|
private storefront: string;
|
||||||
private http: AxiosInstance;
|
private headers: Headers;
|
||||||
|
private params: URLSearchParams;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
storefront: string,
|
storefront: string,
|
||||||
|
|
@ -17,21 +18,58 @@ export default class AppleMusicApi {
|
||||||
mediaUserToken: string
|
mediaUserToken: string
|
||||||
) {
|
) {
|
||||||
this.storefront = storefront;
|
this.storefront = storefront;
|
||||||
this.http = axios.create({
|
|
||||||
baseURL: ampApiUrl
|
|
||||||
});
|
|
||||||
|
|
||||||
this.http.defaults.headers.common["Origin"] = appleMusicHomepageUrl;
|
this.headers = new Headers();
|
||||||
this.http.defaults.headers.common["Media-User-Token"] = mediaUserToken;
|
this.headers.set("Origin", appleMusicHomepageUrl);
|
||||||
|
this.headers.set("Media-User-Token", mediaUserToken);
|
||||||
|
this.headers.set("x-apple-music-user-token", mediaUserToken);
|
||||||
|
this.headers.set("x-apple-renewal", "true");
|
||||||
|
|
||||||
// yeah dude. awesome
|
this.params = new URLSearchParams();
|
||||||
// https://stackoverflow.com/a/54636780
|
this.params.set("l", language);
|
||||||
this.http.defaults.params = {};
|
|
||||||
this.http.defaults.params["l"] = language;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async login(): Promise<void> {
|
public async login(): Promise<void> {
|
||||||
this.http.defaults.headers.common["Authorization"] = `Bearer ${await getToken(appleMusicHomepageUrl)}`;
|
this.headers.set("Authorization", `Bearer ${await getToken(appleMusicHomepageUrl)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: dedupe these functions
|
||||||
|
// TODO: also make their param/body/header stuff more modular
|
||||||
|
// please!!!!!
|
||||||
|
private async get<
|
||||||
|
T
|
||||||
|
> (link: string, params: Record<string, string | number | boolean> = {}): Promise<T> {
|
||||||
|
const url = new URL(link);
|
||||||
|
const urlParams = new URLSearchParams(this.params);
|
||||||
|
for (const entry of Object.entries(params)) { urlParams.set(entry[0], entry[1].toString()); }
|
||||||
|
url.search = urlParams.toString();
|
||||||
|
|
||||||
|
const response = await request(url, { headers: this.headers });
|
||||||
|
const json = await response.body.json();
|
||||||
|
return json as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: discover why it works when its fetch but not request
|
||||||
|
// what i mean by "works" is it doesn't return an error upstream that i can't replicate in cURL
|
||||||
|
// i'm so confused mannnn
|
||||||
|
private async post<
|
||||||
|
T
|
||||||
|
> (link: string, data: Record<string, string | number | boolean> = {}): Promise<T> {
|
||||||
|
const url = new URL(link);
|
||||||
|
const urlParams = new URLSearchParams(this.params);
|
||||||
|
url.search = urlParams.toString();
|
||||||
|
|
||||||
|
const headers = new Headers(this.headers);
|
||||||
|
headers.set("Content-Type", "application/json");
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
headers: headers
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
return json as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAlbum<
|
async getAlbum<
|
||||||
|
|
@ -42,12 +80,10 @@ export default class AppleMusicApi {
|
||||||
extend: T = [] as unknown[] as T,
|
extend: T = [] as unknown[] as T,
|
||||||
relationships: U = ["tracks"] as U
|
relationships: U = ["tracks"] as U
|
||||||
): Promise<GetAlbumResponse<T, U>> {
|
): Promise<GetAlbumResponse<T, U>> {
|
||||||
return (await this.http.get<GetAlbumResponse<T, U>>(`/v1/catalog/${this.storefront}/albums/${id}`, {
|
return await this.get<GetAlbumResponse<T, U>>(`${ampApiUrl}/v1/catalog/${this.storefront}/albums/${id}`, {
|
||||||
params: {
|
extend: extend.join(","),
|
||||||
extend: extend.join(","),
|
include: relationships.join(",")
|
||||||
include: relationships.join(",")
|
});
|
||||||
}
|
|
||||||
})).data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make it so you can get more than the first 100 tracks
|
// TODO: make it so you can get more than the first 100 tracks
|
||||||
|
|
@ -61,12 +97,10 @@ export default class AppleMusicApi {
|
||||||
extend: T = [] as never as T,
|
extend: T = [] as never as T,
|
||||||
relationships: U = ["tracks"] as U
|
relationships: U = ["tracks"] as U
|
||||||
): Promise<GetPlaylistResponse<T, U>> {
|
): Promise<GetPlaylistResponse<T, U>> {
|
||||||
return (await this.http.get<GetPlaylistResponse<T, U>>(`/v1/catalog/${this.storefront}/playlists/${id}`, {
|
return await this.get<GetPlaylistResponse<T, U>>(`${ampApiUrl}/v1/catalog/${this.storefront}/playlists/${id}`, {
|
||||||
params: {
|
extend: extend.join(","),
|
||||||
extend: extend.join(","),
|
include: relationships.join(",")
|
||||||
include: relationships.join(",")
|
});
|
||||||
}
|
|
||||||
})).data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSong<
|
async getSong<
|
||||||
|
|
@ -80,12 +114,10 @@ export default class AppleMusicApi {
|
||||||
extend: T = ["extendedAssetUrls"] as T,
|
extend: T = ["extendedAssetUrls"] as T,
|
||||||
relationships: U = ["albums"] as U
|
relationships: U = ["albums"] as U
|
||||||
): Promise<GetSongResponse<T, U>> {
|
): Promise<GetSongResponse<T, U>> {
|
||||||
return (await this.http.get<GetSongResponse<T, U>>(`/v1/catalog/${this.storefront}/songs/${id}`, {
|
return await this.get<GetSongResponse<T, U>>(`${ampApiUrl}/v1/catalog/${this.storefront}/songs/${id}`, {
|
||||||
params: {
|
extend: extend.join(","),
|
||||||
extend: extend.join(","),
|
include: relationships.join(",")
|
||||||
include: relationships.join(",")
|
});
|
||||||
}
|
|
||||||
})).data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add support for other types / abstract it for other types
|
// TODO: add support for other types / abstract it for other types
|
||||||
|
|
@ -100,19 +132,17 @@ export default class AppleMusicApi {
|
||||||
extend: T = [] as unknown[] as T,
|
extend: T = [] as unknown[] as T,
|
||||||
relationships: U = ["tracks"] as U
|
relationships: U = ["tracks"] as U
|
||||||
): Promise<SearchResponse<T, U>> {
|
): Promise<SearchResponse<T, U>> {
|
||||||
return (await this.http.get(`/v1/catalog/${this.storefront}/search`, {
|
return await this.get(`${ampApiUrl}/v1/catalog/${this.storefront}/search`, {
|
||||||
params: {
|
...this.addScopingParameters("albums", relationships, extend),
|
||||||
...this.addScopingParameters("albums", relationships, extend),
|
...{
|
||||||
...{
|
term: term,
|
||||||
term: term,
|
types: ["albums", "songs"].join(","), // adding "songs" makes search results have albums when searching song name
|
||||||
types: ["albums", "songs"].join(","), // adding "songs" makes search results have albums when searching song name
|
limit: limit,
|
||||||
limit: limit,
|
offset: offset,
|
||||||
offset: offset,
|
extend: extend.join(","),
|
||||||
extend: extend.join(","),
|
include: relationships.join(",")
|
||||||
include: relationships.join(",")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})).data;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getWebplayback(
|
async getWebplayback(
|
||||||
|
|
@ -122,18 +152,18 @@ export default class AppleMusicApi {
|
||||||
// as we know, we can't have fun things with "WOA" urls
|
// as we know, we can't have fun things with "WOA" urls
|
||||||
// https://files.catbox.moe/5oqolg.png (THE LINK WAS CENSORED?? TAKEN DOWN FROM CNN??)
|
// https://files.catbox.moe/5oqolg.png (THE LINK WAS CENSORED?? TAKEN DOWN FROM CNN??)
|
||||||
// https://files.catbox.moe/wjxwzk.png
|
// https://files.catbox.moe/wjxwzk.png
|
||||||
const res = await this.http.post(webplaybackApiUrl, {
|
const res = await this.post<WebplaybackResponse & { failureType?: string }>(webplaybackApiUrl, {
|
||||||
salableAdamId: trackId,
|
salableAdamId: trackId,
|
||||||
language: config.downloader.api.language
|
language: config.downloader.api.language
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.data?.failureType === "3077") {
|
if (res?.failureType === "3077") {
|
||||||
throw new HttpException(404, "track not found");
|
throw new HttpException(404, "track not found");
|
||||||
} else if (res.data?.failureType !== undefined) {
|
} else if (res?.failureType !== undefined) {
|
||||||
throw new HttpException(500, `upstream webplayback api error: ${res.data.failureType}`);
|
throw new HttpException(500, `upstream webplayback api error: ${res.failureType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.data;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getWidevineLicense(
|
async getWidevineLicense(
|
||||||
|
|
@ -141,19 +171,14 @@ export default class AppleMusicApi {
|
||||||
trackUri: string,
|
trackUri: string,
|
||||||
challenge: string
|
challenge: string
|
||||||
): Promise<WidevineLicenseResponse> {
|
): Promise<WidevineLicenseResponse> {
|
||||||
return (await this.http.post(licenseApiUrl, {
|
return (await this.post(licenseApiUrl, {
|
||||||
challenge: challenge,
|
challenge: challenge,
|
||||||
"key-system": "com.widevine.alpha",
|
"key-system": "com.widevine.alpha",
|
||||||
uri: trackUri,
|
uri: trackUri,
|
||||||
adamId: trackId,
|
adamId: trackId,
|
||||||
isLibrary: false,
|
isLibrary: false,
|
||||||
"user-initiated": true
|
"user-initiated": true
|
||||||
}, { headers: {
|
}));
|
||||||
// do these do anything.
|
|
||||||
// i'm including them anyway,,
|
|
||||||
"x-apple-music-user-token": this.http.defaults.headers.common["Media-User-Token"],
|
|
||||||
"x-apple-renewal": true
|
|
||||||
}})).data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper function to automatically add scoping parameters
|
// helper function to automatically add scoping parameters
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { request } from "undici";
|
||||||
import * as log from "../log.js";
|
import * as log from "../log.js";
|
||||||
|
|
||||||
// basically, i don't want to pay 100 dollars for a dev token to the official API
|
// basically, i don't want to pay 100 dollars for a dev token to the official API
|
||||||
|
|
@ -5,8 +6,8 @@ import * as log from "../log.js";
|
||||||
// thanks to this guy complaining to apple for telling us this! https://developer.apple.com/forums/thread/702228
|
// thanks to this guy complaining to apple for telling us this! https://developer.apple.com/forums/thread/702228
|
||||||
// apple says "any other method may be blocked at any time" (posted in mar 2022, most likely not happening)
|
// apple says "any other method may be blocked at any time" (posted in mar 2022, most likely not happening)
|
||||||
export async function getToken(baseUrl: string): Promise<string> {
|
export async function getToken(baseUrl: string): Promise<string> {
|
||||||
const indexResponse = await fetch(baseUrl);
|
const indexResponse = await request(baseUrl);
|
||||||
const indexBody = await indexResponse.text();
|
const indexBody = await indexResponse.body.text();
|
||||||
|
|
||||||
const jsRegex = /\/assets\/index-legacy-[^/]+\.js/;
|
const jsRegex = /\/assets\/index-legacy-[^/]+\.js/;
|
||||||
const jsPath = indexBody.match(jsRegex)?.[0];
|
const jsPath = indexBody.match(jsRegex)?.[0];
|
||||||
|
|
@ -15,8 +16,8 @@ export async function getToken(baseUrl: string): Promise<string> {
|
||||||
throw new Error("could not match for the index javascript file!");
|
throw new Error("could not match for the index javascript file!");
|
||||||
}
|
}
|
||||||
|
|
||||||
const jsResponse = await fetch(baseUrl + jsPath);
|
const jsResponse = await request(baseUrl + jsPath);
|
||||||
const jsBody = await jsResponse.text();
|
const jsBody = await jsResponse.body.text();
|
||||||
|
|
||||||
// the token is actually a base64-encoded JWT
|
// the token is actually a base64-encoded JWT
|
||||||
// `eyJh` === `{"a`, which is the beginning of a JWT (a is the start of alg)
|
// `eyJh` === `{"a`, which is the beginning of a JWT (a is the start of alg)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { createDecipheriv } from "node:crypto";
|
||||||
import type { AlbumAttributes } from "../appleMusicApi/types/attributes.js";
|
import type { AlbumAttributes } from "../appleMusicApi/types/attributes.js";
|
||||||
import { pipeline } from "node:stream/promises";
|
import { pipeline } from "node:stream/promises";
|
||||||
import { createWriteStream } from "node:fs";
|
import { createWriteStream } from "node:fs";
|
||||||
|
import { request } from "undici";
|
||||||
|
|
||||||
export async function downloadSongFile(streamUrl: string, decryptionKey: string, songCodec: RegularCodecType | WebplaybackCodecType, songResponse: GetSongResponse<[], ["albums"]>): Promise<string> {
|
export async function downloadSongFile(streamUrl: string, decryptionKey: string, songCodec: RegularCodecType | WebplaybackCodecType, songResponse: GetSongResponse<[], ["albums"]>): Promise<string> {
|
||||||
let baseOutputName = streamUrl.match(/(?:.*\/)\s*(\S*?)[.?]/)?.[1];
|
let baseOutputName = streamUrl.match(/(?:.*\/)\s*(\S*?)[.?]/)?.[1];
|
||||||
|
|
@ -59,8 +60,8 @@ export async function downloadSongFile(streamUrl: string, decryptionKey: string,
|
||||||
// TODO: less mem alloc/access
|
// TODO: less mem alloc/access
|
||||||
// TODO: use actual atom scanning. what if the magic bytes appear in a sample
|
// TODO: use actual atom scanning. what if the magic bytes appear in a sample
|
||||||
export async function fetchAndDecryptStreamSegment(segmentUrl: string, decryptionKey: string, fetchLength: number, offset: number): Promise<Uint8Array> {
|
export async function fetchAndDecryptStreamSegment(segmentUrl: string, decryptionKey: string, fetchLength: number, offset: number): Promise<Uint8Array> {
|
||||||
const response = await fetch(segmentUrl, { headers: { "range": `bytes=${offset}-${offset + fetchLength - 1}` }});
|
const response = await request(segmentUrl, { headers: { "range": `bytes=${offset}-${offset + fetchLength - 1}` }});
|
||||||
const file = new Uint8Array(await response.arrayBuffer());
|
const file = new Uint8Array(await response.body.bytes());
|
||||||
|
|
||||||
// this translates to "moof"
|
// this translates to "moof"
|
||||||
const moof = new Uint8Array([0x6D, 0x6F, 0x6F, 0x66]);
|
const moof = new Uint8Array([0x6D, 0x6F, 0x6F, 0x66]);
|
||||||
|
|
@ -121,12 +122,9 @@ export async function downloadAlbumCover(albumAttributes: AlbumAttributes<[]>):
|
||||||
const imagePath = path.join(config.downloader.cache.directory, imageFileName);
|
const imagePath = path.join(config.downloader.cache.directory, imageFileName);
|
||||||
|
|
||||||
if (await isFileCached(imageFileName) === false) {
|
if (await isFileCached(imageFileName) === false) {
|
||||||
const response = await fetch(url);
|
const response = await request(url);
|
||||||
|
|
||||||
if (!response.ok) { throw new Error(`failed to fetch artwork: ${response.status}`); }
|
await pipeline(response.body, createWriteStream(imagePath));
|
||||||
if (!response.body) { throw new Error("no response body for artwork!"); }
|
|
||||||
|
|
||||||
await pipeline(response.body as ReadableStream, createWriteStream(imagePath));
|
|
||||||
await addFileToCache(imageFileName);
|
await addFileToCache(imageFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import * as log from "../log.js";
|
import * as log from "../log.js";
|
||||||
import type { SongAttributes } from "../appleMusicApi/types/attributes.js";
|
import type { SongAttributes } from "../appleMusicApi/types/attributes.js";
|
||||||
import hls, { Item } from "parse-hls";
|
import hls, { Item } from "parse-hls";
|
||||||
import axios from "axios";
|
|
||||||
import { widevine, playready, fairplay } from "../constants/keyFormats.js";
|
import { widevine, playready, fairplay } from "../constants/keyFormats.js";
|
||||||
import { songCodecRegex } from "../constants/codecs.js";
|
import { songCodecRegex } from "../constants/codecs.js";
|
||||||
import type { WebplaybackResponse } from "appleMusicApi/index.js";
|
import type { WebplaybackResponse } from "appleMusicApi/index.js";
|
||||||
import type { RegularCodecType, WebplaybackCodecType } from "./codecType.js";
|
import type { RegularCodecType, WebplaybackCodecType } from "./codecType.js";
|
||||||
|
import { request } from "undici";
|
||||||
|
|
||||||
// why is this private
|
// why is this private
|
||||||
// i wish pain on the person who wrote this /j :smile:
|
// i wish pain on the person who wrote this /j :smile:
|
||||||
|
|
@ -47,8 +47,8 @@ export default class StreamInfo {
|
||||||
log.warn("if there is a failure--use a codec that uses the webplayback method");
|
log.warn("if there is a failure--use a codec that uses the webplayback method");
|
||||||
|
|
||||||
const m3u8Url = trackMetadata.extendedAssetUrls.enhancedHls;
|
const m3u8Url = trackMetadata.extendedAssetUrls.enhancedHls;
|
||||||
const m3u8 = await axios.get(m3u8Url, { responseType: "text" });
|
const m3u8 = await request(m3u8Url);
|
||||||
const m3u8Parsed = hls.default.parse(m3u8.data);
|
const m3u8Parsed = hls.default.parse(await m3u8.body.text());
|
||||||
|
|
||||||
const drmInfos = getDrmInfos(m3u8Parsed);
|
const drmInfos = getDrmInfos(m3u8Parsed);
|
||||||
const assetInfos = getAssetInfos(m3u8Parsed);
|
const assetInfos = getAssetInfos(m3u8Parsed);
|
||||||
|
|
@ -59,8 +59,8 @@ export default class StreamInfo {
|
||||||
const drmIds = assetInfos[variantId]["AUDIO-SESSION-KEY-IDS"];
|
const drmIds = assetInfos[variantId]["AUDIO-SESSION-KEY-IDS"];
|
||||||
|
|
||||||
const correctM3u8Url = m3u8Url.substring(0, m3u8Url.lastIndexOf("/")) + "/" + playlist.uri;
|
const correctM3u8Url = m3u8Url.substring(0, m3u8Url.lastIndexOf("/")) + "/" + playlist.uri;
|
||||||
const correctM3u8 = await axios.get(correctM3u8Url, { responseType: "text" });
|
const correctM3u8 = await request(correctM3u8Url);
|
||||||
const correctM3u8Parsed = hls.default.parse(correctM3u8.data);
|
const correctM3u8Parsed = hls.default.parse(await correctM3u8.body.text());
|
||||||
|
|
||||||
const primaryFileUrl = getPrimaryFileUrl(m3u8Parsed);
|
const primaryFileUrl = getPrimaryFileUrl(m3u8Parsed);
|
||||||
|
|
||||||
|
|
@ -97,8 +97,8 @@ export default class StreamInfo {
|
||||||
const trackId = song.songId;
|
const trackId = song.songId;
|
||||||
|
|
||||||
const m3u8Url = asset.URL;
|
const m3u8Url = asset.URL;
|
||||||
const m3u8 = await axios.get(m3u8Url, { responseType: "text" });
|
const m3u8 = await request(m3u8Url);
|
||||||
const m3u8Parsed = hls.default.parse(m3u8.data);
|
const m3u8Parsed = hls.default.parse(await m3u8.body.text());
|
||||||
|
|
||||||
const primaryFileUrl = getPrimaryFileUrl(m3u8Parsed);
|
const primaryFileUrl = getPrimaryFileUrl(m3u8Parsed);
|
||||||
|
|
||||||
|
|
|
||||||
42
src/index.ts
42
src/index.ts
|
|
@ -3,14 +3,33 @@ import process from "node:process";
|
||||||
import * as log from "./log.js";
|
import * as log from "./log.js";
|
||||||
import { appleMusicApi } from "./appleMusicApi/index.js";
|
import { appleMusicApi } from "./appleMusicApi/index.js";
|
||||||
import { app } from "./web/index.js";
|
import { app } from "./web/index.js";
|
||||||
|
// @ts-expect-error: cacheStores should exist--it does not
|
||||||
|
// TODO: file an issue on undici and remove this when fixed
|
||||||
|
// DONE: issue filed... https://github.com/nodejs/undici/issues/4614
|
||||||
|
// OKAY: it was "resolved". just have to wait for the next release
|
||||||
|
import { Agent, interceptors, setGlobalDispatcher, cacheStores } from "undici";
|
||||||
|
|
||||||
await appleMusicApi.login().catch((err) => {
|
setGlobalDispatcher(new Agent().compose([
|
||||||
log.error("failed to login to apple music api");
|
interceptors.responseError(),
|
||||||
|
interceptors.redirect(),
|
||||||
|
interceptors.decompress(),
|
||||||
|
// TODO: configurable cache sizes?
|
||||||
|
// these values are pretty nice for non-binary (lol) data
|
||||||
|
interceptors.cache({ store: new cacheStores.MemoryCacheStore({
|
||||||
|
maxSize: 50 * 1024 * 1024, // 5mb
|
||||||
|
maxCount: 1000,
|
||||||
|
maxEntrySize: 5 * 1024 // 5kb
|
||||||
|
})})
|
||||||
|
]));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await appleMusicApi.login();
|
||||||
|
log.info("logged in to apple music api");
|
||||||
|
} catch (err) {
|
||||||
|
log.error("failed to login to apple music api!");
|
||||||
log.error(err);
|
log.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}).finally(() => {
|
}
|
||||||
log.info("logged in to apple music api");
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const listener = app.listen(config.server.port, () => {
|
const listener = app.listen(config.server.port, () => {
|
||||||
|
|
@ -24,18 +43,7 @@ try {
|
||||||
else { log.info(`hosting on http://localhost:${address.port}`); }
|
else { log.info(`hosting on http://localhost:${address.port}`); }
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error("failed to start server");
|
log.error("failed to start server!");
|
||||||
log.error(err);
|
log.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on("uncaughtException", (err) => {
|
|
||||||
log.error("uncaught exception!");
|
|
||||||
log.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
process.on("unhandledRejection", (err) => {
|
|
||||||
log.error("unhandled rejection!");
|
|
||||||
log.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
|
||||||
18
src/log.ts
18
src/log.ts
|
|
@ -89,3 +89,21 @@ export function debug(...message: unknown[]): void { log(Level.Debug, ...message
|
||||||
export function info(...message: unknown[]): void { log(Level.Info, ...message); }
|
export function info(...message: unknown[]): void { log(Level.Info, ...message); }
|
||||||
export function warn(...message: unknown[]): void { log(Level.Warn, ...message); }
|
export function warn(...message: unknown[]): void { log(Level.Warn, ...message); }
|
||||||
export function error(...message: unknown[]): void { log(Level.Error, ...message); }
|
export function error(...message: unknown[]): void { log(Level.Error, ...message); }
|
||||||
|
|
||||||
|
// custom interceptors for uncaught exceptions and unhandled rejections
|
||||||
|
// additionally, also wrap warnings (but, make the experimental warning shut up)
|
||||||
|
process.on("uncaughtException", (err) => {
|
||||||
|
error("uncaught exception!");
|
||||||
|
error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
process.on("unhandledRejection", (err) => {
|
||||||
|
error("unhandled rejection!");
|
||||||
|
error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
process.removeAllListeners("warning");
|
||||||
|
process.on("warning", (err) => {
|
||||||
|
if (err.name === "ExperimentalWarning") { return; }
|
||||||
|
warn(err);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,11 @@ router.get(path, async (req, res, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptionKey =
|
const decryptionKey =
|
||||||
await getKeyFromCache(id, codecType.codecType) ||
|
await getKeyFromCache(id, codecType.codecType) ??
|
||||||
await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId);
|
await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId);
|
||||||
await addKeyToCache(id, codecType.codecType, decryptionKey);
|
await addKeyToCache(id, codecType.codecType, decryptionKey);
|
||||||
|
|
||||||
|
// TODO: stream to user
|
||||||
const filePath = await downloadSongFile(streamInfo.streamUrl, decryptionKey, codecType.codecType, trackMetadata);
|
const filePath = await downloadSongFile(streamInfo.streamUrl, decryptionKey, codecType.codecType, trackMetadata);
|
||||||
const fileExt = "." + filePath.split(".").at(-1) as string; // safe cast, filePath is always a valid path
|
const fileExt = "." + filePath.split(".").at(-1) as string; // safe cast, filePath is always a valid path
|
||||||
const fileName = formatSongForFs(trackAttributes) + fileExt;
|
const fileName = formatSongForFs(trackAttributes) + fileExt;
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ router.get(path, async (req, res, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptionKey =
|
const decryptionKey =
|
||||||
await getKeyFromCache(trackId, codecType.codecType) ||
|
await getKeyFromCache(trackId, codecType.codecType) ??
|
||||||
await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId);
|
await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId);
|
||||||
await addKeyToCache(trackId, codecType.codecType, decryptionKey);
|
await addKeyToCache(trackId, codecType.codecType, decryptionKey);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,6 @@ paths[path] = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: cache the decryption key for a while, so we don't have to fetch it every time
|
|
||||||
// the way we could do that is store track id + codec mapped to a decryption key (in memory? for like how long? maybe have an expiry?)
|
|
||||||
router.get(path, async (req, res, next) => {
|
router.get(path, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id, originalMp4, codec } = (await validate(req, schema)).query;
|
const { id, originalMp4, codec } = (await validate(req, schema)).query;
|
||||||
|
|
@ -68,7 +66,7 @@ router.get(path, async (req, res, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptionKey =
|
const decryptionKey =
|
||||||
await getKeyFromCache(id, codecType.codecType) ||
|
await getKeyFromCache(id, codecType.codecType) ??
|
||||||
await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId);
|
await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId);
|
||||||
await addKeyToCache(id, codecType.codecType, decryptionKey);
|
await addKeyToCache(id, codecType.codecType, decryptionKey);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import formatDuration from "format-duration";
|
||||||
import { back, front } from "./endpoints/index.js";
|
import { back, front } from "./endpoints/index.js";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
import { fromZodError } from "zod-validation-error";
|
import { fromZodError } from "zod-validation-error";
|
||||||
import { AxiosError } from "axios";
|
import { errors as undiciErrors } from "undici";
|
||||||
import { env } from "../config.js";
|
import { env } from "../config.js";
|
||||||
import { createOpenApiDocument } from "./openApi.js";
|
import { createOpenApiDocument } from "./openApi.js";
|
||||||
|
|
||||||
|
|
@ -54,10 +54,11 @@ app.use((req, _res, next) => {
|
||||||
|
|
||||||
// ex. if the apple music api returns a 403, we want to return a 403
|
// ex. if the apple music api returns a 403, we want to return a 403
|
||||||
// this is so damn useful, i'm so glad i thought of this
|
// this is so damn useful, i'm so glad i thought of this
|
||||||
app.use((err: AxiosError, _req: Request, _res: Response, next: NextFunction) => {
|
// TODO: make this only happen on AM api? doesn't make too much sense otherwise
|
||||||
if (err instanceof AxiosError && err.response) {
|
app.use((err: undiciErrors.ResponseError, _req: Request, _res: Response, next: NextFunction) => {
|
||||||
const status = err.response.status;
|
if (err instanceof undiciErrors.ResponseError) {
|
||||||
const message = `upstream api error: ${err.response.status}`;
|
const status = err.statusCode;
|
||||||
|
const message = `fetch error/upstream error: ${err.statusCode}. file an issue if unexpected/reoccuring`;
|
||||||
|
|
||||||
next(new HttpException(status, message));
|
next(new HttpException(status, message));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue