diff --git a/.gitignore b/.gitignore index 7d2188c..0b93b42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules dist +build bin Downloads diff --git a/package-lock.json b/package-lock.json index d7df622..e244e0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,1109 +1,48 @@ { "name": "tidalwave", - "version": "0.0.2", + "version": "0.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tidalwave", - "version": "0.0.2", - "license": "ISC", + "version": "0.12.0", + "license": "GPL-3.0", "devDependencies": { - "eslint": "^9.38.0", - "eslint-config-prettier": "^10.1.8", - "prettier": "^3.6.2" + "@types/node": "^25.5.0", + "typescript": "^6.0.2" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", - "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.16.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", - "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", - "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.16.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/acorn": { - "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": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", - "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.1", - "@eslint/core": "^0.16.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.38.0", - "@eslint/plugin-kit": "^0.4.0", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-prettier": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", - "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "funding": { - "url": "https://opencollective.com/eslint-config-prettier" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/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/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/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" + "undici-types": "~7.18.0" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/glob-parent": { + "node_modules/typescript": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, + "license": "Apache-2.0", "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=14.17" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/package.json b/package.json index 66dd0a0..4624e43 100644 --- a/package.json +++ b/package.json @@ -2,18 +2,23 @@ "name": "tidalwave", "version": "0.12.0", "description": "A TIDAL downloader for tracks, albums, artist discographies and playlists", - "main": "./src/index.js", + "main": "./src/index.ts", "scripts": { - "build": "npm run build:all", - "build:linux": "npm run build:linux-x64", - "build:windows": "npm run build:windows-x64", - "build:macos": "npm run build:macos-arm64", - "build:linux-x64": "cd scripts && bash ./build-linux-x64.sh", - "build:linux-arm64": "cd scripts && bash ./build-linux-arm64.sh", - "build:windows-x64": "cd scripts && bash ./build-windows-x64.sh", - "build:macos-x64": "cd scripts && bash ./build-macos-x64.sh", - "build:macos-arm64": "cd scripts && bash ./build-macos-arm64.sh", - "build:all": "cd scripts && bash ./build-all.sh", + "build": "tsc", + "dev": "tsc --watch", + "start": "npm run start:bun", + "start:node": "npm run build && node dist", + "start:bun": "bun .", + "package": "npm run package:all", + "package:linux": "npm run package:linux-x64", + "package:windows": "npm run package:windows-x64", + "package:macos": "npm run package:macos-arm64", + "package:linux-x64": "cd scripts && bash ./package-linux-x64.sh", + "package:linux-arm64": "cd scripts && bash ./package-linux-arm64.sh", + "package:windows-x64": "cd scripts && bash ./package-windows-x64.sh", + "package:macos-x64": "cd scripts && bash ./package-macos-x64.sh", + "package:macos-arm64": "cd scripts && bash ./package-macos-arm64.sh", + "package:all": "cd scripts && bash ./package-all.sh", "download-binaries": "cd scripts && bash ./download-binaries.sh" }, "repository": { @@ -34,5 +39,9 @@ "bugs": { "url": "https://github.com/Lyall-A/tidalwave/issues" }, - "homepage": "https://github.com/Lyall-A/tidalwave#readme" + "homepage": "https://github.com/Lyall-A/tidalwave#readme", + "devDependencies": { + "@types/node": "^25.5.0", + "typescript": "^6.0.2" + } } diff --git a/scripts/build-all.sh b/scripts/build-all.sh deleted file mode 100755 index c049dc4..0000000 --- a/scripts/build-all.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -./build-linux-x64.sh -./build-linux-arm64.sh -./build-macos-x64.sh -./build-macos-arm64.sh -./build-windows-x64.sh \ No newline at end of file diff --git a/scripts/build-linux-arm64.sh b/scripts/build-linux-arm64.sh deleted file mode 100755 index 7cdcb89..0000000 --- a/scripts/build-linux-arm64.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -./build.sh linux-arm64 bun-linux-arm64 "" .tar.gz \ No newline at end of file diff --git a/scripts/build-linux-x64.sh b/scripts/build-linux-x64.sh deleted file mode 100755 index abdb06b..0000000 --- a/scripts/build-linux-x64.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -./build.sh linux-x64 bun-linux-x64 "" .tar.gz \ No newline at end of file diff --git a/scripts/build-macos-arm64.sh b/scripts/build-macos-arm64.sh deleted file mode 100755 index c3f3a90..0000000 --- a/scripts/build-macos-arm64.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -./build.sh macos-arm64 bun-darwin-arm64 \ No newline at end of file diff --git a/scripts/build-macos-x64.sh b/scripts/build-macos-x64.sh deleted file mode 100755 index bbb9f40..0000000 --- a/scripts/build-macos-x64.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -./build.sh macos-x64 bun-darwin-x64 \ No newline at end of file diff --git a/scripts/build-windows-x64.sh b/scripts/build-windows-x64.sh deleted file mode 100755 index c77f1ed..0000000 --- a/scripts/build-windows-x64.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -./build.sh windows-x64 bun-windows-x64 .exe .zip \ No newline at end of file diff --git a/scripts/download-binaries.sh b/scripts/download-binaries.sh index 3a93b20..1b16d17 100755 --- a/scripts/download-binaries.sh +++ b/scripts/download-binaries.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -e + ffmpeg_linux_x64_download_url=https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz ffmpeg_linux_x64_download_extension=.tar.xz ffmpeg_linux_x64_download_path=ffmpeg-linux-x64 diff --git a/scripts/package-all.sh b/scripts/package-all.sh new file mode 100755 index 0000000..2efadb7 --- /dev/null +++ b/scripts/package-all.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +./package-linux-x64.sh +./package-linux-arm64.sh +./package-macos-x64.sh +./package-macos-arm64.sh +./package-windows-x64.sh \ No newline at end of file diff --git a/scripts/package-linux-arm64.sh b/scripts/package-linux-arm64.sh new file mode 100755 index 0000000..9fd18ba --- /dev/null +++ b/scripts/package-linux-arm64.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +./package.sh linux-arm64 bun-linux-arm64 "" .tar.gz \ No newline at end of file diff --git a/scripts/package-linux-x64.sh b/scripts/package-linux-x64.sh new file mode 100755 index 0000000..29e3ab4 --- /dev/null +++ b/scripts/package-linux-x64.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +./package.sh linux-x64 bun-linux-x64 "" .tar.gz \ No newline at end of file diff --git a/scripts/package-macos-arm64.sh b/scripts/package-macos-arm64.sh new file mode 100755 index 0000000..a87339c --- /dev/null +++ b/scripts/package-macos-arm64.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +./package.sh macos-arm64 bun-darwin-arm64 \ No newline at end of file diff --git a/scripts/package-macos-x64.sh b/scripts/package-macos-x64.sh new file mode 100755 index 0000000..7478a15 --- /dev/null +++ b/scripts/package-macos-x64.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +./package.sh macos-x64 bun-darwin-x64 \ No newline at end of file diff --git a/scripts/package-windows-x64.sh b/scripts/package-windows-x64.sh new file mode 100755 index 0000000..d0e2d44 --- /dev/null +++ b/scripts/package-windows-x64.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +./package.sh windows-x64 bun-windows-x64 .exe .zip \ No newline at end of file diff --git a/scripts/build.sh b/scripts/package.sh similarity index 54% rename from scripts/build.sh rename to scripts/package.sh index 0c07b0d..c8f6b60 100755 --- a/scripts/build.sh +++ b/scripts/package.sh @@ -5,9 +5,9 @@ target="${2:-bun}" file_ext="$3" archive_ext="${4:-.zip}" -dist_dir=../dist +build_dir=../build bin_dir=../bin -out_dir=$dist_dir/$os +out_dir=$build_dir/$os filename=tidalwave archive_name=tidalwave-$os @@ -19,11 +19,9 @@ bun build \ --compile \ --production \ --target=$target \ - --external="./config.json" \ - --external="./secrets.json" \ - --define="__filename=process.execPath" \ + --define="isBuild=true" \ --outfile="$out_dir/$filename$file_ext" \ - ./index.js + ./index.ts cp ./default.config.json "$out_dir/config.json" cp ../README.md "$out_dir/README.md" @@ -31,11 +29,12 @@ cp ../LICENSE "$out_dir/LICENSE" cp -r "$bin_dir/$os" "$out_dir/bin" chmod +x "$out_dir/$filename$file_ext" -cp -r "$out_dir" "$dist_dir/$archive_name" +cp -r "$out_dir" "$build_dir/$archive_name" if [[ "$archive_ext" == ".tar.gz" ]]; then - 7z a "$dist_dir/$archive_name.tar" "$dist_dir/$archive_name" -bso0 - 7z a "$dist_dir/$archive_name.tar.gz" "$dist_dir/$archive_name.tar" -bso0 + 7z a "$build_dir/$archive_name.tar" "$build_dir/$archive_name" -bso0 + 7z a "$build_dir/$archive_name.tar.gz" "$build_dir/$archive_name.tar" -bso0 + rm "$build_dir/$archive_name.tar" else - 7z a "$dist_dir/$archive_name$archive_ext" "$dist_dir/$archive_name" -bso0 + 7z a "$build_dir/$archive_name$archive_ext" "$build_dir/$archive_name" -bso0 fi -rm -rf "$dist_dir/$archive_name" +rm -rf "$build_dir/$archive_name" diff --git a/src/globals.js b/src/globals.js deleted file mode 100644 index 2829aee..0000000 --- a/src/globals.js +++ /dev/null @@ -1,195 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const parseConfig = require('./utils/parseConfig'); -const Logger = require('./utils/Logger'); - -// bun moment -const execDir = path.dirname(__filename); -const execFile = __filename; - -// Read config -const configPath = path.resolve(execDir, 'config.json'); -if (!fs.existsSync(configPath)) fs.writeFileSync(configPath, JSON.stringify(require('./default.config.json'), null, 4)); -const config = parseConfig(configPath); - -// Read secrets -const secretsPath = config.secretsPath ? path.resolve(execDir, config.secretsPath) : undefined; -const secrets = fs.existsSync(secretsPath) ? JSON.parse(fs.readFileSync(secretsPath)) : { }; - -const logger = new Logger(); - -module.exports = { - config, - secrets, - configPath, - secretsPath, - execDir, - execFile, - logger, - argOptions: [ - { name: 'help', shortName: 'h', noValue: true, description: 'Displays this menu' }, - { name: 'debug', noValue: true, description: 'Enable debug', hidden: true }, - - { name: 'track', shortName: 't', type: 'int', description: 'Download a track ID', valueDescription: 'track-id' }, - { name: 'album', shortName: 'm', type: 'int', description: 'Download a album ID', valueDescription: 'album-id' }, - { name: 'video', shortName: 'v', description: 'Download a video ID', valueDescription: 'video-id' }, - { name: 'artist', shortName: 'a', type: 'int', description: 'Download an artist ID\'s discography', valueDescription: 'artist-id' }, - { name: 'playlist', shortName: 'p', description: 'Download all items from a playlist UUID', valueDescription: 'playlist-uuid' }, - { name: 'mix', shortName: 'x', description: 'Download all items from a mix ID', valueDescription: 'mix-id' }, - { name: 'search', shortName: 's', description: 'Download top search', valueDescription: 'query' }, - { name: 'search:track', shortName: 's:t', description: 'Download top search for a track', valueDescription: 'query' }, - { name: 'search:album', shortName: 's:m', description: 'Download top search for a album', valueDescription: 'query' }, - { name: 'search:video', shortName: 's:v', description: 'Download top search for a video', valueDescription: 'query' }, - { name: 'search:artist', shortName: 's:a', description: 'Download top search for a artist', valueDescription: 'query' }, - { name: 'search:playlist', shortName: 's:p', description: 'Download top search for a playlist', valueDescription: 'query' }, - { name: 'url', shortName: 'u', description: 'Download from a TIDAL URL', valueDescription: 'url' }, - { name: 'update', description: 'Update an existing file with metadata from TIDAL', valueDescription: 'path' }, - - { name: 'track-quality', shortName: 'tq', aliases: ['quality'], shortAliases: ['q'], description: 'Track download quality', valueDescription: 'low|high|max', default: config.trackQuality }, - { name: 'video-quality', shortName: 'vq', description: 'Video download quality', valueDescription: 'low|high|max|', default: config.videoQuality }, - { name: 'dolby-atmos', shortName: 'da', type: 'bool', description: 'Downloads in immersive audio when available. Requires a token from a mobile device', valueDescription: 'yes|no', default: config.useDolbyAtmos, hidden: true }, - { name: 'metadata', shortName: 'md', type: 'bool', description: 'Embed metadata to download', valueDescription: 'yes|no', default: config.embedMetadata }, - { name: 'lyrics', shortName: 'l', type: 'bool', description: 'Download lyrics if available', valueDescription: 'yes|no', default: config.getLyrics }, - { name: 'cover', shortName: 'c', type: 'bool', description: 'Download cover art', valueDescription: 'yes|no', default: config.getCover }, - { name: 'overwrite', shortName: 'ow', type: 'bool', description: 'Overwrite existing downloads', valueDescription: 'yes|no', default: config.overwriteExisting } - ], - tidalTrackQualities: { - 'LOW': 'HIGH', - 'HIGH': 'LOSSLESS', - 'MAX': 'HI_RES_LOSSLESS' - }, - tidalVideoQualities: { - 'LOW': '480', - 'MEDIUM': '720', - 'HIGH': '1080', - 'MAX': null - }, - tidalVideoCoverSizes: { - '640': '640x640', - '1280': '1280x1280', - '1280x720': '1280x720', - '640x360': '640x360', - 'ORIGINAL': 'origin' - }, - tidalAlbumCoverSizes: { - '320': '320x320', - '640': '640x640', - '1280': '1280x1280', - 'ORIGINAL': 'origin' - }, - tidalArtistPictureSizes: { - 'ORIGINAL': 'origin' - }, - tidalPlaylistImageSizes: { - '320': '320x320', - '640': '640x640', - '1280': '1280x1280', - 'ORIGINAL': 'origin' - }, - tidalMixImageSizes: { - 'SMALL': 'SMALL', - 'MEDIUM': 'MEDIUM', - 'LARGE': 'LARGE' - }, - tidalCredits: [ - // NOTE: found from searching various albums and tracks, theres definitely more - - // Found in both track and album credits - { type: 'Producer' }, - { type: 'Assistant Engineer' }, - { type: 'Engineer' }, - - // Track credits - { type: 'Executive Producer' }, - { type: 'Composer' }, - { type: 'Mastering Engineer' }, - { type: 'Mixing Engineer' }, - { type: 'Additional Engineer' }, - { type: 'Recording Engineer' }, - { type: 'Associated Performer' }, - { type: 'Assistant Producer' }, - { type: 'Assistant Recording Engineer' }, - { type: 'Assistant Mixing Engineer' }, - { type: 'Featured Artist' }, - { type: 'Remixer', }, - { type: 'Drum Kit' }, - { type: 'Synthesizer' }, - { type: 'Mixer' }, - { type: 'Lyricist' }, - { type: 'Background Vocal' }, - { type: 'Talkbox' }, - { type: 'Vocoder' }, - { type: 'Vocal' }, - { type: 'Guitar' }, - { type: 'Bass' }, - { type: 'Piano' }, - { type: 'Drums' }, - { type: 'Drum' }, - { type: 'Horn' }, - { type: 'Strings' }, - { type: 'Whistles' }, - { type: 'Keyboards' }, - - // Album credits - { type: 'Primary Artist' }, - { type: 'Arranger' }, - { type: 'Mixing' }, - { type: 'Mixing Assistant' }, - { type: 'Mastering' }, - { type: 'Music Publisher', tagName: 'publisher' }, - { type: 'Record Label', tagName: 'label' }, - { type: 'Layout', tagName: null }, - { type: 'Artwork', tagName: null }, - { type: 'Package Design', tagName: null }, - { type: 'Art Direction', tagName: null }, - { type: 'Design', tagName: null }, - { type: 'Vocals', tagName: null }, - { type: 'Graphic Design', tagName: null }, - - // Unknown - { type: 'Additional Mixing Engineer' }, - { type: 'Keyboard' }, - { type: 'Banjo' }, - { type: 'Vocalist' }, - { type: 'Vocal Producer' }, - { type: 'Vocal Arranger' }, - { type: 'Programmer' }, - { type: 'Mastering Second Engineer' }, - { type: 'Production Coordinator' }, - { type: 'Sequencer' }, - { type: 'Conductor' }, - { type: 'Programming' }, - { type: 'Saxophone' }, - { type: 'Recording' }, - { type: 'Second Engineer' }, - { type: 'Co-Producer' }, - { type: 'Violin' }, - { type: 'Cello' }, - { type: 'Percussion' }, - { type: 'Coordination' }, - { type: 'Background Vocalist' }, - { type: 'A&R' }, - { type: 'Creative Director' }, - { type: 'Drum Programming' }, - { type: 'Studio Personnel' }, - { type: 'Management' }, - { type: 'Additional Producer' }, - { type: 'Writer' }, - { type: 'SoundEffects', tagName: 'sound_effects' }, - { type: 'DrumProgrammer', tagName: 'drum_programmer' }, - { type: 'MusicProduction', tagName: 'music_production' }, - { type: 'BassVocalist', tagName: 'bass_vocalist' }, - { type: 'AdditionalProducer', tagName: 'additional_producer' }, - { type: 'AdditionalVocalist', tagName: 'additional_vocalist' }, - { type: 'RecordingArranger', tagName: 'recording_arranger' }, - { type: 'ChoirArranger', tagName: 'choir_arranger' }, - { type: 'Photography', tagName: null }, - { type: 'Original Paintings', tagName: null }, - { type: 'Marketing', tagName: null }, - ].map(credit => { - // Fill blank tagName - if (credit.tagName === undefined) credit.tagName = credit.type.toLowerCase().replace(/\s/g, '_'); - return credit; - }) -} \ No newline at end of file diff --git a/src/globals.ts b/src/globals.ts new file mode 100644 index 0000000..686a1b3 --- /dev/null +++ b/src/globals.ts @@ -0,0 +1,187 @@ +import fs from 'fs'; +import path from 'path'; + +import parseConfig from './utils/parseConfig.js'; +import Logger from './utils/Logger.js'; + +export declare const isBuild: boolean | undefined; + +export const execDir = typeof isBuild !== 'undefined' && isBuild ? path.dirname(process.execPath) : __dirname; // TODO: i dont like this + +// Read config +export const configPath = path.resolve(execDir, 'config.json'); +if (!fs.existsSync(configPath)) fs.writeFileSync(configPath, JSON.stringify(require('./default.config.json'), null, 4)); +export const config = parseConfig(configPath); + +// Read secrets +export const secretsPath = config.secretsPath ? path.resolve(execDir, config.secretsPath) : undefined; +export const secrets = secretsPath && fs.existsSync(secretsPath) ? JSON.parse(fs.readFileSync(secretsPath, 'utf-8')) : { }; + +export const logger = new Logger(); + +export const argOptions = [ + { name: 'help', shortName: 'h', noValue: true, description: 'Displays this menu' }, + { name: 'debug', noValue: true, description: 'Enable debug', hidden: true }, + + { name: 'track', shortName: 't', type: 'int', description: 'Download a track ID', valueDescription: 'track-id' }, + { name: 'album', shortName: 'm', type: 'int', description: 'Download a album ID', valueDescription: 'album-id' }, + { name: 'video', shortName: 'v', description: 'Download a video ID', valueDescription: 'video-id' }, + { name: 'artist', shortName: 'a', type: 'int', description: 'Download an artist ID\'s discography', valueDescription: 'artist-id' }, + { name: 'playlist', shortName: 'p', description: 'Download all items from a playlist UUID', valueDescription: 'playlist-uuid' }, + { name: 'mix', shortName: 'x', description: 'Download all items from a mix ID', valueDescription: 'mix-id' }, + { name: 'search', shortName: 's', description: 'Download top search', valueDescription: 'query' }, + { name: 'search:track', shortName: 's:t', description: 'Download top search for a track', valueDescription: 'query' }, + { name: 'search:album', shortName: 's:m', description: 'Download top search for a album', valueDescription: 'query' }, + { name: 'search:video', shortName: 's:v', description: 'Download top search for a video', valueDescription: 'query' }, + { name: 'search:artist', shortName: 's:a', description: 'Download top search for a artist', valueDescription: 'query' }, + { name: 'search:playlist', shortName: 's:p', description: 'Download top search for a playlist', valueDescription: 'query' }, + { name: 'url', shortName: 'u', description: 'Download from a TIDAL URL', valueDescription: 'url' }, + { name: 'update', description: 'Update an existing file with metadata from TIDAL', valueDescription: 'path' }, + + { name: 'track-quality', shortName: 'tq', aliases: ['quality'], shortAliases: ['q'], description: 'Track download quality', valueDescription: 'low|high|max', default: config.trackQuality }, + { name: 'video-quality', shortName: 'vq', description: 'Video download quality', valueDescription: 'low|high|max|', default: config.videoQuality }, + { name: 'dolby-atmos', shortName: 'da', type: 'bool', description: 'Downloads in immersive audio when available. Requires a token from a mobile device', valueDescription: 'yes|no', default: config.useDolbyAtmos, hidden: true }, + { name: 'metadata', shortName: 'md', type: 'bool', description: 'Embed metadata to download', valueDescription: 'yes|no', default: config.embedMetadata }, + { name: 'lyrics', shortName: 'l', type: 'bool', description: 'Download lyrics if available', valueDescription: 'yes|no', default: config.getLyrics }, + { name: 'cover', shortName: 'c', type: 'bool', description: 'Download cover art', valueDescription: 'yes|no', default: config.getCover }, + { name: 'overwrite', shortName: 'ow', type: 'bool', description: 'Overwrite existing downloads', valueDescription: 'yes|no', default: config.overwriteExisting } +]; + +export const tidalTrackQualities = { + 'LOW': 'HIGH', + 'HIGH': 'LOSSLESS', + 'MAX': 'HI_RES_LOSSLESS' +} +export const tidalVideoQualities = { + 'LOW': '480', + 'MEDIUM': '720', + 'HIGH': '1080', + 'MAX': null +} +export const tidalVideoCoverSizes = { + '640': '640x640', + '1280': '1280x1280', + '1280x720': '1280x720', + '640x360': '640x360', + 'ORIGINAL': 'origin' +} +export const tidalAlbumCoverSizes = { + '320': '320x320', + '640': '640x640', + '1280': '1280x1280', + 'ORIGINAL': 'origin' +} +export const tidalArtistPictureSizes = { + 'ORIGINAL': 'origin' +} +export const tidalPlaylistImageSizes = { + '320': '320x320', + '640': '640x640', + '1280': '1280x1280', + 'ORIGINAL': 'origin' +} +export const tidalMixImageSizes = { + 'SMALL': 'SMALL', + 'MEDIUM': 'MEDIUM', + 'LARGE': 'LARGE' +} +export const tidalCredits = [ + // NOTE: found from searching various albums and tracks, theres definitely more + + // Found in both track and album credits + { type: 'Producer' }, + { type: 'Assistant Engineer' }, + { type: 'Engineer' }, + + // Track credits + { type: 'Executive Producer' }, + { type: 'Composer' }, + { type: 'Mastering Engineer' }, + { type: 'Mixing Engineer' }, + { type: 'Additional Engineer' }, + { type: 'Recording Engineer' }, + { type: 'Associated Performer' }, + { type: 'Assistant Producer' }, + { type: 'Assistant Recording Engineer' }, + { type: 'Assistant Mixing Engineer' }, + { type: 'Featured Artist' }, + { type: 'Remixer', }, + { type: 'Drum Kit' }, + { type: 'Synthesizer' }, + { type: 'Mixer' }, + { type: 'Lyricist' }, + { type: 'Background Vocal' }, + { type: 'Talkbox' }, + { type: 'Vocoder' }, + { type: 'Vocal' }, + { type: 'Guitar' }, + { type: 'Bass' }, + { type: 'Piano' }, + { type: 'Drums' }, + { type: 'Drum' }, + { type: 'Horn' }, + { type: 'Strings' }, + { type: 'Whistles' }, + { type: 'Keyboards' }, + + // Album credits + { type: 'Primary Artist' }, + { type: 'Arranger' }, + { type: 'Mixing' }, + { type: 'Mixing Assistant' }, + { type: 'Mastering' }, + { type: 'Music Publisher', tagName: 'publisher' }, + { type: 'Record Label', tagName: 'label' }, + { type: 'Layout', tagName: null }, + { type: 'Artwork', tagName: null }, + { type: 'Package Design', tagName: null }, + { type: 'Art Direction', tagName: null }, + { type: 'Design', tagName: null }, + { type: 'Vocals', tagName: null }, + { type: 'Graphic Design', tagName: null }, + + // Unknown + { type: 'Additional Mixing Engineer' }, + { type: 'Keyboard' }, + { type: 'Banjo' }, + { type: 'Vocalist' }, + { type: 'Vocal Producer' }, + { type: 'Vocal Arranger' }, + { type: 'Programmer' }, + { type: 'Mastering Second Engineer' }, + { type: 'Production Coordinator' }, + { type: 'Sequencer' }, + { type: 'Conductor' }, + { type: 'Programming' }, + { type: 'Saxophone' }, + { type: 'Recording' }, + { type: 'Second Engineer' }, + { type: 'Co-Producer' }, + { type: 'Violin' }, + { type: 'Cello' }, + { type: 'Percussion' }, + { type: 'Coordination' }, + { type: 'Background Vocalist' }, + { type: 'A&R' }, + { type: 'Creative Director' }, + { type: 'Drum Programming' }, + { type: 'Studio Personnel' }, + { type: 'Management' }, + { type: 'Additional Producer' }, + { type: 'Writer' }, + { type: 'SoundEffects', tagName: 'sound_effects' }, + { type: 'DrumProgrammer', tagName: 'drum_programmer' }, + { type: 'MusicProduction', tagName: 'music_production' }, + { type: 'BassVocalist', tagName: 'bass_vocalist' }, + { type: 'AdditionalProducer', tagName: 'additional_producer' }, + { type: 'AdditionalVocalist', tagName: 'additional_vocalist' }, + { type: 'RecordingArranger', tagName: 'recording_arranger' }, + { type: 'ChoirArranger', tagName: 'choir_arranger' }, + { type: 'Photography', tagName: null }, + { type: 'Original Paintings', tagName: null }, + { type: 'Marketing', tagName: null }, +].map((credit: { type: string; tagName?: string | null }) => { + // Fill blank tagName + if (credit.tagName === undefined) credit.tagName = credit.type.toLowerCase().replace(/\s/g, '_'); + return credit; +}); \ No newline at end of file diff --git a/src/index.js b/src/index.ts similarity index 73% rename from src/index.js rename to src/index.ts index a2a360a..4dbc284 100644 --- a/src/index.js +++ b/src/index.ts @@ -1,21 +1,21 @@ -const fs = require('fs'); -const path = require('path'); - -const requestDeviceAuthorization = require('./utils/requestDeviceAuthorization'); -const getToken = require('./utils/getToken'); -const getAlbum = require('./utils/getAlbum'); -const getArtist = require('./utils/getArtist'); -const getTrack = require('./utils/getTrack'); -const getVideo = require('./utils/getVideo'); -const getPlaylist = require('./utils/getPlaylist'); -const getMix = require('./utils/getMix'); -const search = require('./utils/search'); -const Args = require('./utils/Args'); -const formatPath = require('./utils/formatPath'); -const Logger = require('./utils/Logger'); -const Download = require('./utils/Download'); - -const { +import fs from 'fs'; +import path from 'path'; + +import requestDeviceAuthorization from './utils/requestDeviceAuthorization'; +import getToken from './utils/getToken'; +import getAlbum from'./utils/getAlbum'; +import getArtist from'./utils/getArtist'; +import getTrack from './utils/getTrack'; +import getVideo from'./utils/getVideo'; +import getPlaylist from'./utils/getPlaylist'; +import getMix from'./utils/getMix'; +import search from'./utils/search'; +import Args from './utils/Args'; +import formatPath from'./utils/formatPath'; +import Logger from './utils/Logger'; +import Download from'./utils/Download'; + +import { config, secrets, secretsPath, @@ -24,7 +24,8 @@ const { logger, tidalTrackQualities, tidalVideoQualities -} = require('./globals'); +} from './globals.js'; +import { Album, Artist, Mix, Playlist, Track, Video } from './types'; const args = new Args(process.argv, argOptions); const options = { @@ -58,7 +59,7 @@ const options = { }; logger.debugLogs = options.debug; -logger.debug(`Options:\n${JSON.stringify(options, null, 4)}`); +logger.log(`Options:\n${JSON.stringify(options, null, 4)}`, 'debug'); // Show help if (options.help || [ @@ -75,12 +76,22 @@ if (options.help || [ (async () => { await authorize(); - const tracks = []; - const albums = []; - const videos = []; - const artists = []; - - const queue = []; // Tracks to be downloaded + const tracks: Track[] = []; + const albums: Album[] = []; + const videos: Video[] = []; + const artists: Artist[] = []; + + // Tracks to be downloaded + const queue: { + track?: Track; + album?: Album; + video?: Video; + artists?: Artist[]; + albumArtists?: Artist[]; + playlist?: Playlist; + mix?: Mix; + itemIndex?: number; + }[] = []; for (const trackId of options.tracks) await addTrack(trackId); // Tracks for (const albumId of options.albums) await addAlbum(albumId); // Albums @@ -91,7 +102,7 @@ if (options.help || [ // Searches for (const { type, query } of options.searches) { - logger.info(`Searching for: ${Logger.applyColor({ bold: true }, query)}`, true); + logger.log(`Searching for: ${Logger.applyColor({ bold: true }, query)}`, 'info', true); const result = await search(query, 1).then(results => ( type === 'track' ? results.tracks.map(value => ({ type, value })) : type === 'album' ? results.albums.map(value => ({ type, value })) : @@ -101,12 +112,12 @@ if (options.help || [ results.topResults )[0]); - if (result?.type === 'track') await addTrack(result.value.id); else - if (result?.type === 'album') await addAlbum(result.value.id); else - if (result?.type === 'video') await addVideo(result.value.id); else - if (result?.type === 'artist') await addArtist(result.value.id); else - if (result?.type === 'playlist') await addPlaylist(result.value.id); else - logger.error(`No search results for "${Logger.applyColor({ bold: true }, query)}"`, true, true); + if (result?.type === 'track') await addTrack((result.value as Track).id); else + if (result?.type === 'album') await addAlbum((result.value as Album).id); else + if (result?.type === 'video') await addVideo((result.value as Video).id); else + if (result?.type === 'artist') await addArtist((result.value as Artist).id); else + if (result?.type === 'playlist') await addPlaylist((result.value as Playlist).uuid); else + logger.log(`No search results for "${Logger.applyColor({ bold: true }, query)}"`, 'error', true, true); } // URLS @@ -123,22 +134,22 @@ if (options.help || [ if (type === 'artist') await addArtist(idInt); else if (type === 'playlist') await addPlaylist(id); else if (type === 'mix') await addMix(id); else - logger.error(`Unknown type "${Logger.applyColor({ bold: true }, type)}"`, true, true); // NOTE: not possible with current regex + logger.log(`Unknown type "${Logger.applyColor({ bold: true }, type)}"`, 'error', true, true); // NOTE: not possible with current regex } else { - logger.error(`Couldn't determine URL "${Logger.applyColor({ bold: true }, url)}"`, true, true); + logger.log(`Couldn't determine URL "${Logger.applyColor({ bold: true }, url)}"`, 'error', true, true); } } // const startDate = Date.now(); logger.emptyLine(); - logger.info(`Downloading ${Object.entries({ - track: queue.filter(item => item.track).length, - video: queue.filter(item => item.video).length, + logger.log(`Downloading ${Object.entries({ + track: queue.filter((item: any) => item.track).length, + video: queue.filter((item: any) => item.video).length, }) .filter(([type, count]) => count > 0) .map(([type, count]) => `${Logger.applyColor({ bold: true }, count)} ${type}${count !== 1 ? 's' : ''}`) - .join(', ')}...`); + .join(', ')}...`, 'info'); for (let itemIndex = 0; itemIndex < queue.length; itemIndex++) { const item = queue[itemIndex]; @@ -157,7 +168,7 @@ if (options.help || [ albumArtist: item.albumArtists?.[0], trackNumberPadded: item.track?.trackNumber?.toString().padStart(2, '0'), // TODO: maybe remove this and add a padding function in formatString? queueNum: itemIndex + 1, - itemNum: item.itemIndex + 1, + itemNum: item.itemIndex && item.itemIndex + 1, playlistCover: item.playlist ? item.playlist.images[config.playlistCoverSize?.toUpperCase()] || item.playlist.images['ORIGINAL'] : null, mixCover: item.mix ? item.mix.images[config.mixCoverSize?.toUpperCase()] || item.mix.images['LARGE'] : null, mixDetailCover: item.mix ? item.mix.detailImages[config.mixCoverSize?.toUpperCase()] || item.mix.images['LARGE'] : null, // not currently used @@ -264,8 +275,8 @@ if (options.help || [ coverFilename, playlistCoverFilename: (config.playlistCoverFilename && formatPath(config.playlistCoverFilename, details)) || (config.mixCoverFilename && formatPath(config.mixCoverFilename, details)), playlistFileFilename: (config.playlistFileFilename && formatPath(config.playlistFileFilename, details)) || (config.mixFileFilename && formatPath(config.mixFileFilename, details)), - trackQuality: tidalTrackQualities[options.trackQuality] === undefined ? options.trackQuality : tidalTrackQualities[options.trackQuality], - videoQuality: tidalVideoQualities[options.videoQuality] === undefined ? options.videoQuality : tidalVideoQualities[options.videoQuality], + trackQuality: tidalTrackQualities[options.trackQuality as keyof typeof tidalTrackQualities] === undefined ? options.trackQuality : tidalTrackQualities[options.trackQuality as keyof typeof tidalTrackQualities], + videoQuality: tidalVideoQualities[options.videoQuality as keyof typeof tidalVideoQualities] === undefined ? options.videoQuality : tidalVideoQualities[options.videoQuality as keyof typeof tidalVideoQualities], overwriteExisting: options.overwrite, embedMetadata: options.metadata, metadataEmbedder: config.metadataEmbedder, @@ -290,9 +301,9 @@ if (options.help || [ } // logger.emptyLine(); - // logger.info(`Finished in ${((Date.now() - startDate) / 1000 / 60).toFixed(2)} minute(s)`) + // logger.log(`Finished in ${((Date.now() - startDate) / 1000 / 60).toFixed(2)} minute(s)`, 'info'); - async function addTrack(trackId) { + async function addTrack(trackId: number) { const artists = []; const albumArtists = []; @@ -312,13 +323,13 @@ if (options.help || [ albumArtists }); - logger.info(`Found track: ${Logger.applyColor({ bold: true }, `${track.fullTitle} - ${track.artists[0].name}`)} (${track.id})`, true, true); + logger.log(`Found track: ${Logger.applyColor({ bold: true }, `${track.fullTitle} - ${track.artists[0].name}`)} (${track.id})`, 'info', true, true); } catch (err) { - logger.error(`Could not find track ID: ${Logger.applyColor({ bold: true }, trackId)}`, true, true); + logger.log(`Could not find track ID: ${Logger.applyColor({ bold: true }, trackId)}`, 'error', true, true); } } - async function addAlbum(albumId) { + async function addAlbum(albumId: number) { const tracks = []; try { @@ -343,13 +354,13 @@ if (options.help || [ }); } - logger.info(`Found album: ${Logger.applyColor({ bold: true }, `${album.title} - ${album.artists[0].name}`)} (${album.id})`, true, true); + logger.log(`Found album: ${Logger.applyColor({ bold: true }, `${album.title} - ${album.artists[0].name}`)} (${album.id})`, 'info', true, true); } catch (err) { - logger.error(`Could not find album ID: ${Logger.applyColor({ bold: true }, albumId)}`, true, true); + logger.log(`Could not find album ID: ${Logger.applyColor({ bold: true }, albumId)}`, 'error', true, true); } } - async function addVideo(videoId) { + async function addVideo(videoId: number) { const artists = []; try { @@ -362,13 +373,13 @@ if (options.help || [ artists, }); - logger.info(`Found video: ${Logger.applyColor({ bold: true }, `${video.title} - ${video.artists[0].name}`)} (${video.id})`, true, true); + logger.log(`Found video: ${Logger.applyColor({ bold: true }, `${video.title} - ${video.artists[0].name}`)} (${video.id})`, 'info', true, true); } catch (err) { - logger.error(`Could not find video ID: ${Logger.applyColor({ bold: true }, videoId)}`, true, true); + logger.log(`Could not find video ID: ${Logger.applyColor({ bold: true }, videoId)}`, 'error', true, true); } } - async function addArtist(artistId) { + async function addArtist(artistId: number) { try { const artist = await findArtist(artistId); @@ -394,13 +405,13 @@ if (options.help || [ } } - logger.info(`Found artist: ${Logger.applyColor({ bold: true }, `${artist.name} - ${artist.albums.length} albums`)} (${artist.id})`, true, true); + logger.log(`Found artist: ${Logger.applyColor({ bold: true }, `${artist.name} - ${artist.albums.length} albums`)} (${artist.id})`, 'info', true, true); } catch (err) { - logger.error(`Could not find artist ID: ${Logger.applyColor({ bold: true }, artistId)}`, true, true); + logger.log(`Could not find artist ID: ${Logger.applyColor({ bold: true }, artistId)}`, 'error', true, true); } } - async function addPlaylist(playlistUuid) { + async function addPlaylist(playlistUuid: string) { try { const playlist = await getPlaylist(playlistUuid); @@ -409,15 +420,17 @@ if (options.help || [ // We don't need to fetch the track/video here, everything needed seems to be included already if (itemType === 'track') { + const track = item as Track; + const artists = []; const albumArtists = []; - const album = await findAlbum(item.album.id, item.album); - for (const artist of item.artists || []) artists.push(await findArtist(artist.id, artist)); + const album = await findAlbum(track.album.id, track.album); + for (const artist of track.artists || []) artists.push(await findArtist(artist.id, artist)); for (const artist of album.artists || []) albumArtists.push(await findArtist(artist.id, artist)); queue.push({ - track: item, + track, album, artists, albumArtists, @@ -425,12 +438,14 @@ if (options.help || [ itemIndex }); } else if (itemType === 'video') { + const video = item as Video; + const artists = []; - for (const artist of item.artists) artists.push(await findArtist(artist.id, artist)); + for (const artist of video.artists) artists.push(await findArtist(artist.id, artist)); queue.push({ - video: item, + video, artists, playlist, itemIndex @@ -438,13 +453,13 @@ if (options.help || [ } } - logger.info(`Found playlist: ${Logger.applyColor({ bold: true }, `${playlist.title} - ${playlist.items.length} items`)} (${playlist.uuid})`, true, true); + logger.log(`Found playlist: ${Logger.applyColor({ bold: true }, `${playlist.title} - ${playlist.items.length} items`)} (${playlist.uuid})`, 'info', true, true); } catch (err) { - logger.error(`Could not find playlist UUID: ${Logger.applyColor({ bold: true }, playlistUuid)}`, true, true); + logger.log(`Could not find playlist UUID: ${Logger.applyColor({ bold: true }, playlistUuid)}`, 'error', true, true); } } - async function addMix(mixId) { + async function addMix(mixId: number) { try { const mix = await getMix(mixId); @@ -469,25 +484,25 @@ if (options.help || [ }); } - logger.info(`Found mix: ${Logger.applyColor({ bold: true }, `${mix.title} - ${mix.subTitle}`)} (${mix.id})`, true, true); + logger.log(`Found mix: ${Logger.applyColor({ bold: true }, `${mix.title} - ${mix.subTitle}`)} (${mix.id})`, 'info', true, true); } catch (err) { - logger.error(`Could not find mix ID: ${Logger.applyColor({ bold: true }, mixId)}`, true, true); + logger.log(`Could not find mix ID: ${Logger.applyColor({ bold: true }, mixId)}`, 'error', true, true); } } - async function findTrack(trackId, partialData) { + async function findTrack(trackId: number, partialData?: Track) { const foundTrack = tracks.find(track => track.id === trackId); if (foundTrack) { - logger.debug(`Found already fetched track: ${trackId}`); + logger.log(`Found already fetched track: ${trackId}`, 'debug'); return foundTrack; } else if (partialData && config.forcePartialData) { - logger.warn(`Using partial data for track ${Logger.applyColor({ bold: true }, trackId)}, some information may be missing!`, true, true); + logger.log(`Using partial data for track ${Logger.applyColor({ bold: true }, trackId)}, some information may be missing!`, 'warn', true, true); return partialData; } else { - logger.info(`Getting information about track: ${Logger.applyColor({ bold: true }, trackId)}`, true); + logger.log(`Getting information about track: ${Logger.applyColor({ bold: true }, trackId)}`, 'info', true); const track = await getTrack(trackId).catch(err => { if (partialData && config.partialDataFallback) { - logger.warn(`Failed to get track ${Logger.applyColor({ bold: true }, trackId)}, some information may be missing!`, true, true); + logger.log(`Failed to get track ${Logger.applyColor({ bold: true }, trackId)}, some information may be missing!`, 'warn', true, true); return partialData; } else throw err; }); @@ -496,19 +511,19 @@ if (options.help || [ } } - async function findAlbum(albumId, partialData) { + async function findAlbum(albumId: number, partialData?: Album) { const foundAlbum = albums.find(album => album.id === albumId); if (foundAlbum) { - logger.debug(`Found already fetched album: ${albumId}`); + logger.log(`Found already fetched album: ${albumId}`, 'debug'); return foundAlbum; } else if (partialData && config.forcePartialData) { - logger.warn(`Using partial data for album ${Logger.applyColor({ bold: true }, albumId)}, some information may be missing!`, true, true); + logger.log(`Using partial data for album ${Logger.applyColor({ bold: true }, albumId)}, some information may be missing!`, 'warn', true, true); return partialData; } else { - logger.info(`Getting information about album: ${Logger.applyColor({ bold: true }, albumId)}`, true); + logger.log(`Getting information about album: ${Logger.applyColor({ bold: true }, albumId)}`, 'info', true); const album = await getAlbum(albumId).catch(err => { if (partialData && config.partialDataFallback) { - logger.warn(`Failed to get album ${Logger.applyColor({ bold: true }, albumId)}, some information may be missing!`, true, true); + logger.log(`Failed to get album ${Logger.applyColor({ bold: true }, albumId)}, some information may be missing!`, 'warn', true, true); return partialData; } else throw err; }); @@ -517,32 +532,32 @@ if (options.help || [ } } - async function findVideo(videoId) { - const foundVideo = videos.find(video => video.id === videoId); + async function findVideo(videoId: number): Promise { + const foundVideo = videos.find((video: any) => video.id === videoId); if (foundVideo) { - logger.debug(`Found already fetched video: ${videoId}`); + logger.log(`Found already fetched video: ${videoId}`, 'debug'); return videoId; } else { - logger.info(`Getting information about video: ${Logger.applyColor({ bold: true }, videoId)}`, true); + logger.log(`Getting information about video: ${Logger.applyColor({ bold: true }, videoId)}`, 'info', true); const video = await getVideo(videoId); videos.push(video); return video; } } - async function findArtist(artistId, partialData) { + async function findArtist(artistId: number, partialData?: Artist) { const foundArtist = artists.find(artist => artist.id === artistId); if (foundArtist) { - logger.debug(`Found already fetched artist: ${artistId}`); + logger.log(`Found already fetched artist: ${artistId}`, 'debug'); return foundArtist; } else if (partialData && config.forcePartialData) { - logger.warn(`Using partial data for artist ${Logger.applyColor({ bold: true }, artistId)}, some information may be missing!`, true, true); + logger.log(`Using partial data for artist ${Logger.applyColor({ bold: true }, artistId)}, some information may be missing!`, 'warn', true, true); return partialData; } else { - logger.info(`Getting information about artist: ${Logger.applyColor({ bold: true }, artistId)}`, true); + logger.log(`Getting information about artist: ${Logger.applyColor({ bold: true }, artistId)}`, 'info', true); const artist = await getArtist(artistId).catch(err => { if (partialData && config.partialDataFallback) { - logger.warn(`Failed to get artist ${Logger.applyColor({ bold: true }, artistId)}, some information may be missing!`, true, true); + logger.log(`Failed to get artist ${Logger.applyColor({ bold: true }, artistId)}, some information may be missing!`, 'warn', true, true); return partialData; } else throw err; }); @@ -554,11 +569,11 @@ if (options.help || [ async function authorize() { if (secrets.accessToken && - secrets.accessTokenExpiry > Date.now()) return logger.debug('Token still valid, not refreshing'); // Previous token is still valid + secrets.accessTokenExpiry > Date.now()) return logger.log('Token still valid, not refreshing', 'debug'); // Previous token is still valid if (secrets.refreshToken && secrets.clientId && secrets.clientSecret) { // Refresh token exists - logger.info('Refreshing token'); + logger.log('Refreshing token', 'info'); await getToken('refresh_token', { refreshToken: secrets.refreshToken, clientId: secrets.clientId, @@ -572,17 +587,17 @@ async function authorize() { secrets.countryCode = token.user?.countryCode; secrets.userId = token.user_id; }).catch(err => { - logger.error(`Failed to refresh token: ${err?.error_description || 'No error description'} [${err?.sub_status || 'No error code'}]`); + logger.log(`Failed to refresh token: ${err?.error_description || 'No error description'} [${err?.sub_status || 'No error code'}]`, 'error'); }); } if (!secrets.accessToken || secrets.accessTokenExpiry <= Date.now()) { - logger.debug('Attempting to authorize with device authorization'); + logger.log('Attempting to authorize with device authorization', 'debug'); await authorizeWithDeviceAuthorization({ clientId: config.clientId, clientSecret: config.clientSecret, scope: config.scope - }).then(token => { + }).then((token: any) => { secrets.tokenType = token.token_type; secrets.accessToken = token.access_token; secrets.accessTokenExpiry = Date.now() + (token.expires_in * 1000); @@ -600,9 +615,13 @@ async function authorize() { if (secretsPath) fs.writeFileSync(secretsPath, JSON.stringify(secrets, null, 4)); } -async function authorizeWithDeviceAuthorization(params = {}) { +async function authorizeWithDeviceAuthorization(params: { + clientId: string; + clientSecret: string; + scope: string[]; +}) { const deviceAuthorization = await requestDeviceAuthorization(params.clientId, params.scope); - logger.info(`Please visit ${Logger.applyColor({ bold: true }, `https://${deviceAuthorization.verificationUriComplete || `${deviceAuthorization.verificationUri || 'link.tidal.com'}/${deviceAuthorization.userCode}`}`)} to log in to your TIDAL account.\nWaiting for authorization...`); + logger.log(`Please visit ${Logger.applyColor({ bold: true }, `https://${deviceAuthorization.verificationUriComplete || `${deviceAuthorization.verificationUri || 'link.tidal.com'}/${deviceAuthorization.userCode}`}`)} to log in to your TIDAL account.\nWaiting for authorization...`, 'info'); const deviceAuthorizationStart = Date.now(); const token = await new Promise((resolve, reject) => { @@ -618,7 +637,7 @@ async function authorizeWithDeviceAuthorization(params = {}) { }).catch(err => { if (Date.now() - deviceAuthorizationStart >= deviceAuthorization.expiresIn * 1000) { // Code expired - logger.warn('Code expired!'); + logger.log('Code expired!', 'warn'); return authorizeWithDeviceAuthorization(params); } if (err.sub_status !== 1002) { @@ -637,8 +656,7 @@ async function authorizeWithDeviceAuthorization(params = {}) { function showHelp() { // hell hell hell hell hell - logger.log(null, -` + logger.log(` Usage: ${process.argv0}${path.dirname(process.execPath) === process.cwd() ? '' : ' .'} [options...] Options: diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..3fa8e9d --- /dev/null +++ b/src/types.ts @@ -0,0 +1,128 @@ +// TODO: find optional stuff in all types +// TODO: move fake enums in global.ts to here + +export type Track = { + id: number; + title: string; + fullTitle: string; + version?: string; + duration: number; + upload: boolean; + copyright: string; + explicit: boolean; + mixId: string; + isrc: string; + quality: string; // TODO: enum + modes: string[]; // TODO: enum + qualityTypes: string[]; // TODO: enum + trackNumber: number; + volumeNumber: number; + replayGain: number; + peak: number; + bpm: number; + key: string; // TODO: enum? + keyScale: string; // TODO: enum + url: string; + artists: Artist[]; + album: Album; +}; + +export type Album = { + id: number; + title: string; + version?: string; + description: string; + type: string; // TODO: enum + duration: number; + upload: boolean; + trackCount: number; + volumeCount: number; + releaseDate: string; + copyright: string; + explicit: boolean; + upc: string; + covers?: any; // TODO: type + videoCovers?: any; // TODO: type + quality: string; // TODO: enum + modes: string[]; // TODO: enum + qualityTypes: string[]; // TODO: enum + credits?: Credit[]; + trackCredits?: { track: Track; credits: Credit[]; }[]; + review?: { + originalText: string; + text: string; + source: string; + }; + url: string; + artists: Artist[]; + tracks: Track[]; +}; + +export type Artist = { + id: number; + name: number; + biography?: { + originalText: string; + text: string; + source: string; + }; + pictures?: any; // TODO: type + types: string[]; // TODO: enum + roles: { id: number; category: string; }[]; // TODO: enum on category + albums: Album[]; +}; + +export type Video = { + id: number; + title: string; + type: string; // TODO: enum + duration: number; + releaseDate: string; + explicit: boolean; + quality: string; // TODO: enum + images?: any; + trackNumber: number; + volumeNumber: number; + artists: Artist[]; +}; + +export type Playlist = { + uuid: string; + title: string; + description: string; + duration: number; + images?: any; // TODO + customImage: string; + sharing: SharingLevel; + created: string; + lastUpdated: string; + items: ( + { type: 'track', item: Track } | + { type: 'video', item: Video } + )[]; +} + +export type Mix = { + id: string; + title: string; + subTitle: string; + shortSubTitle: string; + description: string; + images: any; // TODO + detailImages: any; // TODO + tracks: Track[]; +}; + +export type Credit = { + type: string; + tagName: string; + contributors: { + name: string; + id: number; + }[]; +}; + +export enum SharingLevel { + PUBLIC = 'PUBLIC', + PRIVATE = 'PRIVATE' +}; \ No newline at end of file diff --git a/src/utils/Args.js b/src/utils/Args.ts similarity index 61% rename from src/utils/Args.js rename to src/utils/Args.ts index e05999f..8642aed 100644 --- a/src/utils/Args.js +++ b/src/utils/Args.ts @@ -1,7 +1,22 @@ -class Args { - args = []; - constructor(argv = process.argv, argOptions = []) { +type Options = { + name: string; + aliases?: string[]; + shortName?: string; + shortAliases?: string[]; + type?: string; + noValue?: boolean; + default?: any; +} + +export default class Args { + args: { + name: string; + value: any; + options: Options; + }[] = []; + + constructor(argv: string[] = process.argv, argOptions: Options[] = []) { argv.forEach((arg, argIndex) => { const shortArg = arg.match(/^-([^-][^\s]*)$/)?.[1]; const longArg = arg.match(/^--([^-][^\s]*)$/)?.[1]; @@ -9,10 +24,10 @@ class Args { if (!argName) return; const options = argOptions.find(options => { + if (options.name === longArg) return true; + if (longArg && options.aliases && options.aliases.includes(longArg)) return true; if (options.shortName && options.shortName === shortArg) return true; - if (options.shortAliases && options.shortAliases.includes(shortArg)) return true; - if (options.name && options.name === longArg) return true; - if (options.aliases && options.aliases.includes(longArg)) return true; + if (shortArg && options.shortAliases && options.shortAliases.includes(shortArg)) return true; }); if (!options) return; @@ -34,13 +49,11 @@ class Args { }); } - get(name) { + get(name: string) { return this.getAll(name)[0]; } - getAll(name) { - return this.args.filter(arg => arg.name === name).map(arg => arg.value ?? arg.default); + getAll(name: string) { + return this.args.filter(arg => arg.name === name).map(arg => arg.value ?? arg.options.default); } -} - -module.exports = Args; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/Download.js b/src/utils/Download.ts similarity index 84% rename from src/utils/Download.js rename to src/utils/Download.ts index 896e6ae..15f14f7 100644 --- a/src/utils/Download.js +++ b/src/utils/Download.ts @@ -1,21 +1,61 @@ -const fs = require('fs'); -const path = require('path'); -const { setTimeout } = require('timers/promises'); - -const Logger = require('./Logger'); -const getPlaybackInfo = require('./getPlaybackInfo'); -const getTrackManifest = require('./getTrackManifest'); -const parseManifest = require('./parseManifest'); -const createMedia = require('./createMedia'); -const embedMetadata = require('./embedMetadata'); -const extractContainer = require('./extractContainer'); -const getLyrics = require('./getLyrics'); -const formatString = require('./formatString'); -const normalizeTag = require('./normalizeTag'); -const capitalize = require('./capitalize'); - -class Download { - constructor(options = { }) { +import fs from 'fs'; +import path from 'path'; +import { setTimeout } from 'timers/promises'; + +import Logger from './Logger'; +import getPlaybackInfo from './getPlaybackInfo'; +// import getTrackManifest from './getTrackManifest'; +import parseManifest from './parseManifest'; +import createMedia from './createMedia'; +import embedMetadata from './embedMetadata'; +import extractContainer from './extractContainer'; +import getLyrics from './getLyrics'; +import formatString from './formatString'; +import normalizeTag from './normalizeTag'; +import capitalize from './capitalize'; + +export default class Download { + details; + logger; + directory; + mediaFilename; + coverFilename; + playlistCoverFilename; + playlistFileFilename; + trackQuality; + videoQuality; + overwriteExisting; + embedMetadata; + metadataEmbedder; + keepCoverFile; + getCover; + getLyrics; + syncedLyricsOnly; + plainLyricsOnly; + externalLyrics; + useArtistsTag; + artistTagSeparator; + roleTagSeparator; + customMetadata; + keepOriginalFile; + segmentWaitMin; + segmentWaitMax; + downloadLogPadding; + logPrefix; + useDolbyAtmos; + + playbackInfo; + manifest; + segmentUrls; + originalExtension; + mediaExtension; + coverExtension; + lyrics; + metadata; + + fileStream?: fs.WriteStream; + + constructor(options: any = { }) { this.details = options.details; this.logger = options.logger; this.directory = options.directory; @@ -69,7 +109,7 @@ class Download { if (!this.keepOriginalFile) fs.unlinkSync(this.getOriginalPath()); // Delete container file if (!this.keepCoverFile && fs.existsSync(this.getCoverPath())) fs.unlinkSync(this.getCoverPath()); // Delete cover file - this.log(`Completed in ${Math.floor((Date.now() - startDate) / 1000)}s ${Logger.applyColor({ bold: true }, `[${[this.playbackInfo.bitDepth && this.playbackInfo.sampleRate && `${this.playbackInfo.bitDepth}-bit/${this.playbackInfo.sampleRate / 1000} kHz`, `${(this.fileStream.bytesWritten / 1024 / 1024).toFixed(2)} MB`].filter(i => i).join(', ')}]`)}`); + this.log(`Completed in ${Math.floor((Date.now() - startDate) / 1000)}s ${Logger.applyColor({ bold: true }, `[${[this.playbackInfo.bitDepth && this.playbackInfo.sampleRate && `${this.playbackInfo.bitDepth}-bit/${this.playbackInfo.sampleRate / 1000} kHz`, `${(this.fileStream!.bytesWritten / 1024 / 1024).toFixed(2)} MB`].filter(i => i).join(', ')}]`)}`); } async createPlaylist() { @@ -118,13 +158,13 @@ class Download { const segmentManifests = this.manifest.mainManifests[0].segmentManifests; const segmentManifest = this.videoQuality ? - segmentManifests.reduce((closest, curr) => { + segmentManifests.reduce((closest: any, curr: any) => { const height = parseInt(curr.resolution.split('x')[1], 10); const closestHeight = parseInt(closest.resolution.split('x')[1], 10); const targetHeight = parseInt(this.videoQuality, 10); return Math.abs(height - targetHeight) < Math.abs(closestHeight - targetHeight) ? curr : closest; }) : - segmentManifests.reduce((highest, curr) => { + segmentManifests.reduce((highest: any, curr: any) => { const height = parseInt(curr.resolution.split('x')[1], 10); const highestHeight = parseInt(highest.resolution.split('x')[1], 10); return height > highestHeight ? curr : highest; @@ -168,10 +208,8 @@ class Download { // Add to M3U playlist if (this.playlistFileFilename && fs.existsSync(this.getPlaylistFilePath())) { - // #EXTINF:, - - // fs.appendFileSync(this.getPlaylistFilePath(), `#EXTINF:${this.details.duration},${this.details.artists.map(artist => artist.name).join(', ')} - ${this.details.title}\r\n${path.basename(this.getMediaPath())}\r\n`); // #EXTINF:,
- - fs.appendFileSync(this.getPlaylistFilePath(), `#EXTINF:${this.details.duration},${this.details.artist.name} - ${this.details.title}\r\n${path.basename(this.getMediaPath())}\r\n`); + fs.appendFileSync(this.getPlaylistFilePath(), `#EXTINF:${this.details.duration / 1000},${this.details.artist.name} - ${this.details.title}\r\n${path.basename(this.getMediaPath())}\r\n`); } // Metadata @@ -179,19 +217,19 @@ class Download { const track = this.details.track; const albumCredits = this.details.album?.credits || []; - const trackCredits = this.details.album?.trackCredits?.find(({ track }) => track.id === this.details.id)?.credits || []; + const trackCredits = this.details.album?.trackCredits?.find(({ track }: any) => track.id === this.details.id)?.credits || []; - const customMetadata = this.customMetadata?.map(i => ([i[0], formatString(i[1], this.details)])) || []; - const creditMetadata = [...trackCredits, ...albumCredits].map(credit => credit.tagName ? [credit.tagName, normalizeTag(credit.contributors.map(i => i.name), this.roleTagSeparator)] : null).filter(i => i); + const customMetadata = this.customMetadata?.map((i: any) => ([i[0], formatString(i[1], this.details)])) || []; + const creditMetadata = [...trackCredits, ...albumCredits].map(credit => credit.tagName ? [credit.tagName, normalizeTag(credit.contributors.map((i: any) => i.name), this.roleTagSeparator)] : null).filter(i => i); this.metadata = [ ['title', this.details.title], - ['artist', normalizeTag(this.details.artists?.map(i => i.name), !this.useArtistsTag ? this.artistTagSeparator : null)], - ['artists', this.useArtistsTag ? normalizeTag(this.details.artists?.map(i => i.name), this.artistTagSeparator) : null], + ['artist', normalizeTag(this.details.artists?.map((i: any) => i.name), !this.useArtistsTag ? this.artistTagSeparator : null)], + ['artists', this.useArtistsTag ? normalizeTag(this.details.artists?.map((i: any) => i.name), this.artistTagSeparator) : null], ['version', track?.version], ['album', album?.title], - ['albumartist', normalizeTag(this.details.albumArtists?.map(i => i.name), !this.useArtistsTag ? this.artistTagSeparator : null)], - ['albumartists', this.useArtistsTag ? normalizeTag(this.details.albumArtists?.map(i => i.name), this.artistTagSeparator) : null], + ['albumartist', normalizeTag(this.details.albumArtists?.map((i: any) => i.name), !this.useArtistsTag ? this.artistTagSeparator : null)], + ['albumartists', this.useArtistsTag ? normalizeTag(this.details.albumArtists?.map((i: any) => i.name), this.artistTagSeparator) : null], ['albumversion', album?.version], ['releasetype', album?.type && album.type.length > 2 ? capitalize(album.type) : album?.type], ['date', this.details.releaseDate], @@ -216,11 +254,11 @@ class Download { ].filter(([tag, value]) => value !== undefined && value !== null); // most overkill debug log ever - this.logger.debug(`Metadata:\n${this.metadata.map(([tag, value]) => { + this.logger.log(`Metadata:\n${this.metadata.map(([tag, value]: any) => { const padding = ' '.repeat(Logger.getDisplayedLength(this.logger.getLevel('debug')?.prefix || '')); const valuePrefix = `${tag}: `.padEnd(25, ' '); return `${padding}${valuePrefix}${value.toString().replace(/\n/g, `\n${padding}${' '.repeat(Logger.getDisplayedLength(valuePrefix))}`)}`; - }).join('\n')}`); + }).join('\n')}`, 'debug'); } async downloadSegments() { @@ -236,7 +274,7 @@ class Download { if (!this.fileStream.write(segmentData)) { // buffer full, wait for drain - await new Promise(resolve => this.fileStream.once('drain', resolve)); + await new Promise(resolve => this.fileStream!.once('drain', resolve)); } const delay = Math.floor(Math.random() * (this.segmentWaitMax - this.segmentWaitMin + 1) + this.segmentWaitMin); @@ -298,17 +336,15 @@ class Download { return path.join(this.directory, `${this.playlistCoverFilename}${this.coverExtension}`); } - log(msg, level) { + log(msg: string, level?: string) { const levelPrefix = this.logger.getLevel(level)?.prefix; const logPrefix = this.logPrefix || `Downloading ${Logger.applyColor({ bold: true }, this.details.title)} - ${Logger.applyColor({ bold: true }, this.details.artist.name)}: `; const padding = ' '.repeat(Math.max(this.downloadLogPadding - Logger.getDisplayedLength(`${levelPrefix || ''}${logPrefix}`), 0)); const log = `${logPrefix}${padding}${msg}`; if (level) { - this.logger.log(level, log, true, true); + this.logger.log(log, level, true, true); } else { - this.logger.info(log, true); + this.logger.log(log, 'info', true); } } -} - -module.exports = Download; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/Logger.js b/src/utils/Logger.js deleted file mode 100644 index 9ced6ce..0000000 --- a/src/utils/Logger.js +++ /dev/null @@ -1,110 +0,0 @@ -class Logger { - lastLog = ''; - - constructor(options = { }) { - this.debugLogs = options.debugLogs ?? false; - this.levelPadding = options.levelPadding ?? null; - this.levels = options.levels ?? [ - { name: 'INFO', id: 'info', prefix: '' }, - { name: 'WARN', id: 'warn', fgColor: 93 }, - { name: 'ERROR', id: 'error', fgColor: 31 }, - { name: 'DEBUG', id: 'debug', fgColor: 90 }, - ]; - - for (const level of this.levels) { - if (!this.constructor.prototype[level.id]) { - this.constructor.prototype[level.id] = (msg, replaceLine, noStore) => this.log(level.id, msg, replaceLine, noStore); - } - } - } - - static ANSI_CODES = { - RESET: 0, - BOLD: 1, - UNDERLINE: 4, - FG_BLACK: 30, - FG_RED: 31, - FG_GREEN: 32, - FG_YELLOW: 33, - FG_BLUE: 34, - FG_MAGENTA: 35, - FG_CYAN: 36, - FG_WHITE: 37, - FG_BRIGHT_BLACK: 90, - FG_BRIGHT_RED: 91, - FG_BRIGHT_GREEN: 92, - FG_BRIGHT_YELLOW: 93, - FG_BRIGHT_BLUE: 94, - FG_BRIGHT_MAGENTA: 95, - FG_BRIGHT_CYAN: 96, - FG_BRIGHT_WHITE: 97, - BG_BLACK: 40, - BG_RED: 41, - BG_GREEN: 42, - BG_YELLOW: 43, - BG_BLUE: 44, - BG_MAGENTA: 45, - BG_CYAN: 46, - BG_WHITE: 47, - BG_BRIGHT_BLACK: 100, - BG_BRIGHT_RED: 101, - BG_BRIGHT_GREEN: 102, - BG_BRIGHT_YELLOW: 103, - BG_BRIGHT_BLUE: 104, - BG_BRIGHT_MAGENTA: 105, - BG_BRIGHT_CYAN: 106, - BG_BRIGHT_WHITE: 107 - }; - - static applyColor(options = { }, string) { - const ansiCodes = []; - if (options.fg) ansiCodes.push(Logger.ANSI_CODES[`FG_${options.fg}`.toUpperCase()] ?? options.fg); - if (options.bg) ansiCodes.push(Logger.ANSI_CODES[`BG_${options.bg}`.toUpperCase()] ?? options.bg); - if (options.bold) ansiCodes.push(Logger.ANSI_CODES.BOLD); - if (options.underline) ansiCodes.push(Logger.ANSI_CODES.UNDERLINE); - - return `\x1b[${ansiCodes.filter(i => i).join(';')}m${string !== undefined ? `${string}\x1b[${Logger.ANSI_CODES.RESET}m` : ''}`; - } - - static getDisplayedLength(string) { - return string.replace(/\x1b\[[0-9;]*m/g, '').length; - } - - emptyLine() { - this.log(null, ''); - } - - getLevel(levelId) { - const level = this.levels.find(i => i.id === levelId); - if (!level) return; - - return { - ...level, - prefix: level.prefix ?? `[${Logger.applyColor({ fg: level.fgColor, bg: level.bgColor }, level.name)}]${this.levelPadding ? ' '.repeat(this.levelPadding - level.name.length) : ''} `, - suffix: level.suffix ?? '' - } - } - - log(levelId, msg, replaceLine, noStore) { - const level = this.getLevel(levelId); - if (!this.debugLogs && level?.id === 'debug') return; - - if (replaceLine && !this.debugLogs) { - const lastLogLines = this.lastLog.split('\n'); - const [windowWidth, windowHeight] = process.stdout.getWindowSize(); - const lastLogLineCount = lastLogLines.length + lastLogLines.reduce((sum, line) => sum + Math.floor((Logger.getDisplayedLength(line) - 1) / windowWidth), 0); // calculate line count, including \n's and text wrapping - - for (let lineIndex = 0; lineIndex < lastLogLineCount; lineIndex++) { - process.stdout.moveCursor(0, -1); - process.stdout.clearLine(); - } - } - - const log = `${level?.prefix || ''}${msg}${level?.suffix || ''}\n`; - this.lastLog = !noStore ? log : ''; - - process.stdout.write(log); - } -} - -module.exports = Logger; \ No newline at end of file diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts new file mode 100644 index 0000000..33e0486 --- /dev/null +++ b/src/utils/Logger.ts @@ -0,0 +1,128 @@ +export enum ANSI_CODES { + RESET = 0, + BOLD = 1, + UNDERLINE = 4, + FG_BLACK = 30, + FG_RED = 31, + FG_GREEN = 32, + FG_YELLOW = 33, + FG_BLUE = 34, + FG_MAGENTA = 35, + FG_CYAN = 36, + FG_WHITE = 37, + FG_BRIGHT_BLACK = 90, + FG_BRIGHT_RED = 91, + FG_BRIGHT_GREEN = 92, + FG_BRIGHT_YELLOW = 93, + FG_BRIGHT_BLUE = 94, + FG_BRIGHT_MAGENTA = 95, + FG_BRIGHT_CYAN = 96, + FG_BRIGHT_WHITE = 97, + BG_BLACK = 40, + BG_RED = 41, + BG_GREEN = 42, + BG_YELLOW = 43, + BG_BLUE = 44, + BG_MAGENTA = 45, + BG_CYAN = 46, + BG_WHITE = 47, + BG_BRIGHT_BLACK = 100, + BG_BRIGHT_RED = 101, + BG_BRIGHT_GREEN = 102, + BG_BRIGHT_YELLOW = 103, + BG_BRIGHT_BLUE = 104, + BG_BRIGHT_MAGENTA = 105, + BG_BRIGHT_CYAN = 106, + BG_BRIGHT_WHITE = 107 +}; +export default class Logger { + debugLogs: boolean; + levelPadding?: number; + levels: { + id: string; + name: string; + prefix?: string; + suffix?: string; + fgColor?: string | number; + bgColor?: string | number; + }[]; + + lastLog = ''; + + constructor(options: { + debugLogs?: boolean; + levelPadding?: number; + levels?: { + id: string; + name: string; + prefix?: string; + suffix?: string; + fgColor?: string | number; + bgColor?: string | number; + }[]; + } = { }) { + this.debugLogs = options.debugLogs ?? false; + this.levelPadding = options.levelPadding; + this.levels = options.levels ?? [ + { name: 'INFO', id: 'info', prefix: '' }, + { name: 'WARN', id: 'warn', fgColor: 93 }, + { name: 'ERROR', id: 'error', fgColor: 31 }, + { name: 'DEBUG', id: 'debug', fgColor: 90 }, + ]; + } + + static applyColor(options: { + fg?: string | number; + bg?: string | number; + bold?: boolean; + underline?: boolean; + } = { }, string: any) { + const ansiCodes = []; + if (options.fg) ansiCodes.push(ANSI_CODES[`FG_${options.fg}`.toUpperCase() as keyof typeof ANSI_CODES] ?? options.fg); + if (options.bg) ansiCodes.push(ANSI_CODES[`BG_${options.bg}`.toUpperCase() as keyof typeof ANSI_CODES] ?? options.bg); + if (options.bold) ansiCodes.push(ANSI_CODES.BOLD); + if (options.underline) ansiCodes.push(ANSI_CODES.UNDERLINE); + + return `\x1b[${ansiCodes.filter(i => i).join(';')}m${string !== undefined ? `${string}\x1b[${ANSI_CODES.RESET}m` : ''}`; + } + + static getDisplayedLength(string: string) { + return string.replace(/\x1b\[[0-9;]*m/g, '').length; + } + + emptyLine() { + this.log(''); + } + + getLevel(levelId: any) { + const level = this.levels.find(i => i.id === levelId); + if (!level) return; + + return { + ...level, + prefix: level.prefix ?? `[${Logger.applyColor({ fg: level.fgColor, bg: level.bgColor }, level.name)}]${this.levelPadding ? ' '.repeat(this.levelPadding - level.name.length) : ''} `, + suffix: level.suffix ?? '' + } + } + + log(msg: string, levelId?: string, replaceLine?: boolean, noStore?: boolean) { + const level = this.getLevel(levelId); + if (!this.debugLogs && level?.id === 'debug') return; + + if (replaceLine && !this.debugLogs) { + const lastLogLines = this.lastLog.split('\n'); + const [windowWidth, windowHeight] = process.stdout.getWindowSize(); + const lastLogLineCount = lastLogLines.length + lastLogLines.reduce((sum, line) => sum + Math.floor((Logger.getDisplayedLength(line) - 1) / windowWidth), 0); // calculate line count, including \n's and text wrapping + + for (let lineIndex = 0; lineIndex < lastLogLineCount; lineIndex++) { + process.stdout.moveCursor(0, -1); + process.stdout.clearLine(-1); + } + } + + const log = `${level?.prefix || ''}${msg}${level?.suffix || ''}\n`; + this.lastLog = !noStore ? log : ''; + + process.stdout.write(log); + } +} \ No newline at end of file diff --git a/src/utils/capitalize.js b/src/utils/capitalize.ts similarity index 57% rename from src/utils/capitalize.js rename to src/utils/capitalize.ts index d2e2257..c60154c 100644 --- a/src/utils/capitalize.js +++ b/src/utils/capitalize.ts @@ -1,5 +1,3 @@ -function capitalize(string) { +export default function capitalize(string: string) { return `${string.charAt(0).toUpperCase()}${string.substring(1).toLowerCase()}`; -} - -module.exports = capitalize; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/createMedia.js b/src/utils/createMedia.ts similarity index 77% rename from src/utils/createMedia.js rename to src/utils/createMedia.ts index 4a4b3df..bca307c 100644 --- a/src/utils/createMedia.js +++ b/src/utils/createMedia.ts @@ -1,8 +1,8 @@ -const spawn = require('./spawn'); +import spawn from './spawn'; -const { config } = require('../globals'); +import { config } from '../globals'; -function createMedia(inputPath, outputPath, metadata, coverPath, streams = 1) { +export default async function createMedia(inputPath: string, outputPath: string, metadata: [string, string][], coverPath?: string, streams = 1) { return spawn(config.ffmpegPath, [ '-i', inputPath, ...(coverPath ? [ @@ -20,6 +20,4 @@ function createMedia(inputPath, outputPath, metadata, coverPath, streams = 1) { ]).then(spawnedProcess => { if (spawnedProcess.code > 0) throw new Error(`Exited with code ${spawnedProcess.code}! Output:\n${spawnedProcess.stderr.toString()}`); }); -} - -module.exports = createMedia; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/embedMetadata.js b/src/utils/embedMetadata.ts similarity index 70% rename from src/utils/embedMetadata.js rename to src/utils/embedMetadata.ts index d64e311..3010f00 100644 --- a/src/utils/embedMetadata.js +++ b/src/utils/embedMetadata.ts @@ -1,8 +1,8 @@ -const spawn = require('./spawn'); +import spawn from './spawn'; -const { config } = require('../globals'); +import { config } from '../globals'; -function embedMetadata(file, tags) { +export default function embedMetadata(file: string, tags: string[]) { return spawn(config.kid3CliPath, [ ...tags.map(([tag, value, isFile]) => { if (isFile) { @@ -15,8 +15,6 @@ function embedMetadata(file, tags) { ]); }; -function escapeQuotes(input) { +function escapeQuotes(input: string) { return input.toString().replace(/"/g, i => `\\${i}`); -} - -module.exports = embedMetadata; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/extractContainer.js b/src/utils/extractContainer.ts similarity index 60% rename from src/utils/extractContainer.js rename to src/utils/extractContainer.ts index dc55678..7b730de 100644 --- a/src/utils/extractContainer.js +++ b/src/utils/extractContainer.ts @@ -1,17 +1,15 @@ -const spawn = require('./spawn'); +import spawn from './spawn'; -const { config } = require('../globals'); +import { config } from '../globals'; -function extractAudioStream(inputPath, outputPath) { +export default async function extractAudioStream(inputPath: string, outputPath: string) { return spawn(config.ffmpegPath, [ '-i', inputPath, '-map_metadata', '-1', '-c', 'copy', outputPath, '-y' - ]).then(spawnedProcess => { + ]).then((spawnedProcess: any) => { if (spawnedProcess.code > 0) throw new Error(`Exited with code ${spawnedProcess.code}! Output:\n${spawnedProcess.stderr.toString()}`); }); -} - -module.exports = extractAudioStream; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/formatPath.js b/src/utils/formatPath.ts similarity index 63% rename from src/utils/formatPath.js rename to src/utils/formatPath.ts index 3923ff4..afb9e0f 100644 --- a/src/utils/formatPath.js +++ b/src/utils/formatPath.ts @@ -1,14 +1,12 @@ -const path = require('path'); +import path from 'path'; -const formatString = require('./formatString'); +import formatString from './formatString'; -function formatPath(unformattedPath, obj) { +export default function formatPath(unformattedPath: string, obj: { }) { const { root } = path.parse(unformattedPath); return `${root}${path.normalize(unformattedPath) .replace(root, '') .split(path.sep) .map(i => formatString(i, obj).replace(/\/|\\|\?|\*|\:|\||\"|\<|\>/g, '')) .join(path.sep)}`; -}; - -module.exports = formatPath; \ No newline at end of file +}; \ No newline at end of file diff --git a/src/utils/formatString.js b/src/utils/formatString.js deleted file mode 100644 index 8d3133d..0000000 --- a/src/utils/formatString.js +++ /dev/null @@ -1,6 +0,0 @@ -function parseString(string, obj) { - return string - .replace(/{{(.*?)}}/g, (match, group) => group.split('.').reduce((acc, key) => acc && acc[key], obj) || ''); -} - -module.exports = parseString; \ No newline at end of file diff --git a/src/utils/formatString.ts b/src/utils/formatString.ts new file mode 100644 index 0000000..74afbab --- /dev/null +++ b/src/utils/formatString.ts @@ -0,0 +1,4 @@ +export default function formatString(string: string, obj: { }) { + return string + .replace(/{{(.*?)}}/g, (match, group) => group.split('.').reduce((acc: any, key: string) => acc && acc[key], obj) || ''); +} \ No newline at end of file diff --git a/src/utils/getAlbum.js b/src/utils/getAlbum.ts similarity index 68% rename from src/utils/getAlbum.js rename to src/utils/getAlbum.ts index e5765bd..9ea1884 100644 --- a/src/utils/getAlbum.js +++ b/src/utils/getAlbum.ts @@ -1,13 +1,14 @@ -const tidalApi = require('./tidalApi'); -const parseAlbum = require('./parseAlbum'); +import tidalApi from './tidalApi'; -async function getAlbum(albumId) { +import parseAlbum from './parseAlbum'; + +export default async function getAlbum(albumId: number) { return parseAlbum( await tidalApi('privatev1', `/albums/${albumId}`).then(({ json }) => json), await tidalApi('privatev1', '/pages/album', { query: { albumId } }).then(async ({ json }) => { const { album, description, credits, review } = json.rows[0].modules[0]; - const tracks = json.rows[1].modules[0].pagedList.items.filter(({ type }) => type === 'track').map(({ item }) => item); // Default limit seems to be 9999 - const trackCredits = await tidalApi('privatev1', `/albums/${albumId}/items/credits?limit=100`).then(({ json }) => json.items.filter(item => item.type === 'track')); // Limit is 100 + const tracks = json.rows[1].modules[0].pagedList.items.filter(({ type }: any) => type === 'track').map(({ item }: any) => item); // Default limit seems to be 9999 + const trackCredits = await tidalApi('privatev1', `/albums/${albumId}/items/credits?limit=100`).then(({ json }) => json.items.filter((item: any) => item.type === 'track')); // Limit is 100 return { ...album, description, @@ -18,6 +19,4 @@ async function getAlbum(albumId) { }; }) ); -} - -module.exports = getAlbum; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/getArtist.js b/src/utils/getArtist.js deleted file mode 100644 index 1b19f48..0000000 --- a/src/utils/getArtist.js +++ /dev/null @@ -1,11 +0,0 @@ -const tidalApi = require('./tidalApi'); -const parseArtist = require('./parseArtist'); - -function getArtist(artistId) { - return tidalApi('privatev2', `/artist/${artistId}`).then(({ json }) => parseArtist(json.item.data, { - biography: json.header.biography, - albums: json.items.filter(item => item.moduleId === 'ARTIST_ALBUMS' || item.moduleId === 'ARTIST_TOP_SINGLES').map(({ items }) => items.map(({ data }) => data)).flat() - })); -} - -module.exports = getArtist; \ No newline at end of file diff --git a/src/utils/getArtist.ts b/src/utils/getArtist.ts new file mode 100644 index 0000000..64a084e --- /dev/null +++ b/src/utils/getArtist.ts @@ -0,0 +1,10 @@ +import tidalApi from './tidalApi'; + +import parseArtist from './parseArtist'; + +export default async function getArtist(artistId: number) { + return tidalApi('privatev2', `/artist/${artistId}`).then(({ json }) => parseArtist(json.item.data, { + biography: json.header.biography, + albums: json.items.filter((item: any) => item.moduleId === 'ARTIST_ALBUMS' || item.moduleId === 'ARTIST_TOP_SINGLES').map(({ items }: any) => items.map(({ data }: any) => data)).flat() + })); +} \ No newline at end of file diff --git a/src/utils/getLyrics.js b/src/utils/getLyrics.ts similarity index 60% rename from src/utils/getLyrics.js rename to src/utils/getLyrics.ts index 06ebff8..2582508 100644 --- a/src/utils/getLyrics.js +++ b/src/utils/getLyrics.ts @@ -1,11 +1,9 @@ -const tidalApi = require('./tidalApi'); +import tidalApi from './tidalApi'; -function getLyrics(trackId) { - return tidalApi('privatev1', `/tracks/${trackId}/lyrics`).then(({ json }) => ({ +export default async function getLyrics(trackId: number) { + return tidalApi('privatev1', `/tracks/${trackId}/lyrics`).then(({ json }: any) => ({ provider: json.lyricsProvider, plainLyrics: json.lyrics, syncedLyrics: json.subtitles, })); -} - -module.exports = getLyrics; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/getMix.js b/src/utils/getMix.ts similarity index 58% rename from src/utils/getMix.js rename to src/utils/getMix.ts index fc3487b..4337f50 100644 --- a/src/utils/getMix.js +++ b/src/utils/getMix.ts @@ -1,10 +1,9 @@ -const tidalApi = require('./tidalApi'); -const parseMix = require('./parseMix'); +import tidalApi from './tidalApi'; -async function getMix(mixId) { +import parseMix from './parseMix'; + +export default async function getMix(mixId: number) { return tidalApi('privatev1', '/pages/mix', { query: { mixId } }).then(({ json }) => parseMix(json.rows[0].modules[0].mix, { items: json.rows[1].modules[0].pagedList.items })); -} - -module.exports = getMix; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/getPlaybackInfo.js b/src/utils/getPlaybackInfo.ts similarity index 65% rename from src/utils/getPlaybackInfo.js rename to src/utils/getPlaybackInfo.ts index 16cc073..42a2fe5 100644 --- a/src/utils/getPlaybackInfo.js +++ b/src/utils/getPlaybackInfo.ts @@ -1,6 +1,6 @@ -const tidalApi = require('./tidalApi'); +import tidalApi from './tidalApi'; -function getPlaybackInfo(id, type = 'track', quality = 'HI_RES_LOSSLESS', immersiveAudio = false, playbackMode = 'STREAM', assetPresentation = 'FULL') { +export default async function getPlaybackInfo(id: number, type = 'track', quality = 'HI_RES_LOSSLESS', immersiveAudio = false, playbackMode = 'STREAM', assetPresentation = 'FULL') { const isVideo = type === 'video' ? true : false; return tidalApi('privatev1', `/${type === 'video' ? 'videos' : 'tracks'}/${id}/playbackinfo`, { @@ -11,6 +11,4 @@ function getPlaybackInfo(id, type = 'track', quality = 'HI_RES_LOSSLESS', immers assetpresentation: assetPresentation } }).then(res => res.json); -} - -module.exports = getPlaybackInfo; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/getPlaylist.js b/src/utils/getPlaylist.ts similarity index 59% rename from src/utils/getPlaylist.js rename to src/utils/getPlaylist.ts index bc661d4..a3495ee 100644 --- a/src/utils/getPlaylist.js +++ b/src/utils/getPlaylist.ts @@ -1,17 +1,18 @@ -const tidalApi = require('./tidalApi'); -const parsePlaylist = require('./parsePlaylist'); +import tidalApi from './tidalApi'; -async function getPlaylist(playlistUuid) { - const playlist = await tidalApi('privatev2', `/user-playlists/${playlistUuid}`).then(res => res.json); +import parsePlaylist from './parsePlaylist'; - const items = []; - await (async function getItems(offset = 0, limit = 50) { +export default async function getPlaylist(playlistUuid: string) { + const playlist = await tidalApi('privatev2', `/user-playlists/${playlistUuid}`).then((res: any) => res.json); + + const items: any = []; + await (async function getItems(offset = 0, limit = 50): Promise { return tidalApi('privatev1', `/playlists/${playlistUuid}/items`, { query: { offset, limit } - }).then(({ json }) => { + }).then(({ json }: any) => { items.push(...json.items); const nextOffset = json.offset + json.limit; if (nextOffset < json.totalNumberOfItems) return getItems(nextOffset); @@ -21,6 +22,4 @@ async function getPlaylist(playlistUuid) { return parsePlaylist(playlist.playlist, { items }); -} - -module.exports = getPlaylist; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/getToken.js b/src/utils/getToken.ts similarity index 55% rename from src/utils/getToken.js rename to src/utils/getToken.ts index a614035..745300d 100644 --- a/src/utils/getToken.js +++ b/src/utils/getToken.ts @@ -1,36 +1,46 @@ -const { config } = require('../globals'); +import { config } from '../globals'; -function getToken(grantType, params = { }) { - const headers = { }; +export default async function getToken(grantType: string, params: { + clientId?: string; + clientSecret?: string; + code?: string; + redirectUri?: string; + codeVerifier?: string; + refreshToken?: string; + deviceCode?: string; + scope?: string[]; + clientUniqueKey?: string; +} = { }) { + const headers: {[key: string]: any} = { }; const body = new URLSearchParams(); if (grantType === 'client_credentials') { // client_credentials body.append('grant_type', 'client_credentials'); - headers['Authorization'] = Buffer.from(`${params.clientId}:${params.clientSecret}`, 'base64'); + headers['Authorization'] = Buffer.from(`${params.clientId!}:${params.clientSecret!}`, 'base64'); } else if (grantType === 'authorization_code') { // authorization_code body.append('grant_type', 'authorization_code'); - body.append('client_id', params.clientId); - body.append('code', params.code); - body.append('redirect_uri', params.redirectUri); - body.append('code_verifier', params.codeVerifier); + body.append('client_id', params.clientId!); + body.append('code', params.code!); + body.append('redirect_uri', params.redirectUri!); + body.append('code_verifier', params.codeVerifier!); } else if (grantType === 'refresh_token') { // refresh_token body.append('grant_type', 'refresh_token'); - body.append('refresh_token', params.refreshToken); + body.append('refresh_token', params.refreshToken!); // Specific to user tokens if (params.clientId) body.append('client_id', params.clientId); if (params.clientSecret) body.append('client_secret', params.clientSecret); } else if (grantType === 'urn:ietf:params:oauth:grant-type:device_code') { // urn:ietf:params:oauth:grant-type:device_code - body.append('client_id', params.clientId); - body.append('client_secret', params.clientSecret); - body.append('device_code', params.deviceCode); + body.append('client_id', params.clientId!); + body.append('client_secret', params.clientSecret!); + body.append('device_code', params.deviceCode!); body.append('grant_type', 'urn:ietf:params:oauth:grant-type:device_code'); - body.append('scope', params.scope.join(' ')); - body.append('client_unique_key', params.clientUniqueKey); // Not needed + body.append('scope', params.scope!.join(' ')); + body.append('client_unique_key', params.clientUniqueKey!); // Not needed } else throw new Error(`Unknown grant type "${grantType}"`); return fetch(`${config.authApiBaseUrl}/oauth2/token`, { @@ -43,6 +53,4 @@ function getToken(grantType, params = { }) { if (res.status === 200) return json; throw json; }); -} - -module.exports = getToken; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/getTrack.js b/src/utils/getTrack.js deleted file mode 100644 index dec11bb..0000000 --- a/src/utils/getTrack.js +++ /dev/null @@ -1,9 +0,0 @@ -const tidalApi = require('./tidalApi'); - -function getTrack(trackId) { - const parseTrack = require('./parseTrack'); - - return tidalApi('privatev1', `/tracks/${trackId}`).then(({ json }) => parseTrack(json)); -} - -module.exports = getTrack; \ No newline at end of file diff --git a/src/utils/getTrack.ts b/src/utils/getTrack.ts new file mode 100644 index 0000000..aaf3e69 --- /dev/null +++ b/src/utils/getTrack.ts @@ -0,0 +1,7 @@ +import tidalApi from './tidalApi'; + +import parseTrack from './parseTrack'; + +export default async function getTrack(trackId: number) { + return tidalApi('privatev1', `/tracks/${trackId}`).then(({ json }) => parseTrack(json)); +} \ No newline at end of file diff --git a/src/utils/getVideo.js b/src/utils/getVideo.js deleted file mode 100644 index 3533b17..0000000 --- a/src/utils/getVideo.js +++ /dev/null @@ -1,9 +0,0 @@ -const tidalApi = require('./tidalApi'); - -function getVideo(videoId) { - const parseVideo = require('./parseVideo'); - - return tidalApi('privatev1', `/videos/${videoId}`).then(({ json }) => parseVideo(json)); -} - -module.exports = getVideo; \ No newline at end of file diff --git a/src/utils/getVideo.ts b/src/utils/getVideo.ts new file mode 100644 index 0000000..10f3293 --- /dev/null +++ b/src/utils/getVideo.ts @@ -0,0 +1,7 @@ +import tidalApi from'./tidalApi'; + +import parseVideo from './parseVideo'; + +export default async function getVideo(videoId: number) { + return tidalApi('privatev1', `/videos/${videoId}`).then(({ json }) => parseVideo(json)); +} \ No newline at end of file diff --git a/src/utils/normalizeTag.js b/src/utils/normalizeTag.ts similarity index 64% rename from src/utils/normalizeTag.js rename to src/utils/normalizeTag.ts index 152e41d..05f5ad1 100644 --- a/src/utils/normalizeTag.js +++ b/src/utils/normalizeTag.ts @@ -1,10 +1,8 @@ -function normalizeTag(value, separator) { +export default function normalizeTag(value: string | string[], separator?: string) { if (value instanceof Array) { if (separator) return value.join(separator); return value[0]; } else { return value; }; -} - -module.exports = normalizeTag; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/parseAlbum.js b/src/utils/parseAlbum.ts similarity index 66% rename from src/utils/parseAlbum.js rename to src/utils/parseAlbum.ts index 1288d59..64cea12 100644 --- a/src/utils/parseAlbum.js +++ b/src/utils/parseAlbum.ts @@ -1,19 +1,20 @@ -const stripMarkup = require('./stripMarkup'); +import stripMarkup from './stripMarkup'; -const { config, tidalAlbumCoverSizes } = require('../globals'); +import { config, tidalAlbumCoverSizes } from '../globals'; +import { Album } from '../types'; -function parseAlbum(album, additional = { }) { - const parseTrack = require('./parseTrack'); - const parseArtist = require('./parseArtist'); - const parseCredits = require('./parseCredits'); +import parseTrack from './parseTrack'; +import parseArtist from './parseArtist'; +import parseCredits from './parseCredits'; +export default function parseAlbum(album: any, additional: any = { }): Album { return { id: album.id, title: album.title, version: album.version, // NOTE: title seems to already include version, unlike track title description: additional?.description, type: album.type, - duration: album.duration, + duration: album.duration * 1000, upload: album.upload, trackCount: album.numberOfTracks, volumeCount: album.numberOfVolumes, @@ -23,22 +24,18 @@ function parseAlbum(album, additional = { }) { upc: album.upc, covers: album.cover && Object.fromEntries(Object.entries(tidalAlbumCoverSizes).map(([name, size]) => [name, `${config.resourcesBaseUrl}/images/${album.cover.replace(/-/g, '/')}/${size}.jpg`])) || undefined, videoCovers: album.videoCover && Object.fromEntries(Object.entries(tidalAlbumCoverSizes).map(([name, size]) => [name, `${config.resourcesBaseUrl}/videos/${album.cover.replace(/-/g, '/')}/${size}.mp4`])) || undefined, - // cover: album.cover && `${config.resourcesBaseUrl}/images/${album.cover.replace(/-/g, '/')}/origin.jpg` || undefined, - // videoCover: album.videoCover && `${config.resourcesBaseUrl}/videos/${album.videoCover.replace(/-/g, '/')}/origin.mp4` || undefined, quality: album.audioQuality, modes: album.audioModes, qualityTypes: album.mediaMetadata?.tags, - credits: additional?.credits?.items ? parseCredits(additional.credits.items) : null, - trackCredits: additional?.trackCredits ? additional.trackCredits.map(i => ({ track: i.item, credits: parseCredits(i.credits) })) : null, + credits: additional?.credits?.items ? parseCredits(additional.credits.items) : undefined, + trackCredits: additional?.trackCredits ? additional.trackCredits.map((i: any) => ({ track: i.item, credits: parseCredits(i.credits) })) : undefined, review: additional?.review?.text ? { originalText: additional.review.text, text: stripMarkup(additional.review.text), source: additional.review.source - } : null, + } : undefined, url: album.url, artists: album.artists?.map(parseArtist), tracks: additional?.tracks?.map(parseTrack), }; -} - -module.exports = parseAlbum; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/parseArtist.js b/src/utils/parseArtist.ts similarity index 54% rename from src/utils/parseArtist.js rename to src/utils/parseArtist.ts index 3452897..f4de48a 100644 --- a/src/utils/parseArtist.js +++ b/src/utils/parseArtist.ts @@ -1,10 +1,11 @@ -const stripMarkup = require('./stripMarkup'); +import stripMarkup from './stripMarkup'; -const { config, tidalArtistPictureSizes } = require('../globals'); +import { config, tidalArtistPictureSizes } from '../globals'; +import { Artist } from '../types'; -function parseArtist(artist, additional = { }) { - const parseAlbum = require('./parseAlbum'); +import parseAlbum from './parseAlbum'; +export default function parseArtist(artist: any, additional: any = { }): Artist { return { id: artist.id, name: artist.name, @@ -12,16 +13,13 @@ function parseArtist(artist, additional = { }) { originalText: additional.biography.text, text: stripMarkup(additional.biography.text), source: additional.biography.source - } : null, + } : undefined, pictures: artist.picture && Object.fromEntries(Object.entries(tidalArtistPictureSizes).map(([name, size]) => [name, `${config.resourcesBaseUrl}/images/${artist.picture.replace(/-/g, '/')}/${size}.jpg`])) || undefined, - // picture: artist.picture && `${config.resourcesBaseUrl}/images/${artist.picture.replace(/-/g, '/')}/origin.jpg` || undefined, types: artist.artistTypes, - roles: artist.artistRoles?.map(role => ({ + roles: artist.artistRoles?.map((role: any) => ({ id: role.categoryId, name: role.category })), - albums: additional?.albums?.map(album => parseAlbum(album)) + albums: additional?.albums?.map((album: any) => parseAlbum(album)) }; -} - -module.exports = parseArtist; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/parseConfig.js b/src/utils/parseConfig.ts similarity index 64% rename from src/utils/parseConfig.js rename to src/utils/parseConfig.ts index bc56a6d..e21587a 100644 --- a/src/utils/parseConfig.js +++ b/src/utils/parseConfig.ts @@ -1,9 +1,69 @@ -const fs = require('fs'); +import fs from 'fs'; -const defaultConfig = require('../default.config.json'); +import defaultConfig from '../default.config.json'; -function parseConfig(configPath) { - const jsonConfig = JSON.parse(fs.readFileSync(configPath)); +export type TypeOptions = { + directory: string; + filename: string; + coverFilename: string | null; +} + +export type Config = { + _version: number; + defaultTypeOptions: TypeOptions; + typeOptions: { + album?: TypeOptions; + video?: TypeOptions; + playlist?: TypeOptions; + mix?: TypeOptions; + }; + trackQuality: string; + videoQuality: string; + playlistCoverFilename: string; + playlistFileFilename: string; + mixCoverFilename: string; + mixFileFilename: string; + useDolbyAtmos: boolean; + getLyrics: boolean; + syncedLyricsOnly: boolean; + plainLyricsOnly: boolean; + externalLyrics: boolean; + embedMetadata: boolean; + artistTagSeparator: string; + roleTagSeparator: string; + useArtistsTag: boolean; + allowUserUploads: boolean; + forcePartialData: boolean; + partialDataFallback: boolean; + getCover: boolean; + trackCoverSize: string; + videoCoverSize: string; + playlistCoverSize: string; + mixCoverSize: string; + customMetadata: [string, string][]; + metadataEmbedder: string; + downloadLogPadding: number; + overwriteExisting: boolean; + segmentWaitMin: number; + segmentWaitMax: number; + + ffmpegPath: string; + kid3CliPath: string; + secretsPath: string; + + debug: boolean; + clientId: string; + clientSecret: string; + scope: string[]; + openApiV2BaseUrl: string; + authApiBaseUrl: string; + privateApiV1BaseUrl: string; + privateApiV2BaseUrl: string; + resourcesBaseUrl: string; +}; + +export default function parseConfig(configPath: string): Config { + const jsonConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); let version = jsonConfig._version; let shouldUpdate = false; @@ -107,6 +167,4 @@ function parseConfig(configPath) { if (shouldUpdate) fs.writeFileSync(configPath, JSON.stringify(config, null, 4)); return config; -} - -module.exports = parseConfig; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/parseCredits.js b/src/utils/parseCredits.js deleted file mode 100644 index 24265c0..0000000 --- a/src/utils/parseCredits.js +++ /dev/null @@ -1,15 +0,0 @@ -const { logger, tidalCredits } = require('../globals'); - -function parseCredits(credits) { - return credits.map(rawCredit => { - const credit = tidalCredits.find(i => i.type.toLowerCase() === rawCredit.type.toLowerCase()); - if (!credit) return logger.debug(`Got unknown credit type "${rawCredit.type}", contributors: ${rawCredit.contributors.map(i => i.name).join(', ')}`); - return { - type: credit.type, - tagName: credit.tagName, - contributors: rawCredit.contributors - } - }).filter(i => i); -} - -module.exports = parseCredits; \ No newline at end of file diff --git a/src/utils/parseCredits.ts b/src/utils/parseCredits.ts new file mode 100644 index 0000000..7073573 --- /dev/null +++ b/src/utils/parseCredits.ts @@ -0,0 +1,21 @@ +import { logger, tidalCredits } from '../globals'; +import { Credit } from '../types'; + +export default function parseCredits(credits: { + type: string; + contributors: { + name: string; + id: number; + }[]; +}[]) { + return credits.map(rawCredit => { + const credit = tidalCredits.find(i => i.type.toLowerCase() === rawCredit.type.toLowerCase()); + if (!credit) return logger.log(`Got unknown credit type "${rawCredit.type}", contributors: ${rawCredit.contributors.map(i => i.name).join(', ')}`, 'debug'); + + return { + type: credit.type, + tagName: credit.tagName, + contributors: rawCredit.contributors + }; + }).filter(i => i) as Credit[]; +} \ No newline at end of file diff --git a/src/utils/parseManifest.js b/src/utils/parseManifest.ts similarity index 64% rename from src/utils/parseManifest.js rename to src/utils/parseManifest.ts index fe554a5..cca6939 100644 --- a/src/utils/parseManifest.js +++ b/src/utils/parseManifest.ts @@ -1,29 +1,31 @@ -async function parseManifest(manifest, manifestType) { +export default async function parseManifest(manifest: string, manifestType: string) { if (manifestType === 'application/dash+xml') { - const parsedManifest = { - contentType: null, - mimeType: null, - segmentAlignment: null, - codec: null, - bandwidth: null, - audioSamplingRate: null, - timescale: null, - initialization: null, - media: null, - startNumber: null, - + const parsedManifest: { + contentType?: string; + mimeType?: string; + segmentAlignment?: string; + codec?: string; + bandwidth?: string; + audioSamplingRate?: number; + timescale?: string; + initialization?: string; + media?: string; + startNumber?: number; + segments: string[]; + } = { + segments: [] }; // TODO: a little less jank perhaps parsedManifest.codec = manifest.match(/(?:<|\s)codecs="(.*?)"/)?.[1]; - parsedManifest.audioSamplingRate = parseInt(manifest.match(/(?:<|\s)audioSamplingRate="(.*?)"/)?.[1]); + parsedManifest.audioSamplingRate = parseInt(manifest.match(/(?:<|\s)audioSamplingRate="(.*?)"/)?.[1] as any); parsedManifest.initialization = manifest.match(/(?:<|\s)initialization="(.*?)"/)?.[1]; parsedManifest.media = manifest.match(/(?:<|\s)media="(.*?)"/)?.[1]; - parsedManifest.startNumber = parseInt(manifest.match(/(?:<|\s)startNumber="(.*?)"/)?.[1]); + parsedManifest.startNumber = parseInt(manifest.match(/(?:<|\s)startNumber="(.*?)"/)?.[1] as any); - for (let segmentIndex = 0; segmentIndex < parseInt(manifest.match(/(?:<|\s)r="(.*?)"/)?.[1]) + 3; segmentIndex++) { - parsedManifest.segments.push(parsedManifest.media.replace(/\$Number\$/, segmentIndex)) + for (let segmentIndex = 0; segmentIndex < parseInt(manifest.match(/(?:<|\s)r="(.*?)"/)?.[1] as any) + 3; segmentIndex++) { + parsedManifest.segments.push(parsedManifest.media!.replace(/\$Number\$/, segmentIndex.toString())); } return parsedManifest; @@ -46,8 +48,8 @@ async function parseManifest(manifest, manifestType) { })); for (const segmentManifest of segmentManifests) { - segmentManifest.raw = await fetch(segmentManifest.url).then(i => i.text()); - segmentManifest.segments = Array.from(segmentManifest.raw.matchAll(/#EXTINF:(.*?),\n(.*?)\n/g)).map(i => i[2]); + segmentManifest.raw = await fetch(segmentManifest.url).then(i => i.text()) as any; + segmentManifest.segments = Array.from((segmentManifest.raw! as string).matchAll(/#EXTINF:(.*?),\n(.*?)\n/g)).map(i => i[2]) as any; } mainManifests.push({ @@ -72,25 +74,25 @@ async function parseManifest(manifest, manifestType) { segments: manifestJson.urls } } else if (manifestType === 'application/vnd.apple.mpegurl') { - const parsedManifest = { - bandwidth: null, - averageBandwidth: null, - codec: null, - raw: null, + const parsedManifest: { + bandwidth?: number; + averageBandwidth?: number; + codec?: string; + raw?: string; + segments: string[]; + } = { segments: [] }; - parsedManifest.bandwidth = parseInt(manifest.match(/BANDWIDTH=\d+/))?.[1]; - parsedManifest.averageBandwidth = parseInt(manifest.match(/AVERAGE-BANDWIDTH=\d+/))?.[1]; + parsedManifest.bandwidth = parseInt(manifest.match(/BANDWIDTH=\d+/)?.[1] as any); + parsedManifest.averageBandwidth = parseInt(manifest.match(/AVERAGE-BANDWIDTH=\d+/)?.[1] as any); parsedManifest.codec = manifest.match(/CODECS="(.*?)"/)?.[1]?.toLowerCase(); - parsedManifest.raw = Buffer.from(manifest.match(/data:application\/vnd\.apple\.mpegurl;base64,(.*)/)?.[1], 'base64').toString(); - parsedManifest.segments = [parsedManifest.raw.match(/#EXT-X-MAP:URI="(.*?)"/)[1], ...Array.from(parsedManifest.raw.matchAll(/#EXTINF:(.*?),\n(.*?)\n/g)).map(i => i[2])]; + parsedManifest.raw = Buffer.from(manifest.match(/data:application\/vnd\.apple\.mpegurl;base64,(.*)/)?.[1] as any, 'base64').toString(); + parsedManifest.segments = [parsedManifest.raw.match(/#EXT-X-MAP:URI="(.*?)"/)?.[1] as any, ...Array.from(parsedManifest.raw.matchAll(/#EXTINF:(.*?),\n(.*?)\n/g)).map(i => i[2])]; return parsedManifest; } else { throw new Error(`Unknown manifest MIME type "${manifestType}"`); } -} - -module.exports = parseManifest; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/parseMix.js b/src/utils/parseMix.js deleted file mode 100644 index 68a62e2..0000000 --- a/src/utils/parseMix.js +++ /dev/null @@ -1,16 +0,0 @@ -const parseTrack = require("./parseTrack"); - -function parseMix(mix, additional = { }) { - return { - id: mix.id, - title: mix.title, - subTitle: mix.subTitle, - shortSubTitle: mix.shortSubtitle, - description: mix.description, - images: Object.fromEntries(Object.entries(mix.images).map(([key, value]) => [key, value.url])), - detailImages: Object.fromEntries(Object.entries(mix.detailImages).map(([key, value]) => [key, value.url])), - tracks: additional?.items.map(item => parseTrack(item)) - }; -} - -module.exports = parseMix; \ No newline at end of file diff --git a/src/utils/parseMix.ts b/src/utils/parseMix.ts new file mode 100644 index 0000000..1b1c944 --- /dev/null +++ b/src/utils/parseMix.ts @@ -0,0 +1,16 @@ +import { Mix } from '../types'; + +import parseTrack from './parseTrack'; + +export default function parseMix(mix: any, additional: any = { }): Mix { + return { + id: mix.id, + title: mix.title, + subTitle: mix.subTitle, + shortSubTitle: mix.shortSubtitle, + description: mix.description, + images: Object.fromEntries(Object.entries(mix.images).map(([key, value]: any) => [key, value.url])), + detailImages: Object.fromEntries(Object.entries(mix.detailImages).map(([key, value]: any) => [key, value.url])), + tracks: additional?.items.map((item: any) => parseTrack(item)) + }; +} \ No newline at end of file diff --git a/src/utils/parsePlaylist.js b/src/utils/parsePlaylist.ts similarity index 55% rename from src/utils/parsePlaylist.js rename to src/utils/parsePlaylist.ts index 8e7f9cb..0f057b8 100644 --- a/src/utils/parsePlaylist.js +++ b/src/utils/parsePlaylist.ts @@ -1,29 +1,26 @@ -const { config, tidalPlaylistImageSizes } = require ('../globals'); +import { config, tidalPlaylistImageSizes } from '../globals'; +import { Playlist } from '../types'; -const parseTrack = require('./parseTrack'); -const parseVideo = require('./parseVideo'); +import parseTrack from './parseTrack'; +import parseVideo from './parseVideo'; -function parsePlaylist(playlist, additional = { }) { +export default function parsePlaylist(playlist: any, additional: any = { }): Playlist { return { uuid: playlist.uuid, title: playlist.title, description: playlist.description, - duration: playlist.duration, + duration: playlist.duration * 1000, images: playlist.squareImage && Object.fromEntries(Object.entries(tidalPlaylistImageSizes).map(([name, size]) => [name, `${config.resourcesBaseUrl}/images/${playlist.squareImage.replace(/-/g, '/')}/${size}.jpg`])) || undefined, - // image: playlist.squareImage && `${config.resourcesBaseUrl}/images/${playlist.squareImage.replace(/-/g, '/')}/origin.jpg` || undefined, customImage: playlist.customImageUrl, // not used even with custom images? - // trackCount: playlist.numberOfTracks, sharing: playlist.sharingLevel, created: playlist.created, lastUpdated: playlist.lastUpdated, - items: additional?.items?.map(({ type, item }) => ({ + items: additional?.items?.map(({ type, item }: any) => ({ type, item: type === 'track'? parseTrack(item) : type === 'video' ? parseVideo(item) : null - })).filter(({ item }) => item !== null) + })).filter(({ item }: any) => item !== null) }; -} - -module.exports = parsePlaylist; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/parseTrack.js b/src/utils/parseTrack.ts similarity index 79% rename from src/utils/parseTrack.js rename to src/utils/parseTrack.ts index a75f4b9..87a5ad2 100644 --- a/src/utils/parseTrack.js +++ b/src/utils/parseTrack.ts @@ -1,13 +1,15 @@ -function parseTrack(track) { - const parseArtist = require('./parseArtist'); - const parseAlbum = require('./parseAlbum'); +import { Track } from '../types'; +import parseArtist from './parseArtist'; +import parseAlbum from './parseAlbum'; + +export default function parseTrack(track: any): Track { return { id: track.id, title: track.title, fullTitle: `${track.title}${track.version ? ` (${track.version})` : ''}`, version: track.version, - duration: track.duration, + duration: track.duration * 1000, upload: track.upload, copyright: track.copyright, explicit: track.explicit, @@ -27,6 +29,4 @@ function parseTrack(track) { artists: track.artists?.map(parseArtist), album: track.album && parseAlbum(track.album) || undefined }; -} - -module.exports = parseTrack; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/parseVideo.js b/src/utils/parseVideo.ts similarity index 71% rename from src/utils/parseVideo.js rename to src/utils/parseVideo.ts index f130433..e1528f7 100644 --- a/src/utils/parseVideo.js +++ b/src/utils/parseVideo.ts @@ -1,14 +1,15 @@ -const { config, tidalVideoCoverSizes } = require('../globals'); +import { config, tidalVideoCoverSizes } from '../globals'; +import { Video } from '../types'; -// TODO: confirm album is always null -function parseVideo(video) { - const parseArtist = require('./parseArtist'); +import parseArtist from './parseArtist'; +// TODO: confirm album is always null +export default function parseVideo(video: any): Video { return { id: video.id, title: video.title, type: video.type, - duration: video.duration, + duration: video.duration * 1000, releaseDate: video.releaseDate, explicit: video.explicit, quality: video.quality, @@ -17,6 +18,4 @@ function parseVideo(video) { volumeNumber: video.volumeNumber, artists: video.artists?.map(parseArtist), }; -} - -module.exports = parseVideo; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/requestDeviceAuthorization.js b/src/utils/requestDeviceAuthorization.ts similarity index 74% rename from src/utils/requestDeviceAuthorization.js rename to src/utils/requestDeviceAuthorization.ts index 33f14c7..7a131a6 100644 --- a/src/utils/requestDeviceAuthorization.js +++ b/src/utils/requestDeviceAuthorization.ts @@ -1,6 +1,6 @@ -const { config } = require('../globals'); +import { config } from '../globals'; -function requestDeviceAuthorization(clientId, scope) { +export default async function requestDeviceAuthorization(clientId: string, scope: string[]) { return fetch(`${config.authApiBaseUrl}/oauth2/device_authorization`, { method: 'POST', body: new URLSearchParams({ @@ -14,6 +14,4 @@ function requestDeviceAuthorization(clientId, scope) { if (res.status === 200) return json; throw json; }); -} - -module.exports = requestDeviceAuthorization; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/search.js b/src/utils/search.ts similarity index 54% rename from src/utils/search.js rename to src/utils/search.ts index f082b1c..350ad15 100644 --- a/src/utils/search.js +++ b/src/utils/search.ts @@ -1,12 +1,26 @@ -const tidalApi = require('./tidalApi'); +import tidalApi from './tidalApi'; +import parseTrack from './parseTrack'; +import parseAlbum from './parseAlbum'; +import parseArtist from './parseArtist'; +import parsePlaylist from './parsePlaylist'; +import parseVideo from './parseVideo'; -const parseTrack = require('./parseTrack'); -const parseAlbum = require('./parseAlbum'); -const parseArtist = require('./parseArtist'); -const parsePlaylist = require('./parsePlaylist'); -const parseVideo = require('./parseVideo'); +import { Album, Artist, Playlist, Track, Video } from '../types'; -function search(query, limit = 20) { +export default async function search(query: string, limit = 20): Promise<{ + topResults: ( + { type: 'track', value: Track } | + { type: 'album', value: Album } | + { type: 'video', value: Video } | + { type: 'artist', value: Artist } | + { type: 'playlist', value: Playlist } + )[]; + tracks: Track[]; + albums: Album[]; + videos: Video[]; + artists: Artist[]; + playlists: Playlist[]; +}> { return tidalApi('privatev2', '/search/', { query: { limit, @@ -14,13 +28,13 @@ function search(query, limit = 20) { } }).then(({ json }) => { return { - topResults: json.topHits.map(({ type, value }) => { + topResults: json.topHits.map(({ type, value }: any) => { if (type === 'TRACKS') return { type: 'track', value: parseTrack(value) }; if (type === 'ALBUMS') return { type: 'album', value: parseAlbum(value) }; if (type === 'VIDEOS') return { type: 'video', value: parseVideo(value) }; if (type === 'ARTISTS') return { type: 'artist', value: parseArtist(value) }; if (type === 'PLAYLISTS') return { type: 'playlist', value: parsePlaylist(value) }; - }).filter(i => i), + }).filter((i: any) => i), tracks: json.tracks.items.map(parseTrack), albums: json.albums.items.map(parseAlbum), videos: json.videos.items.map(parseVideo), @@ -30,6 +44,4 @@ function search(query, limit = 20) { // users: json.userProfiles.items, } }); -} - -module.exports = search; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/spawn.js b/src/utils/spawn.ts similarity index 57% rename from src/utils/spawn.js rename to src/utils/spawn.ts index ac92aed..b4f8716 100644 --- a/src/utils/spawn.js +++ b/src/utils/spawn.ts @@ -1,11 +1,15 @@ -const child_process = require('child_process'); -const path = require('path'); +import child_process from 'child_process'; +import path from 'path'; -const { execDir, logger } = require('../globals'); +import { execDir, logger } from '../globals'; -function spawn(command, args) { +export default function spawn(command: string, args: string[]): Promise<{ + code: number; + stdout: Uint8Array; + stderr: Uint8Array; +}> { return new Promise((resolve, reject) => { - logger.debug(`Spawning '${command}', args: ${args.join(', ')}`); + logger.log(`Spawning '${command}', args: ${args.join(', ')}`, 'debug'); const spawnedProcess = child_process.spawn(command, args, { env: { @@ -14,18 +18,16 @@ function spawn(command, args) { } }); - const stdoutChunks = []; - const stderrChunks = []; + const stdoutChunks: any = []; + const stderrChunks: any = []; spawnedProcess.stdout.on('data', chunk => stdoutChunks.push(chunk)); spawnedProcess.stderr.on('data', chunk => stderrChunks.push(chunk)); spawnedProcess.on('error', err => reject(err)); - spawnedProcess.on('exit', code => resolve({ + spawnedProcess.on('exit', (code: number) => resolve({ code, stdout: Buffer.concat(stdoutChunks), stderr: Buffer.concat(stderrChunks), })); }); -} - -module.exports = spawn; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/stripMarkup.js b/src/utils/stripMarkup.ts similarity index 69% rename from src/utils/stripMarkup.js rename to src/utils/stripMarkup.ts index 1d40891..3938894 100644 --- a/src/utils/stripMarkup.js +++ b/src/utils/stripMarkup.ts @@ -1,7 +1,5 @@ -function stripMarkup(str) { +export default function stripMarkup(str: string) { return str .replace(/\[wimpLink.*?\](.*?)\[\/wimpLink\]/g, (match, content) => content) .replace(//g, '\n'); -} - -module.exports = stripMarkup; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/tidalApi.js b/src/utils/tidalApi.ts similarity index 65% rename from src/utils/tidalApi.js rename to src/utils/tidalApi.ts index 6f68c6b..664a929 100644 --- a/src/utils/tidalApi.js +++ b/src/utils/tidalApi.ts @@ -1,7 +1,16 @@ -const { config, secrets, logger } = require('../globals'); +import { config, secrets, logger } from '../globals'; -function tidalApi(api = 'openv2', path, options = { }) { - const baseUrl = api === 'openv2' ? config.openApiV2BaseUrl : api === 'privatev1' ? config.privateApiV1BaseUrl : api === 'privatev2' ? config.privateApiV2BaseUrl : null; +export default async function tidalApi(api: 'openv2' | 'privatev1' | 'privatev2' = 'openv2', path: string, options: { + query?: object; + method?: string; + headers?: object; + json?: any; +} = { }) { + const baseUrl = + api === 'openv2' ? config.openApiV2BaseUrl : + api === 'privatev1' ? config.privateApiV1BaseUrl : + api === 'privatev2' ? config.privateApiV2BaseUrl : + null; const params = { ...(options.query || {}), locale: 'en_US', @@ -30,15 +39,13 @@ function tidalApi(api = 'openv2', path, options = { }) { let json; try { json = JSON.parse(text) } catch (err) { }; - logger.debug(`API: ${api}, path: ${path}, params: ${urlSearchParams.toString()}, response: ${status}${statusText ? ` ${statusText}` : ''}`); + logger.log(`API: ${api}, path: ${path}, params: ${urlSearchParams.toString()}, response: ${status}${statusText ? ` ${statusText}` : ''}`, 'debug'); return { status, statusText, text, json - } + }; }); -} - -module.exports = tidalApi; \ No newline at end of file +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d4aac2f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "module": "nodenext", + "target": "esnext", + "types": ["node"], + + "noImplicitAny": true, + "strict": true, + // TODO: make gooder + } +}