init
This commit is contained in:
parent
54dfc1d822
commit
67dce9ddc0
9 changed files with 457 additions and 1 deletions
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# dependencies (bun install)
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
|
||||||
|
tmp
|
||||||
|
localstorage
|
||||||
|
downloaded
|
21
README.md
21
README.md
|
@ -1,2 +1,21 @@
|
||||||
# aiot
|
# bun-react-template
|
||||||
|
|
||||||
|
To install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
To start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
To run for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun start
|
||||||
|
```
|
||||||
|
|
||||||
|
This project was created using `bun init` in bun v1.2.9. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
||||||
|
|
129
bun.lock
Normal file
129
bun.lock
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "bun-react-template",
|
||||||
|
"dependencies": {
|
||||||
|
"@tidal-music/auth": "^1.3.4",
|
||||||
|
"bootstrap": "^5.3.5",
|
||||||
|
"flac-stream-tagger": "^1.0.10",
|
||||||
|
"halfmoon": "^2.0.2",
|
||||||
|
"id3js": "^2.1.1",
|
||||||
|
"node-id3": "^0.2.9",
|
||||||
|
"react": "^19",
|
||||||
|
"react-dom": "^19",
|
||||||
|
"sakura.css": "^1.5.0",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="],
|
||||||
|
|
||||||
|
"@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/common": ["@tidal-music/common@0.1.5", "", { "dependencies": { "@tidal-music/common": "0.1.5" } }, "sha512-C71JfsJ5P1CwObGTOlRH9vDjo4mruV5WeE6C0JmR8eVoivIaf1Ou9QCI7pDa7aWuCUjOOm3Dd5p5MkJ3aCDXHA=="],
|
||||||
|
|
||||||
|
"@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.8", "", { "dependencies": { "bun-types": "1.2.7" } }, "sha512-t8L1RvJVUghW5V+M/fL3Thbxcs0HwNsXsnTEBEfEVqGteiJToOlZ/fyOEaR1kZsNqnu+3XA4RI/qmnX4w6+S+w=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@22.14.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.1.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w=="],
|
||||||
|
|
||||||
|
"@types/react-dom": ["@types/react-dom@19.1.1", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-jFf/woGTVTjUJsl2O7hcopJ1r0upqoq/vIOoCj0yLh3RIXxWcljlpuZ+vEBRXsymD1jhfeJrlyTy/S1UW+4y1w=="],
|
||||||
|
|
||||||
|
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||||
|
|
||||||
|
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
|
||||||
|
|
||||||
|
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||||
|
|
||||||
|
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||||
|
|
||||||
|
"bootstrap": ["bootstrap@5.3.5", "", { "peerDependencies": { "@popperjs/core": "^2.11.8" } }, "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA=="],
|
||||||
|
|
||||||
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="],
|
||||||
|
|
||||||
|
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||||
|
|
||||||
|
"clap": ["clap@3.1.1", "", { "dependencies": { "ansi-colors": "^4.1.1" } }, "sha512-vp42956Ax06WwaaheYEqEOgXZ3VKJxgccZ0gJL0HpyiupkIS9RVJFo5eDU1BPeQAOqz+cclndZg4DCqG1sJReQ=="],
|
||||||
|
|
||||||
|
"css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
||||||
|
|
||||||
|
"csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
|
||||||
|
|
||||||
|
"csso-cli": ["csso-cli@4.0.2", "", { "dependencies": { "chokidar": "^3.5.3", "clap": "^3.1.1", "csso": "^5.0.4", "source-map-js": "^1.0.2" }, "bin": { "csso": "bin/csso" } }, "sha512-p/VipA45w8EmS8Lv6wGtE+UdsbFlqUBGhL9FCTGKxd5dC07mtg3BbZaMzMh0X+oIl2JUGR/mPx5YzuNnTM2a3w=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||||
|
|
||||||
|
"flac-stream-tagger": ["flac-stream-tagger@1.0.10", "", { "dependencies": { "imageinfo": "^1.0.4" } }, "sha512-RqXyWnwx5xGTUuTCxzCCKK085sVxZzVCPwe1LShj1bDAn8zglhYcAPBorL4R0pK6+IIJtJsUuief3AawpNwIjw=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
|
"halfmoon": ["halfmoon@2.0.2", "", { "dependencies": { "csso-cli": "^4.0.2", "rtlcss": "^4.3.0" } }, "sha512-9IZ+OccqINrD5Eu5RgEgdK0c3CvDRIkL/ePMx+PFXwK1LZB0fI9HniKbd6MrMRsL0j7/OnvS0nGDgoaZvqeGkg=="],
|
||||||
|
|
||||||
|
"iconv-lite": ["iconv-lite@0.6.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ=="],
|
||||||
|
|
||||||
|
"id3js": ["id3js@2.1.1", "", {}, "sha512-9Gi+sG0RHSa5qn8hkwi2KCl+2jV8YrtiZidXbOO3uLfRAxc2jilRg0fiQ3CbeoAmR7G7ap3RVs1kqUVhIyZaog=="],
|
||||||
|
|
||||||
|
"imageinfo": ["imageinfo@1.0.4", "", {}, "sha512-BJml4q/QCO2187F4UcO/b6hTYIhbq4nnd1XNs65jyCED9em4m6XmeGWDxjewjfJoC7VJABhOdmqb64KA24rLZw=="],
|
||||||
|
|
||||||
|
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
|
||||||
|
|
||||||
|
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||||
|
|
||||||
|
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||||
|
|
||||||
|
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||||
|
|
||||||
|
"mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"node-id3": ["node-id3@0.2.9", "", { "dependencies": { "iconv-lite": "0.6.2" } }, "sha512-dSxhuxrkkGVRgUhDHFxdY0pilzOREcodO01HcZWfaRkCaPWGmo0dOgD8ygyL6ln4Iv4cmfRxAWn1WD9bIB9Bhw=="],
|
||||||
|
|
||||||
|
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||||
|
|
||||||
|
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||||
|
|
||||||
|
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
|
||||||
|
|
||||||
|
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||||
|
|
||||||
|
"rtlcss": ["rtlcss@4.3.0", "", { "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0", "postcss": "^8.4.21", "strip-json-comments": "^3.1.1" }, "bin": { "rtlcss": "bin/rtlcss.js" } }, "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig=="],
|
||||||
|
|
||||||
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"sakura.css": ["sakura.css@1.5.0", "", {}, "sha512-AcAZa9F4SCs2xaKLWcXQxJxKfeod2PN3sR31+R22MKuyoJxNChH1wBG4mQaY9gVpJ3VpNA1XHPOrOM9hFo9cSw=="],
|
||||||
|
|
||||||
|
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||||
|
|
||||||
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
}
|
||||||
|
}
|
2
bunfig.toml
Normal file
2
bunfig.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[serve.static]
|
||||||
|
env = "BUN_PUBLIC_*"
|
54
index.ts
Normal file
54
index.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import Auth from "./src/helpers/auth";
|
||||||
|
import Utils from "./src/helpers/utils";
|
||||||
|
|
||||||
|
const utils = new Utils(await Auth());
|
||||||
|
|
||||||
|
Bun.serve({
|
||||||
|
routes: {
|
||||||
|
"/track": async (req: { url: string }) => {
|
||||||
|
const url = new URL(req.url)
|
||||||
|
|
||||||
|
const id = url.searchParams.get("id")
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return new Response("Missing id", { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackId = parseInt(id)
|
||||||
|
|
||||||
|
const file = Bun.file(`downloaded/${trackId}.flac`)
|
||||||
|
|
||||||
|
if (await file.exists()) {
|
||||||
|
return new Response(file, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "audio/flac",
|
||||||
|
"Content-Disposition": `attachment; filename="${trackId}.flac"`,
|
||||||
|
"Cache-Control": "public, max-age=31536000",
|
||||||
|
"ETag": trackId.toString(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { manifestMimeType, manifest } = await utils.fetchTrack(parseInt(id))
|
||||||
|
|
||||||
|
const audio = await utils.tagFlac(trackId, await utils.downloadFlac(manifestMimeType, manifest))
|
||||||
|
|
||||||
|
await Bun.write(`downloaded/${trackId}.flac`, audio)
|
||||||
|
|
||||||
|
return Response.json(audio, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "audio/flac",
|
||||||
|
"Content-Disposition": `attachment; filename="${trackId}.flac"`,
|
||||||
|
"Cache-Control": "public, max-age=31536000",
|
||||||
|
"ETag": trackId.toString(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"/tracks": async () => {
|
||||||
|
const tracks = await utils.fetchTracks();
|
||||||
|
|
||||||
|
return Response.json(tracks);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
development: true
|
||||||
|
})
|
27
package.json
Normal file
27
package.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "bun-react-template",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun --hot index.ts",
|
||||||
|
"build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'",
|
||||||
|
"start": "NODE_ENV=production bun index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tidal-music/auth": "^1.3.4",
|
||||||
|
"bootstrap": "^5.3.5",
|
||||||
|
"flac-stream-tagger": "^1.0.10",
|
||||||
|
"halfmoon": "^2.0.2",
|
||||||
|
"id3js": "^2.1.1",
|
||||||
|
"node-id3": "^0.2.9",
|
||||||
|
"react": "^19",
|
||||||
|
"react-dom": "^19",
|
||||||
|
"sakura.css": "^1.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"@types/bun": "latest"
|
||||||
|
}
|
||||||
|
}
|
30
src/helpers/auth.ts
Normal file
30
src/helpers/auth.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import "../localstorage";
|
||||||
|
|
||||||
|
import { init, initializeDeviceLogin, finalizeDeviceLogin, credentialsProvider } from "@tidal-music/auth";
|
||||||
|
|
||||||
|
const clientId = "zU4XHVVkc2tDPo4t";
|
||||||
|
const clientSecret = "VJKhDFqJPqvsPVNBV6ukXTJmwlvbttP7wlMlrc72se4=";
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
await init({
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
credentialsStorageKey: "tidal-credentials",
|
||||||
|
scopes: ["r_usr", "w_usr", "w_sub"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const credentials = await credentialsProvider.getCredentials();
|
||||||
|
|
||||||
|
if (typeof credentials.userId !== "string" || (credentials.expires || 0) < Date.now()) {
|
||||||
|
|
||||||
|
const response = await initializeDeviceLogin();
|
||||||
|
|
||||||
|
console.log(`Please open https://${response.verificationUriComplete} to login.`)
|
||||||
|
|
||||||
|
await finalizeDeviceLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"Authorization": `Bearer ${(await credentialsProvider.getCredentials()).token || ""}`
|
||||||
|
}
|
||||||
|
}
|
140
src/helpers/utils.ts
Normal file
140
src/helpers/utils.ts
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
import { FlacStreamTagger } from "flac-stream-tagger";
|
||||||
|
|
||||||
|
export default class {
|
||||||
|
authHeaders: { [key: string]: string }
|
||||||
|
|
||||||
|
constructor(authHeaders: { [key: string]: string }) {
|
||||||
|
this.authHeaders = authHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchTrack(id: number) {
|
||||||
|
const audio = await fetch(`https://api.tidal.com/v1/tracks/${id}/playbackinfopostpaywall/v4?audioquality=HI_RES_LOSSLESS&playbackmode=STREAM&assetpresentation=FULL`, {
|
||||||
|
headers: this.authHeaders
|
||||||
|
})
|
||||||
|
return await audio.json() as {
|
||||||
|
trackId: number,
|
||||||
|
audioPresentation: string,
|
||||||
|
audioMode: string,
|
||||||
|
audioQuality: string,
|
||||||
|
manifestMimeType: string,
|
||||||
|
manifestHash: string,
|
||||||
|
manifest: string,
|
||||||
|
albumReplayGain: number,
|
||||||
|
albumPeakAmplitude: number,
|
||||||
|
trackReplayGain: number,
|
||||||
|
trackPeakAmplitude: number,
|
||||||
|
bitDepth: number,
|
||||||
|
sampleRate: number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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", {
|
||||||
|
headers: this.authHeaders
|
||||||
|
})
|
||||||
|
|
||||||
|
return await tracks.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadFlac(manifestMimeType:string, manifest: string) {
|
||||||
|
const id = Bun.nanoseconds().toString(36);
|
||||||
|
if (manifestMimeType === "application/dash+xml") {
|
||||||
|
await Bun.write(`tmp/${id}.mpd`, Buffer.from(manifest, "base64").toString("utf-8"))
|
||||||
|
|
||||||
|
await Bun.$`mpv --ao=null --stream-record=tmp/${id}.flac --speed=100 tmp/${id}.mpd`.quiet()
|
||||||
|
|
||||||
|
await Bun.file(`tmp/${id}.mpd`).delete();
|
||||||
|
|
||||||
|
const flac = Bun.file(`tmp/${id}.flac`)
|
||||||
|
|
||||||
|
const audioBuffer = await flac.arrayBuffer();
|
||||||
|
|
||||||
|
await flac.delete()
|
||||||
|
|
||||||
|
return audioBuffer;
|
||||||
|
} else if (manifestMimeType === "application/vnd.tidal.bts") {
|
||||||
|
const data = JSON.parse(Buffer.from(manifest, "base64").toString("utf-8")) as {
|
||||||
|
"mimeType": string,
|
||||||
|
"codecs": string,
|
||||||
|
"encryptionType": string,
|
||||||
|
"urls": string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const flac = await fetch(data.urls[0])
|
||||||
|
|
||||||
|
return await flac.arrayBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ArrayBuffer(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async tagFlac(id: number, audioBuffer: ArrayBuffer) {
|
||||||
|
const trackReq = await fetch(`https://api.tidal.com/v1/tracks/${id}/?countryCode=US`, {
|
||||||
|
headers: this.authHeaders
|
||||||
|
})
|
||||||
|
|
||||||
|
const track = await trackReq.json() as {
|
||||||
|
title: string,
|
||||||
|
replayGain: number,
|
||||||
|
peak: number,
|
||||||
|
streamStartDate: string,
|
||||||
|
trackNumber: number,
|
||||||
|
volumeNumber: number, // discNumber
|
||||||
|
copyright: string,
|
||||||
|
bpm: number,
|
||||||
|
url: string, // comment
|
||||||
|
isrc: string,
|
||||||
|
artists: [
|
||||||
|
{
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
album: {
|
||||||
|
id: number,
|
||||||
|
title: string,
|
||||||
|
cover: string,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const alubmReq = await fetch(`https://api.tidal.com/v1/albums/${track.album.id}/?countryCode=US`, {
|
||||||
|
headers: this.authHeaders
|
||||||
|
})
|
||||||
|
|
||||||
|
const album = await alubmReq.json() as {
|
||||||
|
title: string,
|
||||||
|
numberOfTracks: number,
|
||||||
|
releaseDate: string,
|
||||||
|
cover: string,
|
||||||
|
upc: string,
|
||||||
|
artists: [
|
||||||
|
{
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
return FlacStreamTagger.fromBuffer(Buffer.from(audioBuffer), {
|
||||||
|
tagMap: {
|
||||||
|
title: track.title,
|
||||||
|
trackNumber: track.trackNumber.toString(),
|
||||||
|
discNumber: track.volumeNumber.toString(),
|
||||||
|
bpm: track.bpm.toString(),
|
||||||
|
date: track.streamStartDate,
|
||||||
|
copyright: track.copyright,
|
||||||
|
REPLAYGAIN_TRACK_GAIN: track.replayGain.toString(),
|
||||||
|
REPLAYGAIN_TRACK_PEAK: track.peak.toString(),
|
||||||
|
comment: track.url,
|
||||||
|
isrc: track.isrc,
|
||||||
|
upc: album.upc,
|
||||||
|
artist: track.artists.map((a) => a.name),
|
||||||
|
album: album.title,
|
||||||
|
albumArtist: album.artists.map((a) => a.name),
|
||||||
|
totalTracks: album.numberOfTracks.toString(),
|
||||||
|
year: album.releaseDate.split("-")[0] || "",
|
||||||
|
},
|
||||||
|
picture: {
|
||||||
|
buffer: Buffer.from(await (await fetch(`https://resources.tidal.com/images/${album.cover.replaceAll("-", "/")}/1280x1280.jpg`)).arrayBuffer()),
|
||||||
|
}
|
||||||
|
}).toBuffer()
|
||||||
|
}
|
||||||
|
}
|
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue