streaming!!, oh and file names, linting ig..

This commit is contained in:
Reid 2025-08-15 01:40:21 -07:00
parent 7b15834f17
commit f2800f13c8
Signed by: reidlab
GPG key ID: DAF5EAF6665839FD
23 changed files with 1195 additions and 254 deletions

View file

@ -40,8 +40,8 @@ currently you can only get basic widevine ones, everything related to playready
guaranteed formats to work include:
- aac-legacy
- aac-he-legacy
- `aac_legacy`
- `aac_he_legacy`
## screenshots

View file

@ -1,33 +1,55 @@
import typescriptEslint from "typescript-eslint";
import stylistic from "@stylistic/eslint-plugin";
export default [
...typescriptEslint.configs.strict,
...typescriptEslint.configs.stylistic,
{
plugins: {
"@typescript-eslint": typescriptEslint.plugin,
"@stylistic": stylistic
}
},
{
ignores: [
"**/dist/*",
"**/result/*",
"**/node_modules/*"
"**/dist/**",
"**/result/**",
"**/node_modules/**"
]
},
...typescriptEslint.configs.strict,
{
rules: {
"quotes": ["error", "double"],
"semi": ["error", "always"],
"comma-dangle": ["error", "never"],
"@stylistic/indent": ["error", 4],
"@stylistic/quotes": ["error", "double"],
"@stylistic/semi": ["error", "always"],
"@stylistic/comma-dangle": ["error", "never"],
// TODO: find a rule to make it so that relative imports are needed
// this is because those pass type checking, and fails at runtime
// not very typescript-coded (typescript shouldnt require runtime debugging, thats the point of it)
// TODO: make imports forced to be dynamic
// TODO: find a rule to make seperators on interfaces consistent
// it... let's just say... it pmo
"@stylistic/member-delimiter-style": [
"error",
{
multilineDetection: "brackets",
multiline: {
delimiter: "semi",
requireLast: true
},
singleline: {
delimiter: "comma",
requireLast: false
}
}
],
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
varsIgnorePattern: "^_",
argsIgnorePattern: "^_"
argsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
caughtErrors: "all",
destructuredArrayIgnorePattern: "^_"
}
]
}

View file

@ -25,7 +25,7 @@
# uncomment this and let the build fail, then get the current hash
# very scuffed but endorsed!
# npmDepsHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
npmDepsHash = "sha256-1p/QMZQoFwXxXxlTYTtdHQ3O7p+RSzD3RpoKRB40CHg=";
npmDepsHash = "sha256-lvueqcSBjtt9RSMwq2NWCAVT0NrZwDmhEYkjtdOs7js=";
nativeBuildInputs = with pkgs; [ makeWrapper ];
@ -40,7 +40,8 @@
--prefix PATH : ${makeBinPath buildInputs} \
--add-flags "$out/dist/src/index.js" \
--set VIEWS_DIR $out/views \
--set PUBLIC_DIR $out/public
--set PUBLIC_DIR $out/public \
--set NODE_ENV production
runHook postInstall
'';
@ -165,8 +166,8 @@
preStart = ''
config='${cfg.stateDir}/config.toml'
cp -f '${toml.generate "config.toml" cfg.config}' "$config"
''; # TODO: symlink instead of cp, shouldn't matter for reproducibility since its preStart but whatever
ln -sf '${toml.generate "config.toml" cfg.config}' "$config"
'';
serviceConfig = {
Type = "simple";

599
package-lock.json generated
View file

@ -29,20 +29,21 @@
"zod-validation-error": "^4.0.1"
},
"devDependencies": {
"@stylistic/eslint-plugin": "^3.1.0",
"@types/express": "^5.0.3",
"@types/source-map-support": "^0.5.10",
"@types/swagger-ui-express": "^4.1.8",
"@typescript-eslint/parser": "^7.12.0",
"concurrently": "^9.2.0",
"eslint": "^8.57.0",
"eslint": "^8.57.1",
"typescript": "^5.9.2",
"typescript-eslint": "^7.13.0"
"typescript-eslint": "^8.39.1"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
"integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -393,6 +394,70 @@
"hasInstallScript": true,
"license": "Apache-2.0"
},
"node_modules/@stylistic/eslint-plugin": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-3.1.0.tgz",
"integrity": "sha512-pA6VOrOqk0+S8toJYhQGv2MWpQQR0QpeUo9AhNkC49Y26nxBQ/nH1rta9bUU1rPw2fJ1zZEMV5oCX5AazT7J2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/utils": "^8.13.0",
"eslint-visitor-keys": "^4.2.0",
"espree": "^10.3.0",
"estraverse": "^5.3.0",
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"peerDependencies": {
"eslint": ">=8.40.0"
}
},
"node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@stylistic/eslint-plugin/node_modules/espree": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.15.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@stylistic/eslint-plugin/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@ -520,40 +585,6 @@
"@types/serve-static": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
"integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.18.0",
"@typescript-eslint/type-utils": "7.18.0",
"@typescript-eslint/utils": "7.18.0",
"@typescript-eslint/visitor-keys": "7.18.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
"ts-api-utils": "^1.3.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^7.0.0",
"eslint": "^8.56.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz",
@ -583,6 +614,42 @@
}
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz",
"integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.39.1",
"@typescript-eslint/types": "^8.39.1",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/project-service/node_modules/@typescript-eslint/types": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz",
"integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
@ -601,32 +668,133 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz",
"integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==",
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz",
"integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.18.0",
"@typescript-eslint/utils": "7.18.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.56.0"
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz",
"integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/typescript-estree": "8.39.1",
"@typescript-eslint/utils": "8.39.1",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz",
"integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz",
"integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.39.1",
"@typescript-eslint/tsconfig-utils": "8.39.1",
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.1",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz",
"integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.1",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/eslint-visitor-keys": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"node_modules/@typescript-eslint/types": {
@ -673,26 +841,132 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
"integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz",
"integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.18.0",
"@typescript-eslint/types": "7.18.0",
"@typescript-eslint/typescript-estree": "7.18.0"
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.39.1",
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/typescript-estree": "8.39.1"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.56.0"
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz",
"integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz",
"integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz",
"integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.39.1",
"@typescript-eslint/tsconfig-utils": "8.39.1",
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.1",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz",
"integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.1",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/utils/node_modules/eslint-visitor-keys": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@typescript-eslint/utils/node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
@ -755,9 +1029,9 @@
}
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
@ -3518,30 +3792,197 @@
}
},
"node_modules/typescript-eslint": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.18.0.tgz",
"integrity": "sha512-PonBkP603E3tt05lDkbOMyaxJjvKqQrXsnow72sVeOFINDE/qNmnnd+f9b4N+U7W6MXnnYyrhtmF2t08QWwUbA==",
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.1.tgz",
"integrity": "sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"@typescript-eslint/utils": "7.18.0"
"@typescript-eslint/eslint-plugin": "8.39.1",
"@typescript-eslint/parser": "8.39.1",
"@typescript-eslint/typescript-estree": "8.39.1",
"@typescript-eslint/utils": "8.39.1"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.56.0"
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz",
"integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.39.1",
"@typescript-eslint/type-utils": "8.39.1",
"@typescript-eslint/utils": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.1",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.39.1",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz",
"integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.39.1",
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/typescript-estree": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.1",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz",
"integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/types": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz",
"integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz",
"integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.39.1",
"@typescript-eslint/tsconfig-utils": "8.39.1",
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.1",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz",
"integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.1",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/typescript-eslint/node_modules/eslint-visitor-keys": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/typescript-eslint/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/typescript-eslint/node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"node_modules/uglify-js": {

View file

@ -32,13 +32,14 @@
"zod-validation-error": "^4.0.1"
},
"devDependencies": {
"@stylistic/eslint-plugin": "^3.1.0",
"@types/express": "^5.0.3",
"@types/source-map-support": "^0.5.10",
"@types/swagger-ui-express": "^4.1.8",
"@typescript-eslint/parser": "^7.12.0",
"concurrently": "^9.2.0",
"eslint": "^8.57.0",
"eslint": "^8.57.1",
"typescript": "^5.9.2",
"typescript-eslint": "^7.13.0"
"typescript-eslint": "^8.39.1"
}
}

View file

@ -95,8 +95,8 @@ export default class AppleMusicApi {
U extends RelationshipTypes<T> = ["tracks"]
> (
term: string,
limit: number = 25,
offset: number = 0,
limit = 25,
offset = 0,
extend: T = [] as unknown[] as T,
relationships: U = ["tracks"] as U
): Promise<SearchResponse<T, U>> {
@ -118,10 +118,10 @@ export default class AppleMusicApi {
async getWebplayback(
trackId: string
): Promise<WebplaybackResponse> {
// this is one of those endpoints that returns a 200
// no matter what happens, even if theres an error
// so we gotta do this stuuuupid hack
// TODO: find a better way to do this
// no way around this
// 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/wjxwzk.png
const res = await this.http.post(webplaybackApiUrl, {
salableAdamId: trackId,
language: config.downloader.api.language
@ -162,8 +162,8 @@ export default class AppleMusicApi {
names: string | string[],
relationships: string[],
extend: string[]
): { [scope: string]: string } {
const params: { [scope: string]: string } = {};
): Record<string, string> {
const params: Record<string, string> = {};
for (const name of Array.isArray(names) ? names : [names]) {
for (const relationship of relationships) { params[`include[${name}]`] = relationship; }
@ -179,5 +179,5 @@ export const appleMusicApi = new AppleMusicApi(env.ITUA, config.downloader.api.l
// these are super special types
// i'm not putting this in the ./types folder.
// maybe ltr bleh
export type WebplaybackResponse = { songList: { assets: { flavor: string, URL: string }[], songId: string }[] };
export type WidevineLicenseResponse = { license: string | undefined };
export interface WebplaybackResponse { songList: { assets: { flavor: string, URL: string }[], songId: string }[] };
export interface WidevineLicenseResponse { license: string | undefined };

View file

@ -27,7 +27,7 @@ export async function getToken(baseUrl: string): Promise<string> {
throw new Error("could not find match for the api token in the index javascript file");
}
log.debug("got token");
log.debug("got api token");
return token;
}

View file

@ -6,68 +6,68 @@ import type {
} from "./extensions.js";
export type AlbumAttributes<
T extends AlbumAttributesExtensionTypes,
T extends AlbumAttributesExtensionTypes
> = {
artistName: string
artwork: Artwork
contentRating?: string
copyright?: string
editorialNotes?: EditorialNotes
genreNames: string[]
isCompilation: boolean
isComplete: boolean
isMasteredForItunes: boolean
isSingle: boolean
name: string
playParams?: PlayParameters
recordLabel?: string
releaseDate?: string
trackCount: number
upc?: string
url: string
artistName: string;
artwork: Artwork;
contentRating?: string;
copyright?: string;
editorialNotes?: EditorialNotes;
genreNames: string[];
isCompilation: boolean;
isComplete: boolean;
isMasteredForItunes: boolean;
isSingle: boolean;
name: string;
playParams?: PlayParameters;
recordLabel?: string;
releaseDate?: string;
trackCount: number;
upc?: string;
url: string;
}
& Pick<AlbumAttributesExtensionMap, T[number]>
& Pick<AlbumAttributesExtensionMap, T[number]>;
export type PlaylistAttributes<
T extends PlaylistAttributesExtensionTypes,
T extends PlaylistAttributesExtensionTypes
> = {
artwork?: Artwork
curatorName: string
description?: DescriptionAttribute,
isChart: boolean,
lastModifiedDate?: string
name: string
playlistType: string
playParams?: PlayParameters
url: string
artwork?: Artwork;
curatorName: string;
description?: DescriptionAttribute;
isChart: boolean;
lastModifiedDate?: string;
name: string;
playlistType: string;
playParams?: PlayParameters;
url: string;
}
& Pick<PlaylistAttributesExtensionMap, T[number]>
& Pick<PlaylistAttributesExtensionMap, T[number]>;
export type SongAttributes<
T extends SongAttributesExtensionTypes,
T extends SongAttributesExtensionTypes
> = {
albumName: string
artistName: string
artwork: Artwork
attribution?: string
composerName?: string
contentRating?: string
discNumber?: number
durationInMillis: number
editorialNotes?: EditorialNotes
genreNames: string[]
hasLyrics: boolean
isAppleDigitalMaster: boolean
isrc?: string
movementCount?: number
movementName?: string
movementNumber?: number
name: string
playParams?: PlayParameters
previews: Preview[]
releaseDate?: string
trackNumber?: number
url: string
workName?: string
albumName: string;
artistName: string;
artwork: Artwork;
attribution?: string;
composerName?: string;
contentRating?: string;
discNumber?: number;
durationInMillis: number;
editorialNotes?: EditorialNotes;
genreNames: string[];
hasLyrics: boolean;
isAppleDigitalMaster: boolean;
isrc?: string;
movementCount?: number;
movementName?: string;
movementNumber?: number;
name: string;
playParams?: PlayParameters;
previews: Preview[];
releaseDate?: string;
trackNumber?: number;
url: string;
workName?: string;
}
& Pick<SongAttributesExtensionMap, T[number]>
& Pick<SongAttributesExtensionMap, T[number]>;

View file

@ -3,27 +3,27 @@ export type AnyAttributesExtensionTypes = AnyAttributesExtensionType[];
export type AlbumAttributesExtensionType = keyof AlbumAttributesExtensionMap;
export type AlbumAttributesExtensionTypes = AlbumAttributesExtensionType[];
export type AlbumAttributesExtensionMap = {
artistUrl: string
audioVariants?: string[]
export interface AlbumAttributesExtensionMap {
artistUrl: string;
audioVariants?: string[];
}
export type PlaylistAttributesExtensionType = keyof PlaylistAttributesExtensionMap;
export type PlaylistAttributesExtensionTypes = PlaylistAttributesExtensionType[];
export type PlaylistAttributesExtensionMap = {
trackTypes: string[]
export interface PlaylistAttributesExtensionMap {
trackTypes: string[];
}
export type SongAttributesExtensionType = keyof SongAttributesExtensionMap;
export type SongAttributesExtensionTypes = SongAttributesExtensionType[];
export type SongAttributesExtensionMap = {
artistUrl: string,
audioVariants?: string[],
export interface SongAttributesExtensionMap {
artistUrl: string;
audioVariants?: string[];
extendedAssetUrls: {
plus: string,
lightweight: string
superLightweight: string
lightweightPlus: string
enhancedHls: string
}
plus: string;
lightweight: string;
superLightweight: string;
lightweightPlus: string;
enhancedHls: string;
};
}

View file

@ -1,38 +1,38 @@
// https://developer.apple.com/documentation/applemusicapi/descriptionattribute
export interface DescriptionAttribute {
short?: string
standard: string
short?: string;
standard: string;
}
// https://developer.apple.com/documentation/applemusicapi/artwork
export interface Artwork {
bgColor?: string
height: number
width: number
textColor1?: string
textColor2?: string
textColor3?: string
textColor4?: string
url: string
bgColor?: string;
height: number;
width: number;
textColor1?: string;
textColor2?: string;
textColor3?: string;
textColor4?: string;
url: string;
}
// https://developer.apple.com/documentation/applemusicapi/editorialnotes
export interface EditorialNotes {
short?: string
standard?: string
name?: string
tagline?: string
short?: string;
standard?: string;
name?: string;
tagline?: string;
}
// https://developer.apple.com/documentation/applemusicapi/playparameters
export interface PlayParameters {
id: string
kind: string
id: string;
kind: string;
}
// https://developer.apple.com/documentation/applemusicapi/preview
export interface Preview {
artwork?: Artwork
url: string
hlsUrl?: string
artwork?: Artwork;
url: string;
hlsUrl?: string;
}

View file

@ -5,23 +5,23 @@ import type { AlbumAttributes, PlaylistAttributes, SongAttributes } from "./attr
import type { AlbumAttributesExtensionTypes, AnyAttributesExtensionTypes, PlaylistAttributesExtensionTypes, SongAttributesExtensionTypes } from "./extensions.js";
// TODO: have something like this for every resource
export type Relationship<T> = {
export interface Relationship<T> {
href?: string;
next?: string;
data: {
// TODO: there is extra types here (id, type, etc) i just can't cba to add them lol
// probably not important ! ahahahah
// seems to be the same basic "resource" pattern i'm starting to notice (id(?), href, type, meta (not included), etc)
attributes: T
}[]
attributes: T;
}[];
}
export type RelationshipType<T extends AnyAttributesExtensionTypes> = keyof RelationshipTypeMap<T>;
export type RelationshipTypes<T extends AnyAttributesExtensionTypes> = RelationshipType<T>[];
export type RelationshipTypeMap<T extends AnyAttributesExtensionTypes> = {
albums: AlbumAttributes<Extract<T, AlbumAttributesExtensionTypes>>,
export interface RelationshipTypeMap<T extends AnyAttributesExtensionTypes> {
albums: AlbumAttributes<Extract<T, AlbumAttributesExtensionTypes>>;
// TODO: from what i can tell, playlists can NOT be used as a relationship type? kept in case
playlists: PlaylistAttributes<Extract<T, PlaylistAttributesExtensionTypes>>,
playlists: PlaylistAttributes<Extract<T, PlaylistAttributesExtensionTypes>>;
// TODO: tracks can also be music videos, uh oh.
tracks: SongAttributes<Extract<T, SongAttributesExtensionTypes>>
tracks: SongAttributes<Extract<T, SongAttributesExtensionTypes>>;
}

View file

@ -14,18 +14,18 @@ export interface GetAlbumResponse<
> {
// https://developer.apple.com/documentation/applemusicapi/albums
data: {
id: string,
type: "albums",
href: string,
id: string;
type: "albums";
href: string;
// https://developer.apple.com/documentation/applemusicapi/albums/attributes-data.dictionary
attributes: AlbumAttributes<T>,
attributes: AlbumAttributes<T>;
// https://developer.apple.com/documentation/applemusicapi/albums/relationships-data.dictionary
relationships: {
[K in U[number]]: Relationship<
K extends RelationshipType<T> ? RelationshipTypeMap<T>[K] : never
>
}
}[]
};
}[];
}
// https://developer.apple.com/documentation/applemusicapi/get-a-catalog-playlist
@ -35,18 +35,18 @@ export interface GetPlaylistResponse<
> {
// https://developer.apple.com/documentation/applemusicapi/playlists
data: {
id: string
type: "playlists"
href: string
id: string;
type: "playlists";
href: string;
// https://developer.apple.com/documentation/applemusicapi/playlists/attributes-data.dictionary
attributes: SongAttributes<T>
attributes: SongAttributes<T>;
// https://developer.apple.com/documentation/applemusicapi/playlists/relationships-data.dictionary
relationships: {
[K in U[number]]: Relationship<
K extends RelationshipType<T> ? RelationshipTypeMap<T>[K] : never
>
}
}[]
};
}[];
}
// https://developer.apple.com/documentation/applemusicapi/get-a-catalog-song
@ -56,18 +56,18 @@ export interface GetSongResponse<
> {
// https://developer.apple.com/documentation/applemusicapi/songs
data: {
id: string
type: "songs"
href: string
id: string;
type: "songs";
href: string;
// https://developer.apple.com/documentation/applemusicapi/songs/attributes-data.dictionary
attributes: SongAttributes<T>
attributes: SongAttributes<T>;
// https://developer.apple.com/documentation/applemusicapi/songs/relationships-data.dictionary
relationships: {
[K in U[number]]: Relationship<
K extends RelationshipType<T> ? RelationshipTypeMap<T>[K] : never
>
}
}[]
};
}[];
}
// TODO: support more than just albums
@ -85,19 +85,19 @@ export interface SearchResponse<
albums?: {
// https://developer.apple.com/documentation/applemusicapi/albums
data: {
id: string,
type: "albums",
href: string,
attributes: AlbumAttributes<Extract<T, AlbumAttributesExtensionTypes>>,
id: string;
type: "albums";
href: string;
attributes: AlbumAttributes<Extract<T, AlbumAttributesExtensionTypes>>;
// https://developer.apple.com/documentation/applemusicapi/albums/relationships-data.dictionary
relationships: {
[K in U[number]]: Relationship<
K extends RelationshipType<T> ? RelationshipTypeMap<T>[K] : never
>
}
}[],
href?: string,
next?: string,
}
}
};
}[];
href?: string;
next?: string;
};
};
}

View file

@ -57,11 +57,12 @@ function removeCacheEntry(fileName: string): void {
try {
fs.unlinkSync(path.join(config.downloader.cache.directory, fileName));
} catch (err) {
log.error(`could not remove cache entry ${fileName}`);
log.error("this could result in 2 effects:");
log.error("1. the cache entry will be removed, and the file never existed, operation is perfect, ignore this");
log.error("2. the cache entry will be removed, but the file exists, so it will remain in the filesystem");
log.error("if you experience the latter, the manual deletion of the file is required to fix this.");
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
log.debug(`file for cache entry ${fileName} missing, dropping`);
} else {
log.error(`could not remove cache entry ${fileName}!`);
log.error(err);
}
}
}

View file

@ -1,6 +1,6 @@
import type { RegularCodecType } from "downloader/codecType.js";
export const songCodecRegex: { [key in RegularCodecType]: RegExp } = {
export const songCodecRegex: Record<RegularCodecType, RegExp> = {
"aac": /audio-stereo-\d+/,
"aac_he": /audio-HE-stereo-\d+/,
"aac_binaural": /audio-stereo-\d+-binaural/,

View file

@ -6,7 +6,6 @@ import { pipeline } from "node:stream/promises";
import { addToCache, isCached } from "../cache.js";
// TODO: simply add more fields. ha!
// TODO: add album cover
// TODO: add lyrics (what format??)
export class FileMetadata {
public readonly artist: string;
@ -50,7 +49,7 @@ export class FileMetadata {
this.composer = composer;
}
public static fromSongResponse(trackMetadata: GetSongResponse<["extendedAssetUrls"], ["albums"]>): FileMetadata {
public static fromSongResponse(trackMetadata: GetSongResponse<[], ["albums"]>): FileMetadata {
const trackAttributes = trackMetadata.data[0].attributes;
const albumAttributes = trackMetadata.data[0].relationships.albums.data[0].attributes;
@ -102,8 +101,9 @@ export class FileMetadata {
"-i", imagePath,
"-map", "0",
"-map", "1",
"-disposition:v", "attached_pic",
"-c:a", "copy",
"-c:v", "mjpeg"
"-c:v", "copy"
];
}

28
src/downloader/format.ts Normal file
View file

@ -0,0 +1,28 @@
import type { SongAttributes } from "../appleMusicApi/types/attributes.js";
const illegalCharReplacements: Record<string, string> = {
"?": "",
"!": "",
"*": "",
"/": "",
"\\": "",
":": "",
"\"": "",
"<": "",
">": "",
"|": ""
};
// TODO: make these configurable, too opinionated right now
// eventually i'll make an account system? maybe you could do through there
// or i'll just make it config on the server
export function formatSong(trackAttributes: SongAttributes<[]>): string {
const title = trackAttributes.name.replace(/[?!*\/\\:"<>|]/g, (match) => illegalCharReplacements[match] || match);
const disc = trackAttributes.discNumber;
const track = trackAttributes.trackNumber;
if (track === undefined) { throw new Error("track number is undefined in track attributes!"); }
if (disc === undefined) { throw new Error("disc number is undefined in track attributes!"); }
return `${disc}-${track.toString().padStart(2, "0")} - ${title}`;
}

View file

@ -5,14 +5,19 @@ import { addToCache, isCached } from "../cache.js";
import type { RegularCodecType, WebplaybackCodecType } from "./codecType.js";
import type { GetSongResponse } from "../appleMusicApi/types/responses.js";
import { FileMetadata } from "./fileMetadata.js";
import { createDecipheriv } from "node:crypto";
import * as log from "../log.js";
export async function downloadSongFile(streamUrl: string, decryptionKey: string, songCodec: RegularCodecType | WebplaybackCodecType, songResponse: GetSongResponse<[], ["albums"]>): Promise<string> {
log.debug("downloading song file and hopefully decrypting it");
log.debug({ streamUrl: streamUrl, songCodec: songCodec });
export async function downloadSong(streamUrl: string, decryptionKey: string, songCodec: RegularCodecType | WebplaybackCodecType, songResponse: GetSongResponse<["extendedAssetUrls"], ["albums"]>): Promise<string> {
let baseOutputName = streamUrl.match(/(?:.*\/)\s*(\S*?)[.?]/)?.[1];
if (!baseOutputName) { throw new Error("could not get base output name from stream url!"); }
baseOutputName += `_${songCodec}`;
const encryptedName = baseOutputName + "_enc.mp4";
const encryptedPath = path.join(config.downloader.cache.directory, encryptedName);
const decryptedName = baseOutputName + ".mp4";
const decryptedName = baseOutputName + ".m4a";
const decryptedPath = path.join(config.downloader.cache.directory, decryptedName);
if ( // TODO: remove check for encrypted file/cache for encrypted?
@ -58,3 +63,219 @@ export async function downloadSong(streamUrl: string, decryptionKey: string, son
return decryptedPath;
}
// here's where shit gets real...
// here's also where i regret using javascript
// TODO: less mem alloc/access
// 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> {
log.debug("downloading and hopefully decrypting stream segment");
log.debug({ segmentUrl: segmentUrl, offset: offset, fetchLength: fetchLength });
const response = await fetch(segmentUrl, { headers: { "range": `bytes=${offset}-${offset + fetchLength - 1}` }});
const file = new Uint8Array(await response.arrayBuffer());
// this translates to "moof"
const moof = new Uint8Array([0x6D, 0x6F, 0x6F, 0x66]);
const moofIndex = file.findIndex((_v, i) => {
return file.subarray(i, i + moof.length).every((byte, j) => { return byte === moof[j]; });
});
const ivs = await extractIvFromFile(file);
const sampleLocs = await extractSampleLocationsFromFile(file);
if (moofIndex !== -1) {
sampleLocs.forEach((loc, i) => {
const iv = ivs[i].value;
const subsamples = ivs[i].subsamples;
const sample = file.subarray( // minus 4 because size
moofIndex + loc.offset - 4,
moofIndex + loc.offset + loc.size - 4
);
if (subsamples.length > 0) {
let pos = 0;
const decipher = createDecipheriv("aes-128-ctr", Buffer.from(decryptionKey, "hex"), Buffer.concat([iv, Buffer.alloc(8)]));
subsamples.forEach(({ clearBytes, encryptedBytes }) => {
pos += clearBytes;
if (encryptedBytes > 0) {
const chunk = sample.subarray(pos, pos + encryptedBytes);
const decryptedChunk = Buffer.concat([decipher.update(chunk), decipher.final()]);
decryptedChunk.copy(sample, pos);
pos += encryptedBytes;
}
});
} else {
const decipher = createDecipheriv("aes-128-ctr", Buffer.from(decryptionKey, "hex"), Buffer.concat([iv, Buffer.alloc(8)]));
const decrypted = Buffer.concat([decipher.update(sample), decipher.final()]);
file.set(decrypted, moofIndex + loc.offset - 4);
}
});
}
return file;
}
interface IvValue {
value: Buffer;
subsamples: Subsample[];
}
interface Subsample {
clearBytes: number;
encryptedBytes: number;
}
async function extractIvFromFile(file: Uint8Array): Promise<IvValue[]> {
const ivArray: IvValue[] = [];
let maxSampleCount: number | undefined;
let subsampleEncryptionPresent = false;
for (let i = 0; i < file.length; i++) {
// this translates to "senc"
if (
file[i] === 0x73 &&
file[i+1] === 0x65 &&
file[i+2] === 0x6E &&
file[i+3] === 0x63
) {
// skip 4 bytes -- skip "senc" header
i += 4;
// skip 1 byte -- skip version
i += 1;
const flags = (file[i] << 16) | (file[i+1] << 8) | file[i+2];
subsampleEncryptionPresent = (flags & 0x000002) !== 0;
// skip 4 bytes -- skip flags
i += 3;
// uint8x4 -> uint32x1
maxSampleCount = (file[i] << 24) | (file[i+1] << 16) | (file[i+2] << 8) | file[i+3];
// skip 4 bytes -- skip sample count
i += 4;
for (let sampleIndex = 0; sampleIndex < maxSampleCount; sampleIndex++) {
const iv = file.subarray(i, i + 8);
// skip 8 bytes -- skip iv
i += 8;
const subsamples: Subsample[] = [];
if (subsampleEncryptionPresent) {
const subsampleCount = (file[i] << 8) | file[i+1];
// skip 2 bytes -- skip subsample count
i += 2;
for (let j = 0; j < subsampleCount; j++) {
const clearBytes = (file[i] << 8) | file[i+1];
// skip 2 bytes -- skip clear bytes
i += 2;
const encryptedBytes = (file[i] << 24) | (file[i+1] << 16) | (file[i+2] << 8) | file[i+3];
// skip 4 bytes -- skip encrypted bytes
i += 4;
subsamples.push({
clearBytes: clearBytes,
encryptedBytes: encryptedBytes
});
}
}
ivArray.push({
value: Buffer.from(iv),
subsamples: subsamples
});
}
}
}
return ivArray;
}
interface SampleLocation {
offset: number;
size: number;
}
async function extractSampleLocationsFromFile(file: Uint8Array): Promise<SampleLocation[]> {
const sampleLocations: SampleLocation[] = [];
for (let i = 0; i < file.length; i++) {
// this translates to "trun"
if (
file[i] === 0x74 &&
file[i+1] === 0x72 &&
file[i+2] === 0x75 &&
file[i+3] === 0x6E
) {
// skip 4 bytes -- skip "trun" header
i += 4;
// skip 1 byte -- skip version
i += 1;
const flags = (file[i] << 16) | (file[i+1] << 8) | file[i+2];
const dataOffsetPresent = (flags & 0x000001) !== 0;
const firstSampleFlagsPresent = (flags & 0x000004) !== 0;
const sampleDurationPresent = (flags & 0x000100) !== 0;
const sampleSizePresent = (flags & 0x000200) !== 0;
const sampleFlagsPresent = (flags & 0x000400) !== 0;
const sampleCompositionTimeOffsetsPresent = (flags & 0x000800) !== 0;
// skip 3 bytes -- skip flags
i += 3;
if (!dataOffsetPresent) { throw new Error("data offset not present in trun atom!"); }
if (!sampleSizePresent) { throw new Error("sample size not present in trun atom!"); }
// TODO: add these flags
if (firstSampleFlagsPresent) { throw new Error("first sample flags not supported yet!"); }
if (sampleFlagsPresent) { throw new Error("sample flags not supported yet!"); }
if (sampleCompositionTimeOffsetsPresent) { throw new Error("sample composition time offsets not supported yet!"); }
const sampleCount = (file[i] << 24) | (file[i+1] << 16) | (file[i+2] << 8) | file[i+3];
// skip 4 bytes -- skip sample count
i += 4;
let sampleDataOffset = (file[i] << 24) | (file[i+1] << 16) | (file[i+2] << 8) | file[i+3];
// skip 4 bytes -- skip data offset
i += 4;
for (let j = 0; j < sampleCount; j++) {
// honestly? i'm scared of what apple is doing to where this could be true
// for context, only ones that use subsample encryption have this... on the last segment?????
// truly something that you gotta ponder about for a second
// skip 4 bytes -- skip sample duration
if (sampleDurationPresent) { i += 4; }
const sampleSize = (file[i] << 24) | (file[i+1] << 16) | (file[i+2] << 8) | file[i+3];
// skip 4 bytes -- skip sample size
i += 4;
sampleLocations.push({ offset: sampleDataOffset, size: sampleSize });
sampleDataOffset += sampleSize;
}
}
}
return sampleLocations;
}

View file

@ -15,7 +15,7 @@ export async function getWidevineDecryptionKey(psshDataUri: string, trackId: str
let challenge: Buffer;
try {
challenge = session.createLicenseRequest();
} catch (err) {
} catch (_err) {
// for some reason, if gotten from a webplayback manifest, the pssh is in a completely different format
// well, somewhat. it's just the raw data, we have to rebuild the pssh
const rebuiltPssh = psshTools.widevine.encodePssh({
@ -24,6 +24,9 @@ export async function getWidevineDecryptionKey(psshDataUri: string, trackId: str
keyIds: [Buffer.from(dataUriToBuffer(psshDataUri).buffer).toString("hex")]
});
// i'd love to log the error but it feels weird doing that and spammy
// also its the most useless error ever. "the pssh is not an actuall pssh"
// that typo is intentional, the library is like that
log.warn("pssh was invalid, treating it as raw data (this is expected in the webplayback manifest)");
log.warn("this should not throw an error, unless the pssh data is actually invalid");

View file

@ -14,6 +14,8 @@ type M3u8 = ReturnType<typeof hls.default.parse>;
export default class StreamInfo {
public readonly trackId: string;
public readonly streamUrl: string;
public readonly streamParsed: M3u8;
public readonly primaryFileUrl: string;
public readonly widevinePssh: string | undefined;
public readonly playreadyPssh: string | undefined;
public readonly fairplayKey: string | undefined;
@ -21,20 +23,25 @@ export default class StreamInfo {
private constructor(
trackId: string,
streamUrl: string,
streamParsed: M3u8,
primaryFileUrl: string,
widevinePssh: string | undefined,
playreadyPssh: string | undefined,
fairplayKey: string | undefined
) {
this.trackId = trackId;
this.streamUrl = streamUrl;
this.streamParsed = streamParsed;
this.primaryFileUrl = primaryFileUrl;
this.widevinePssh = widevinePssh;
this.playreadyPssh = playreadyPssh;
this.fairplayKey = fairplayKey;
}
// TODO: why can't we decrypt widevine ones with this?
// why can't we decrypt widevine ones with this?
// we get a valid key.. but it doesn't work :-(
// upd: it seems thats just how the cookie crumbles. oh well
// upd: removed todo. as i said its just how it is
public static async fromTrackMetadata(trackMetadata: SongAttributes<["extendedAssetUrls"]>, codec: RegularCodecType): Promise<StreamInfo> {
log.warn("the track metadata method is experimental, and may not work or give correct values!");
log.warn("if there is a failure--use a codec that uses the webplayback method");
@ -52,6 +59,10 @@ export default class StreamInfo {
const drmIds = assetInfos[variantId]["AUDIO-SESSION-KEY-IDS"];
const correctM3u8Url = m3u8Url.substring(0, m3u8Url.lastIndexOf("/")) + "/" + playlist.uri;
const correctM3u8 = await axios.get(correctM3u8Url, { responseType: "text" });
const correctM3u8Parsed = hls.default.parse(correctM3u8.data);
const primaryFileUrl = getPrimaryFileUrl(m3u8Parsed);
const widevinePssh = getWidevinePssh(drmInfos, drmIds);
const playreadyPssh = getPlayreadyPssh(drmInfos, drmIds);
@ -63,6 +74,8 @@ export default class StreamInfo {
return new StreamInfo(
trackId,
correctM3u8Url,
correctM3u8Parsed,
primaryFileUrl,
widevinePssh,
playreadyPssh,
fairplayKey
@ -87,6 +100,8 @@ export default class StreamInfo {
const m3u8 = await axios.get(m3u8Url, { responseType: "text" });
const m3u8Parsed = hls.default.parse(m3u8.data);
const primaryFileUrl = getPrimaryFileUrl(m3u8Parsed);
const widevinePssh = m3u8Parsed.lines.find((line) => { return line.name === "key"; })?.attributes?.uri;
if (widevinePssh === undefined) { throw new Error("widevine uri is missing!"); }
if (typeof widevinePssh !== "string") { throw new Error("widevine uri is not a string!"); }
@ -95,6 +110,8 @@ export default class StreamInfo {
return new StreamInfo(
trackId,
m3u8Url,
m3u8Parsed,
primaryFileUrl,
widevinePssh,
undefined,
undefined
@ -102,7 +119,11 @@ export default class StreamInfo {
}
}
type DrmInfos = { [key: string]: { [key: string]: { "URI": string } }; };
function getPrimaryFileUrl(m3u8Data: M3u8): string {
return m3u8Data.segments[0].uri;
}
type DrmInfos = Record<string, Record<string, { "URI": string }>>;;
function getDrmInfos(m3u8Data: M3u8): DrmInfos {
// see `getAssetInfos` for the reason why this is so bad
for (const line of m3u8Data.lines) {
@ -121,7 +142,7 @@ function getDrmInfos(m3u8Data: M3u8): DrmInfos {
throw new Error("m3u8 missing audio session key info!");
}
type AssetInfos = { [key: string]: { "AUDIO-SESSION-KEY-IDS": string[]; }; }
type AssetInfos = Record<string, { "AUDIO-SESSION-KEY-IDS": string[] }>;
function getAssetInfos(m3u8Data: M3u8): AssetInfos {
// LOL??? THIS LIBRARY IS SO BAD
// YOU CAN'T MAKE THIS SHIT UP

View file

@ -0,0 +1,89 @@
import express from "express";
import { validate } from "../../validate.js";
import z from "zod";
import { CodecType, regularCodecTypeSchema, webplaybackCodecTypeSchema, type RegularCodecType, type WebplaybackCodecType } from "../../../downloader/codecType.js";
import { appleMusicApi } from "../../../appleMusicApi/index.js";
import StreamInfo from "../../../downloader/streamInfo.js";
import hls from "parse-hls";
import { paths } from "../../openApi.js";
type M3u8 = ReturnType<typeof hls.default.parse>;
const router = express.Router();
const path = "/convertPlaylist";
const schema = z.object({
query: z.object({
id: z.string(),
codec: z.enum([...regularCodecTypeSchema.options, ...webplaybackCodecTypeSchema.options])
})
});
paths[path] = {
get: {
requestParams: { query: schema.shape.query },
responses: {
200: { description: "returns a m3u8 playlist for the song" },
400: { description: "bad request, invalid query parameters. sent as a zod error with details" },
default: { description: "upstream api error, or some other error" }
}
}
};
router.get(path, async (req, res, next) => {
try {
const { id, codec } = (await validate(req, schema)).query;
const codecType = new CodecType(codec);
let m3u8Parsed: M3u8;
let streamUrl: string;
if (codecType.regularOrWebplayback === "regular") {
const regularCodec = codecType.codecType as RegularCodecType; // safe cast, zod
const trackMetadata = await appleMusicApi.getSong(id);
const trackAttributes = trackMetadata.data[0].attributes;
const streamInfo = await StreamInfo.fromTrackMetadata(trackAttributes, regularCodec);
m3u8Parsed = streamInfo.streamParsed;
streamUrl = streamInfo.streamUrl;
} else if (codecType.regularOrWebplayback === "webplayback") {
const webplaybackCodec = codecType.codecType as WebplaybackCodecType; // safe cast, zod
const webplaybackResponse = await appleMusicApi.getWebplayback(id);
const streamInfo = await StreamInfo.fromWebplayback(webplaybackResponse, webplaybackCodec);
m3u8Parsed = streamInfo.streamParsed;
streamUrl = streamInfo.streamUrl;
} else {
// TODO: this is unreachable
// typescript doesn't think so
// i think its because of the "let"s why we need this
// fucks up our planned de-dupe on every use of `regularOrWebplayback`
// damn !
throw new Error("invalid codec type!");
}
const ogMp4Name = m3u8Parsed.segments[0].uri;
const ogMp4Url = streamUrl.substring(0, streamUrl.lastIndexOf("/")) + "/" + ogMp4Name;
const mp4PathParams = new URLSearchParams();
mp4PathParams.append("id", id);
mp4PathParams.append("originalMp4", ogMp4Url);
mp4PathParams.append("codec", codec);
const mp4Path = "downloadSegment" + "?" + mp4PathParams.toString();
const m3u8Text = m3u8Parsed.lines.map((line) => {
if (line.name === "key") { return ""; }
if (line.name === "map") { return line.content.replace(/(URI=")[^"]*(")/, `$1${mp4Path}$2`); }
if (!line.content.startsWith("#")) { return mp4Path; }
return line.content;
});
res.setHeader("Content-Type", "application/vnd.apple.mpegurl");
res.send(m3u8Text.join("\n"));
} catch (err) {
next(err);
}
});
export default router;

View file

@ -1,5 +1,5 @@
import { getWidevineDecryptionKey } from "../../../downloader/keygen.js";
import { downloadSong } from "../../../downloader/index.js";
import { downloadSongFile } from "../../../downloader/index.js";
import express from "express";
import StreamInfo from "../../../downloader/streamInfo.js";
import { appleMusicApi } from "../../../appleMusicApi/index.js";
@ -7,6 +7,7 @@ import { z } from "zod";
import { validate } from "../../validate.js";
import { CodecType, regularCodecTypeSchema, webplaybackCodecTypeSchema, type RegularCodecType, type WebplaybackCodecType } from "../../../downloader/codecType.js";
import { paths } from "../../openApi.js";
import { formatSong } from "../../../downloader/format.js";
const router = express.Router();
@ -31,6 +32,7 @@ paths[path] = {
// TODO: support more encryption schemes
// TODO: some type of agnostic-ness for the encryption schemes on regular codec
// TODO: now that i think about it.. there gotta be an easier way for the codec type stuff im cryin, like look in dl segment too
router.get(path, async (req, res, next) => {
try {
const { id, codec } = (await validate(req, schema)).query;
@ -45,8 +47,13 @@ router.get(path, async (req, res, next) => {
if (streamInfo.widevinePssh !== undefined) {
const decryptionKey = await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId);
const filePath = await downloadSong(streamInfo.streamUrl, decryptionKey, regularCodec, trackMetadata);
res.download(filePath);
const filePath = await downloadSongFile(streamInfo.streamUrl, decryptionKey, regularCodec, trackMetadata);
const fileExt = "." + filePath.split(".").at(-1) as string; // safe cast, filePath is always a valid path
const fileName = formatSong(trackAttributes) + fileExt;
res.attachment(fileName);
res.sendFile(filePath, { root: "." });
} else {
throw new Error("no decryption key found for regular codec! this is typical. don't fret!");
}
@ -54,12 +61,18 @@ router.get(path, async (req, res, next) => {
const webplaybackCodec = codecType.codecType as WebplaybackCodecType; // safe cast, zod
const webplaybackResponse = await appleMusicApi.getWebplayback(id);
const trackMetadata = await appleMusicApi.getSong(id);
const trackAttributes = trackMetadata.data[0].attributes;
const streamInfo = await StreamInfo.fromWebplayback(webplaybackResponse, webplaybackCodec);
if (streamInfo.widevinePssh !== undefined) {
const decryptionKey = await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId);
const filePath = await downloadSong(streamInfo.streamUrl, decryptionKey, webplaybackCodec, trackMetadata);
res.download(filePath);
const filePath = await downloadSongFile(streamInfo.streamUrl, decryptionKey, webplaybackCodec, trackMetadata);
const fileExt = "." + filePath.split(".").at(-1) as string; // safe cast, filePath is always a valid path
const fileName = formatSong(trackAttributes) + fileExt;
res.attachment(fileName);
res.sendFile(filePath, { root: "." });
} else {
throw new Error("no decryption key found for web playback! this should not happen..");
}

View file

@ -0,0 +1,96 @@
import express from "express";
import { paths } from "../../openApi.js";
import z from "zod";
import { CodecType, regularCodecTypeSchema, webplaybackCodecTypeSchema, type RegularCodecType, type WebplaybackCodecType } from "../../../downloader/codecType.js";
import { validate } from "../../validate.js";
import StreamInfo from "../../../downloader/streamInfo.js";
import { appleMusicApi } from "../../../appleMusicApi/index.js";
import { getWidevineDecryptionKey } from "../../../downloader/keygen.js";
import { fetchAndDecryptStreamSegment } from "../../../downloader/index.js";
const router = express.Router();
const path = "/downloadSegment";
const schema = z.object({
query: z.object({
id: z.string(),
originalMp4: z.url(),
codec: z.enum([...regularCodecTypeSchema.options, ...webplaybackCodecTypeSchema.options])
}),
headers: z.object({
range: z
.string()
.regex(/bytes=\d+-\d+/)
.transform((val) => {
// safe because of regex
const parts = val.split("=");
const values = parts[1].split("-");
return {
start: parseInt(values[0], 10),
end: parseInt(values[1], 10)
};
})
})
});
paths[path] = {
get: {
description: "returns a segment of a song in an mp4 container (see headers, single part byte ranges only) will fail to decrypt if full samples and initialization vectors are not included (and their metadata.) for use with translating m3u8 files that apple provides",
requestParams: { query: schema.shape.query, header: schema.shape.headers },
responses: {
200: { description: "returns a segment of a song in an mp4 container" },
400: { description: "bad request, invalid query parameters. sent as a zod error with details" },
default: { description: "upstream api error, or some other error" }
}
}
};
// 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) => {
try {
const { id, originalMp4, codec } = (await validate(req, schema)).query;
const { range: { start, end } } = (await validate(req, schema)).headers;
const codecType = new CodecType(codec);
if (codecType.regularOrWebplayback === "regular") {
const regularCodec = codecType.codecType as RegularCodecType; // safe cast, zod
const trackMetadata = await appleMusicApi.getSong(id);
const trackAttributes = trackMetadata.data[0].attributes;
const streamInfo = await StreamInfo.fromTrackMetadata(trackAttributes, regularCodec);
if (streamInfo.widevinePssh !== undefined) {
const decryptionKey = await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId);
const file = await fetchAndDecryptStreamSegment(originalMp4, decryptionKey, end - start + 1, start);
res.setHeader("Content-Type", "application/mp4");
res.setHeader("Content-Range", `bytes ${start}-${end}/*`);
res.setHeader("Accept-Ranges", "bytes");
res.status(206).send(file);
} else {
throw new Error("no decryption key found for regular codec! this is typical. don't fret!");
}
} else if (codecType.regularOrWebplayback === "webplayback") {
const webplaybackCodec = codecType.codecType as WebplaybackCodecType; // safe cast, zod
const webplaybackResponse = await appleMusicApi.getWebplayback(id);
const streamInfo = await StreamInfo.fromWebplayback(webplaybackResponse, webplaybackCodec);
if (streamInfo.widevinePssh !== undefined) {
const decryptionKey = await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId);
const file = await fetchAndDecryptStreamSegment(originalMp4, decryptionKey, end - start + 1, start);
res.setHeader("Content-Type", "application/mp4");
res.setHeader("Content-Range", `bytes ${start}-${end}/*`);
res.setHeader("Accept-Ranges", "bytes");
res.status(206).send(file);
} else {
throw new Error("no decryption key found for web playback! this should not happen..");
}
}
} catch (err) {
next(err);
}
});
export default router;

View file

@ -8,11 +8,15 @@ export const front = [
];
import backDownload from "./back/download.js";
import convertPlaylist from "./back/convertPlaylist.js";
import downloadSegment from "./back/downloadSegment.js";
import getAlbumMetadata from "./back/getAlbumMetadata.js";
import getPlaylistMetadata from "./back/getPlaylistMetadata.js";
import getTrackMetadata from "./back/getTrackMetadata.js";
export const back = [
backDownload,
convertPlaylist,
downloadSegment,
getAlbumMetadata,
getPlaylistMetadata,
getTrackMetadata