Refactor dependencies and implement audio decryption logic for enhanced track handling
This commit is contained in:
parent
5325bbc34a
commit
bd8287e935
5 changed files with 148 additions and 111 deletions
26
bun.lock
26
bun.lock
|
@ -4,9 +4,9 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "bun-react-template",
|
"name": "bun-react-template",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@liveinstantly/dash-mpd-parser": "^0.5.0",
|
|
||||||
"@tidal-music/auth": "^1.3.4",
|
"@tidal-music/auth": "^1.3.4",
|
||||||
"@types/fluent-ffmpeg": "^2.1.27",
|
"@types/fluent-ffmpeg": "^2.1.27",
|
||||||
|
"dasha": "3.1.5",
|
||||||
"ffmpeg-static": "^5.2.0",
|
"ffmpeg-static": "^5.2.0",
|
||||||
"flac-stream-tagger": "^1.0.10",
|
"flac-stream-tagger": "^1.0.10",
|
||||||
"fluent-ffmpeg": "^2.1.3",
|
"fluent-ffmpeg": "^2.1.3",
|
||||||
|
@ -17,9 +17,9 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@derhuerst/http-basic": ["@derhuerst/http-basic@8.2.4", "", { "dependencies": { "caseless": "^0.12.0", "concat-stream": "^2.0.0", "http-response-object": "^3.0.1", "parse-cache-control": "^1.0.1" } }, "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw=="],
|
"@babel/runtime": ["@babel/runtime@7.27.1", "", {}, "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="],
|
||||||
|
|
||||||
"@liveinstantly/dash-mpd-parser": ["@liveinstantly/dash-mpd-parser@0.5.0", "", { "dependencies": { "xmldom": "^0.6.0" } }, "sha512-ritwVw23l0lh4jeXLOtRitusbbAfg99Ztdz4rBg0HyGTZKwYC46FO8zotQFnZd9Vd+k8u9s90kwhf6PK91msKw=="],
|
"@derhuerst/http-basic": ["@derhuerst/http-basic@8.2.4", "", { "dependencies": { "caseless": "^0.12.0", "concat-stream": "^2.0.0", "http-response-object": "^3.0.1", "parse-cache-control": "^1.0.1" } }, "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw=="],
|
||||||
|
|
||||||
"@tidal-music/auth": ["@tidal-music/auth@1.3.4", "", { "dependencies": { "@tidal-music/auth": "1.3.4", "@tidal-music/common": "^0.1.5", "@tidal-music/true-time": "^0.3.0" } }, "sha512-eN2tBqsj9L20RpNpxWSwfArIAk2rHK6LH2wtQK4OIN2u4pDJU1WbFdcK0owj3eoEzSgudYORErjPedbvsSrOgA=="],
|
"@tidal-music/auth": ["@tidal-music/auth@1.3.4", "", { "dependencies": { "@tidal-music/auth": "1.3.4", "@tidal-music/common": "^0.1.5", "@tidal-music/true-time": "^0.3.0" } }, "sha512-eN2tBqsj9L20RpNpxWSwfArIAk2rHK6LH2wtQK4OIN2u4pDJU1WbFdcK0owj3eoEzSgudYORErjPedbvsSrOgA=="],
|
||||||
|
|
||||||
|
@ -27,26 +27,32 @@
|
||||||
|
|
||||||
"@tidal-music/true-time": ["@tidal-music/true-time@0.3.0", "", { "dependencies": { "@tidal-music/true-time": "0.3.0" } }, "sha512-nO9DfLKLu5sCI/v6Yp8DSseu8qis0RI1r6l7oIFCusOAedkIbQT/K+ELaaz7cxXR4cboopUZGW55v4RVwxHE/g=="],
|
"@tidal-music/true-time": ["@tidal-music/true-time@0.3.0", "", { "dependencies": { "@tidal-music/true-time": "0.3.0" } }, "sha512-nO9DfLKLu5sCI/v6Yp8DSseu8qis0RI1r6l7oIFCusOAedkIbQT/K+ELaaz7cxXR4cboopUZGW55v4RVwxHE/g=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.2.10", "", { "dependencies": { "bun-types": "1.2.10" } }, "sha512-eilv6WFM3M0c9ztJt7/g80BDusK98z/FrFwseZgT4bXCq2vPhXD4z8R3oddmAn+R/Nmz9vBn4kweJKmGTZj+lg=="],
|
"@types/bun": ["@types/bun@1.2.12", "", { "dependencies": { "bun-types": "1.2.12" } }, "sha512-lY/GQTXDGsolT/TiH72p1tuyUORuRrdV7VwOTOjDOt8uTBJQOJc5zz3ufwwDl0VBaoxotSk4LdP0hhjLJ6ypIQ=="],
|
||||||
|
|
||||||
"@types/fluent-ffmpeg": ["@types/fluent-ffmpeg@2.1.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-QiDWjihpUhriISNoBi2hJBRUUmoj/BMTYcfz+F+ZM9hHWBYABFAE6hjP/TbCZC0GWwlpa3FzvHH9RzFeRusZ7A=="],
|
"@types/fluent-ffmpeg": ["@types/fluent-ffmpeg@2.1.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-QiDWjihpUhriISNoBi2hJBRUUmoj/BMTYcfz+F+ZM9hHWBYABFAE6hjP/TbCZC0GWwlpa3FzvHH9RzFeRusZ7A=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@22.14.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA=="],
|
"@types/node": ["@types/node@22.14.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA=="],
|
||||||
|
|
||||||
|
"@videojs/vhs-utils": ["@videojs/vhs-utils@4.1.1", "", { "dependencies": { "@babel/runtime": "^7.12.5", "global": "^4.4.0" } }, "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA=="],
|
||||||
|
|
||||||
"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||||
|
|
||||||
"async": ["async@0.2.10", "", {}, "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="],
|
"async": ["async@0.2.10", "", {}, "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="],
|
||||||
|
|
||||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.2.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-b5ITZMnVdf3m1gMvJHG+gIfeJHiQPJak0f7925Hxu6ZN5VKA8AGy4GZ4lM+Xkn6jtWxg5S3ldWvfmXdvnkp3GQ=="],
|
"bun-types": ["bun-types@1.2.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-tvWMx5vPqbRXgE8WUZI94iS1xAYs8bkqESR9cxBB1Wi+urvfTrF1uzuDgBHFAdO0+d2lmsbG3HmeKMvUyj6pWA=="],
|
||||||
|
|
||||||
"caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="],
|
"caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="],
|
||||||
|
|
||||||
"concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="],
|
"concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="],
|
||||||
|
|
||||||
|
"dasha": ["dasha@3.1.5", "", { "dependencies": { "m3u8-parser": "^7.2.0" } }, "sha512-Zz/oCfZBPa2g5PlZA7uwfH9B76D0QrX6DYsTFFqBfAuMtB9MBPGOkEkHAbXL9JX7wyU2OiMXYZbMlyUT7uQ9hQ=="],
|
||||||
|
|
||||||
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||||
|
|
||||||
|
"dom-walk": ["dom-walk@0.1.2", "", {}, "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="],
|
||||||
|
|
||||||
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||||
|
|
||||||
"ffmpeg-static": ["ffmpeg-static@5.2.0", "", { "dependencies": { "@derhuerst/http-basic": "^8.2.0", "env-paths": "^2.2.0", "https-proxy-agent": "^5.0.0", "progress": "^2.0.3" } }, "sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA=="],
|
"ffmpeg-static": ["ffmpeg-static@5.2.0", "", { "dependencies": { "@derhuerst/http-basic": "^8.2.0", "env-paths": "^2.2.0", "https-proxy-agent": "^5.0.0", "progress": "^2.0.3" } }, "sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA=="],
|
||||||
|
@ -55,6 +61,8 @@
|
||||||
|
|
||||||
"fluent-ffmpeg": ["fluent-ffmpeg@2.1.3", "", { "dependencies": { "async": "^0.2.9", "which": "^1.1.1" } }, "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q=="],
|
"fluent-ffmpeg": ["fluent-ffmpeg@2.1.3", "", { "dependencies": { "async": "^0.2.9", "which": "^1.1.1" } }, "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q=="],
|
||||||
|
|
||||||
|
"global": ["global@4.4.0", "", { "dependencies": { "min-document": "^2.19.0", "process": "^0.11.10" } }, "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w=="],
|
||||||
|
|
||||||
"http-response-object": ["http-response-object@3.0.2", "", { "dependencies": { "@types/node": "^10.0.3" } }, "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA=="],
|
"http-response-object": ["http-response-object@3.0.2", "", { "dependencies": { "@types/node": "^10.0.3" } }, "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA=="],
|
||||||
|
|
||||||
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
|
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
|
||||||
|
@ -65,10 +73,16 @@
|
||||||
|
|
||||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"m3u8-parser": ["m3u8-parser@7.2.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@videojs/vhs-utils": "^4.1.1", "global": "^4.4.0" } }, "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ=="],
|
||||||
|
|
||||||
|
"min-document": ["min-document@2.19.0", "", { "dependencies": { "dom-walk": "^0.1.0" } }, "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"parse-cache-control": ["parse-cache-control@1.0.1", "", {}, "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="],
|
"parse-cache-control": ["parse-cache-control@1.0.1", "", {}, "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="],
|
||||||
|
|
||||||
|
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
|
||||||
|
|
||||||
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
|
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
|
||||||
|
|
||||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||||
|
@ -85,8 +99,6 @@
|
||||||
|
|
||||||
"which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="],
|
"which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="],
|
||||||
|
|
||||||
"xmldom": ["xmldom@0.6.0", "", {}, "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg=="],
|
|
||||||
|
|
||||||
"http-response-object/@types/node": ["@types/node@10.17.60", "", {}, "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="],
|
"http-response-object/@types/node": ["@types/node@10.17.60", "", {}, "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
24
index.ts
24
index.ts
|
@ -50,7 +50,7 @@ Bun.serve({
|
||||||
},
|
},
|
||||||
development: true
|
development: true
|
||||||
})
|
})
|
||||||
/*
|
|
||||||
const tracks = await utils.fetchTracks();
|
const tracks = await utils.fetchTracks();
|
||||||
|
|
||||||
for await (const track of tracks.items) {
|
for await (const track of tracks.items) {
|
||||||
|
@ -59,21 +59,32 @@ for await (const track of tracks.items) {
|
||||||
|
|
||||||
const trackId = parseInt(id)
|
const trackId = parseInt(id)
|
||||||
|
|
||||||
if (await Bun.file(`downloaded/${trackId}.flac`).exists()) {
|
if (await Bun.file(`downloaded/${trackId}.flac`).exists() || await Bun.file(`downloaded/${trackId}.m4a`).exists()) {
|
||||||
//console.log(`Already downloaded ${trackId}.flac`)
|
//console.log(`Already downloaded ${trackId}.flac`)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackData = await utils.fetchTrack(parseInt(id))
|
const trackData = await utils.fetchTrack(trackId)
|
||||||
if (trackData?.status === 404 || trackData === null) {
|
if (trackData?.status === 404 || trackData === null) {
|
||||||
console.error(`track ${trackId} not available`)
|
console.error(`track ${trackId} not available`)
|
||||||
|
await Bun.sleep(1000) // wait 1 second before next track
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const audio = await utils.tagFlac(trackId, await utils.downloadFlac(trackData.manifestMimeType, trackData.manifest))
|
const audio = await utils.downloadFlac(trackData.manifestMimeType, trackData.manifest)
|
||||||
|
|
||||||
//await Bun.write(`downloaded/${trackId}.flac`, audio)
|
const format = audio.mimeType.split("/")[1];
|
||||||
//await utimes(`downloaded/${trackId}.flac`, createdAt, createdAt)
|
|
||||||
|
const name = `downloaded/${trackId}.${format}`
|
||||||
|
if (audio.mimeType === "audio/flac") {
|
||||||
|
const taggedAudio = await utils.tagFlac(trackId, audio.buffer)
|
||||||
|
|
||||||
|
await Bun.write(name, taggedAudio)
|
||||||
|
} else {
|
||||||
|
await Bun.write(name, audio.buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
await utimes(name, createdAt, createdAt)
|
||||||
//console.log(`Downloaded ${trackId}.flac`)
|
//console.log(`Downloaded ${trackId}.flac`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to download ${trackId}.flac`, e)
|
console.error(`Failed to download ${trackId}.flac`, e)
|
||||||
|
@ -81,4 +92,3 @@ for await (const track of tracks.items) {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Done")
|
console.log("Done")
|
||||||
*/
|
|
|
@ -8,9 +8,9 @@
|
||||||
"start": "bun index.ts"
|
"start": "bun index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@liveinstantly/dash-mpd-parser": "^0.5.0",
|
|
||||||
"@tidal-music/auth": "^1.3.4",
|
"@tidal-music/auth": "^1.3.4",
|
||||||
"@types/fluent-ffmpeg": "^2.1.27",
|
"@types/fluent-ffmpeg": "^2.1.27",
|
||||||
|
"dasha": "3.1.5",
|
||||||
"ffmpeg-static": "^5.2.0",
|
"ffmpeg-static": "^5.2.0",
|
||||||
"flac-stream-tagger": "^1.0.10",
|
"flac-stream-tagger": "^1.0.10",
|
||||||
"fluent-ffmpeg": "^2.1.3"
|
"fluent-ffmpeg": "^2.1.3"
|
||||||
|
|
52
src/helpers/decrypt.ts
Normal file
52
src/helpers/decrypt.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { createDecipheriv } from "crypto";
|
||||||
|
import type { TidalManifest } from "../../Caches/PlaybackInfoTypes";
|
||||||
|
|
||||||
|
// Do not change this
|
||||||
|
const mastKey = "UIlTTEMmmLfGowo/UC60x2H45W6MdGgTRfo/umg4754=";
|
||||||
|
const mastKeyBuffer = Buffer.from(mastKey, "base64");
|
||||||
|
|
||||||
|
type DecryptedKey = {
|
||||||
|
key: Buffer;
|
||||||
|
nonce: Buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const decryptKeyId = (keyId: string): DecryptedKey => {
|
||||||
|
// Decode the base64 strings to buffers
|
||||||
|
const keyIdBuffer = Buffer.from(keyId, "base64");
|
||||||
|
|
||||||
|
// Get the IV from the first 16 bytes of the securityToken
|
||||||
|
const iv = keyIdBuffer.subarray(0, 16);
|
||||||
|
const keyIdEnc = keyIdBuffer.subarray(16);
|
||||||
|
|
||||||
|
// Initialize decryptor
|
||||||
|
const decryptor = createDecipheriv("aes-256-cbc", Uint8Array.from(mastKeyBuffer), Uint8Array.from(iv));
|
||||||
|
|
||||||
|
// Decrypt the security token
|
||||||
|
const keyIdDec = decryptor.update(Uint8Array.from(keyIdEnc));
|
||||||
|
|
||||||
|
// Get the audio stream decryption key and nonce from the decrypted security token
|
||||||
|
const key = keyIdDec.subarray(0, 16);
|
||||||
|
const nonce = keyIdDec.subarray(16, 24);
|
||||||
|
|
||||||
|
return { key, nonce };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extend nonce to 16 bytes (nonce + counter)
|
||||||
|
const makeDecipheriv = ({ key, nonce }: DecryptedKey) => {
|
||||||
|
const iv = new Uint8Array([...nonce, ...new Uint8Array(8)]);
|
||||||
|
return createDecipheriv("aes-128-ctr", Uint8Array.from(key), iv);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const makeDecipher = (manifest: { keyId: string; encryptionType: string }) => {
|
||||||
|
switch (manifest.encryptionType) {
|
||||||
|
case "OLD_AES": {
|
||||||
|
return makeDecipheriv(decryptKeyId(manifest.keyId));
|
||||||
|
}
|
||||||
|
case "NONE": {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unexpected manifest encryption type ${manifest.encryptionType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,63 +1,31 @@
|
||||||
|
import { credentialsProvider } from "@tidal-music/auth";
|
||||||
import { FlacStreamTagger } from "flac-stream-tagger";
|
import { FlacStreamTagger } from "flac-stream-tagger";
|
||||||
import Ffmpeg from "fluent-ffmpeg";
|
import Ffmpeg from "fluent-ffmpeg";
|
||||||
import ffmpegPath from "ffmpeg-static";
|
import ffmpegPath from "ffmpeg-static";
|
||||||
import { PassThrough } from "stream";
|
import { parse } from "dasha";
|
||||||
import { DashMPD } from '@liveinstantly/dash-mpd-parser';
|
|
||||||
|
|
||||||
|
import { makeDecipher } from "./decrypt";
|
||||||
|
|
||||||
Ffmpeg.setFfmpegPath(ffmpegPath as string);
|
Ffmpeg.setFfmpegPath(ffmpegPath as string);
|
||||||
|
|
||||||
export default class {
|
export default class {
|
||||||
authHeaders: { [key: string]: string }
|
private _authHeaders: { [key: string]: string }
|
||||||
|
private _userId: number;
|
||||||
|
|
||||||
constructor(authHeaders: { [key: string]: string }) {
|
constructor(authHeaders: { [key: string]: string }) {
|
||||||
this.authHeaders = authHeaders
|
this._authHeaders = authHeaders
|
||||||
|
this._userId = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async convertAacToFlac(buffer: Buffer<ArrayBuffer> | ArrayBuffer) {
|
async init() {
|
||||||
const inputStream = new PassThrough();
|
const { token } = await credentialsProvider.getCredentials() as { token: string };
|
||||||
inputStream.end(Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer));
|
this._userId = JSON.parse(Buffer.from(token, "base64").toString("utf-8")).uid;
|
||||||
|
|
||||||
const outputChunks: Buffer[] = [];
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
Ffmpeg(inputStream)
|
|
||||||
.format("flac")
|
|
||||||
.on("error", reject)
|
|
||||||
.on("end", resolve)
|
|
||||||
.pipe()
|
|
||||||
.on("data", chunk => outputChunks.push(chunk));
|
|
||||||
});
|
|
||||||
|
|
||||||
return Buffer.concat(outputChunks);;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async convertMp4ToFlac(buffer: Buffer) {
|
|
||||||
const inputStream = new PassThrough();
|
|
||||||
inputStream.end(buffer);
|
|
||||||
|
|
||||||
const outputChunks: Buffer[] = [];
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
Ffmpeg(inputStream)
|
|
||||||
.audioCodec("flac")
|
|
||||||
.format("flac")
|
|
||||||
.on("error", reject)
|
|
||||||
.on("end", () => resolve(Buffer.concat(outputChunks)))
|
|
||||||
.pipe()
|
|
||||||
.on("data", chunk => outputChunks.push(chunk));
|
|
||||||
});
|
|
||||||
|
|
||||||
return Buffer.concat(outputChunks);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchTrack(id: number) {
|
async fetchTrack(id: number) {
|
||||||
const audio = await fetch(`https://desktop.tidal.com/v1/tracks/${id}/playbackinfo?audioquality=HI_RES_LOSSLESS&playbackmode=STREAM&assetpresentation=FULL`, {
|
const audio = await fetch(`https://desktop.tidal.com/v1/tracks/${id}/playbackinfo?audioquality=HI_RES_LOSSLESS&playbackmode=STREAM&assetpresentation=FULL`, {
|
||||||
headers: this.authHeaders
|
headers: this._authHeaders
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(await audio.text())
|
|
||||||
|
|
||||||
return await audio.json() as {
|
return await audio.json() as {
|
||||||
trackId: number,
|
trackId: number,
|
||||||
audioPresentation: string,
|
audioPresentation: string,
|
||||||
|
@ -77,8 +45,8 @@ export default class {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchTracks() {
|
async fetchTracks() {
|
||||||
const tracks = await fetch("https://listen.tidal.com/v1/users/199235629/favorites/tracks?offset=0&limit=10000&order=DATE&orderDirection=DESC&countryCode=US", {
|
const tracks = await fetch(`https://desktop.tidal.com/v1/users/${this._userId}/favorites/tracks?offset=0&limit=50&order=DATE&orderDirection=DESC&countryCode=US&locale=en_US&deviceType=DESKTOP`, {
|
||||||
headers: this.authHeaders
|
headers: this._authHeaders
|
||||||
})
|
})
|
||||||
|
|
||||||
return await tracks.json();
|
return await tracks.json();
|
||||||
|
@ -86,55 +54,46 @@ export default class {
|
||||||
|
|
||||||
async downloadFlac(manifestMimeType: string, manifest: string) {
|
async downloadFlac(manifestMimeType: string, manifest: string) {
|
||||||
if (manifestMimeType === "application/dash+xml") {
|
if (manifestMimeType === "application/dash+xml") {
|
||||||
const mpd = new DashMPD();
|
const parsedManifest = await parse(Buffer.from(manifest, "base64").toString("utf-8"), "https://sp-ad-cf.audio.tidal.com")
|
||||||
mpd.parse(Buffer.from(manifest, "base64").toString("utf-8"))
|
|
||||||
|
|
||||||
const Representation = mpd.getJSON().MPD.Period[0].AdaptationSet[0].Representation[0];
|
|
||||||
const segmentTemplate = Representation.SegmentTemplate;
|
|
||||||
const segmentCount = segmentTemplate.SegmentTimeline.S[0]["@r"];
|
|
||||||
const segmentBuffers = [];
|
const segmentBuffers = [];
|
||||||
|
|
||||||
const initialization = await fetch(segmentTemplate["@initialization"])
|
for await (const track of parsedManifest.tracks.all) {
|
||||||
const initializationBuffer = await initialization.arrayBuffer()
|
for await (const segment of track.segments) {
|
||||||
segmentBuffers.push(Buffer.from(initializationBuffer));
|
const req = await fetch(segment.url)
|
||||||
|
const content = await req.arrayBuffer()
|
||||||
const segmentPromises = [];
|
const contentBuffer = Buffer.from(content)
|
||||||
for (let i = 0; i < 1 + segmentCount; i++) {
|
segmentBuffers.push(contentBuffer)
|
||||||
const segmentUrl = segmentTemplate["@media"].replace("$Number$", segmentTemplate["@startNumber"] + i);
|
}
|
||||||
segmentPromises.push(fetch(segmentUrl).then(segment => segment.arrayBuffer()).then(segmentBuffer => Buffer.from(segmentBuffer)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const segmentBuffersArray = await Promise.all(segmentPromises);
|
return { buffer: Buffer.concat(segmentBuffers), mimeType: "audio/m4a" }
|
||||||
segmentBuffers.push(...segmentBuffersArray);
|
|
||||||
|
|
||||||
const flac = await this.convertMp4ToFlac(Buffer.concat(segmentBuffers))
|
|
||||||
|
|
||||||
return { buffer: flac, mimeType: "audio/flac" }
|
|
||||||
} else if (manifestMimeType === "application/vnd.tidal.bts") {
|
} else if (manifestMimeType === "application/vnd.tidal.bts") {
|
||||||
const data = JSON.parse(Buffer.from(manifest, "base64").toString("utf-8")) as {
|
const data = JSON.parse(Buffer.from(manifest, "base64").toString("utf-8")) as {
|
||||||
"mimeType": string,
|
"mimeType": string,
|
||||||
"codecs": string,
|
"codecs": string,
|
||||||
"encryptionType": string,
|
"encryptionType": string,
|
||||||
|
"keyId": string,
|
||||||
"urls": string[]
|
"urls": string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const decryptor = makeDecipher({ keyId: data.keyId, encryptionType: data.encryptionType });
|
||||||
|
|
||||||
const flac = await fetch(data.urls[0])
|
const flac = await fetch(data.urls[0])
|
||||||
|
|
||||||
return { buffer: await flac.arrayBuffer(), mimeType: data.mimeType }
|
const flacBuffer = await flac.arrayBuffer()
|
||||||
|
|
||||||
|
const decryptedBuffer = decryptor ? Buffer.concat([decryptor.update(Buffer.from(flacBuffer)), decryptor.final()]) : Buffer.from(flacBuffer);
|
||||||
|
|
||||||
|
return { buffer: decryptedBuffer, mimeType: "audio/flac" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { buffer: new ArrayBuffer(0), mimeType: "" } // TODO: Handle other mime types
|
return { buffer: new ArrayBuffer(0), mimeType: "audio/unknown" } // TODO: Handle other mime types
|
||||||
}
|
}
|
||||||
|
|
||||||
async tagFlac(id: number, audioBuffer: { buffer: Buffer<ArrayBuffer> | ArrayBuffer, mimeType: string }) {
|
async tagFlac(id: number, audioBuffer: Buffer<ArrayBuffer> | ArrayBuffer) {
|
||||||
const fileId = Bun.nanoseconds().toString(36);
|
const trackReq = await fetch(`https://desktop.tidal.com/v1/tracks/${id}/?countryCode=US`, {
|
||||||
|
headers: this._authHeaders
|
||||||
if (audioBuffer.mimeType === "audio/mp4") {
|
|
||||||
audioBuffer.buffer = await this.convertAacToFlac(audioBuffer.buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
const trackReq = await fetch(`https://api.tidal.com/v1/tracks/${id}/?countryCode=US`, {
|
|
||||||
headers: this.authHeaders
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const track = await trackReq.json() as {
|
const track = await trackReq.json() as {
|
||||||
|
@ -161,11 +120,11 @@ export default class {
|
||||||
status?: string,
|
status?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const alubmReq = await fetch(`https://api.tidal.com/v1/albums/${track.album.id}/?countryCode=US`, {
|
const albumReq = await fetch(`https://desktop.tidal.com/v1/albums/${track.album.id}/?countryCode=US`, {
|
||||||
headers: this.authHeaders
|
headers: this._authHeaders
|
||||||
})
|
})
|
||||||
|
|
||||||
const album = await alubmReq.json() as {
|
const album = await albumReq.json() as {
|
||||||
title: string,
|
title: string,
|
||||||
numberOfTracks: number,
|
numberOfTracks: number,
|
||||||
releaseDate: string,
|
releaseDate: string,
|
||||||
|
@ -178,28 +137,32 @@ export default class {
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
return FlacStreamTagger.fromBuffer(Buffer.from(Buffer.isBuffer(audioBuffer.buffer) ? audioBuffer.buffer : Buffer.from(audioBuffer.buffer)), {
|
const albumArtReq = await fetch(`https://resources.tidal.com/images/${album.cover.replaceAll("-", "/")}/1280x1280.jpg`)
|
||||||
|
const albumArt = albumArtReq.ok ? await albumArtReq.arrayBuffer() : new ArrayBuffer(0);
|
||||||
|
|
||||||
|
return FlacStreamTagger.fromBuffer(Buffer.from(Buffer.isBuffer(audioBuffer) ? audioBuffer : Buffer.from(audioBuffer)), {
|
||||||
tagMap: {
|
tagMap: {
|
||||||
title: track.title,
|
title: track?.title || "",
|
||||||
trackNumber: track.trackNumber.toString(),
|
trackNumber: (track?.trackNumber || 0).toString(),
|
||||||
discNumber: track.volumeNumber.toString(),
|
discNumber: (track?.volumeNumber || 0).toString(),
|
||||||
bpm: (track.bpm || 0).toString(),
|
bpm: (track?.bpm || 0).toString(),
|
||||||
date: track.streamStartDate,
|
date: track?.streamStartDate || "",
|
||||||
copyright: track.copyright,
|
copyright: track?.copyright || "",
|
||||||
REPLAYGAIN_TRACK_GAIN: track.replayGain.toString(),
|
REPLAYGAIN_TRACK_GAIN: (track?.replayGain || 0).toString(),
|
||||||
REPLAYGAIN_TRACK_PEAK: track.peak.toString(),
|
REPLAYGAIN_TRACK_PEAK: (track?.peak || 0).toString(),
|
||||||
comment: track.url,
|
comment: track?.url || "",
|
||||||
isrc: track.isrc,
|
isrc: track?.isrc || "",
|
||||||
upc: album.upc,
|
upc: album?.upc || "",
|
||||||
artist: track.artists.map((a) => a.name),
|
artist: track?.artists.map((a) => a.name) || [],
|
||||||
album: album.title,
|
album: album?.title || "",
|
||||||
albumArtist: album.artists.map((a) => a.name),
|
albumArtist: album?.artists.map((a) => a.name) || [],
|
||||||
totalTracks: album.numberOfTracks.toString(),
|
totalTracks: (album?.numberOfTracks || 0).toString(),
|
||||||
year: album.releaseDate.split("-")[0] || "",
|
year: album?.releaseDate.split("-")[0] || "",
|
||||||
},
|
},
|
||||||
picture: {
|
picture: {
|
||||||
buffer: Buffer.from(await (await fetch(`https://resources.tidal.com/images/${album.cover.replaceAll("-", "/")}/1280x1280.jpg`)).arrayBuffer()),
|
buffer: Buffer.from(albumArt),
|
||||||
}
|
}
|
||||||
}).toBuffer()
|
}).toBuffer()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue