streaming!!, oh and file names, linting ig..
This commit is contained in:
parent
7b15834f17
commit
f2800f13c8
23 changed files with 1195 additions and 254 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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: "^_"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
599
package-lock.json
generated
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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]>;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>>;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
11
src/cache.ts
11
src/cache.ts
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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/,
|
||||
|
|
|
@ -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
28
src/downloader/format.ts
Normal 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}`;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
89
src/web/endpoints/back/convertPlaylist.ts
Normal file
89
src/web/endpoints/back/convertPlaylist.ts
Normal 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;
|
|
@ -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..");
|
||||
}
|
||||
|
|
96
src/web/endpoints/back/downloadSegment.ts
Normal file
96
src/web/endpoints/back/downloadSegment.ts
Normal 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;
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue