diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b9e3fe4 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index 9f8feb8..3ee2bd7 100644 --- a/README.md +++ b/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. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..4c0d753 --- /dev/null +++ b/bun.lock @@ -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=="], + } +} diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..9819bf6 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[serve.static] +env = "BUN_PUBLIC_*" \ No newline at end of file diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..a78af63 --- /dev/null +++ b/index.ts @@ -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 +}) \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..db08560 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/helpers/auth.ts b/src/helpers/auth.ts new file mode 100644 index 0000000..f6e092d --- /dev/null +++ b/src/helpers/auth.ts @@ -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 || ""}` + } +} \ No newline at end of file diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts new file mode 100644 index 0000000..12f88ec --- /dev/null +++ b/src/helpers/utils.ts @@ -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() + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b123577 --- /dev/null +++ b/tsconfig.json @@ -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/*"] + } + } +}