diff --git a/bun.lock b/bun.lock index a8cc768..5eb08ec 100644 --- a/bun.lock +++ b/bun.lock @@ -4,12 +4,10 @@ "": { "name": "bun-react-template", "dependencies": { - "@liveinstantly/dash-mpd-parser": "^0.5.0", "@tidal-music/auth": "^1.3.4", "@types/fluent-ffmpeg": "^2.1.27", "ffmpeg-static": "^5.2.0", "flac-stream-tagger": "^1.0.10", - "fluent-ffmpeg": "^2.1.3", }, "devDependencies": { "@types/bun": "latest", @@ -19,27 +17,25 @@ "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=="], - "@liveinstantly/dash-mpd-parser": ["@liveinstantly/dash-mpd-parser@0.5.0", "", { "dependencies": { "xmldom": "^0.6.0" } }, "sha512-ritwVw23l0lh4jeXLOtRitusbbAfg99Ztdz4rBg0HyGTZKwYC46FO8zotQFnZd9Vd+k8u9s90kwhf6PK91msKw=="], - "@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.10", "", { "dependencies": { "bun-types": "1.2.10" } }, "sha512-eilv6WFM3M0c9ztJt7/g80BDusK98z/FrFwseZgT4bXCq2vPhXD4z8R3oddmAn+R/Nmz9vBn4kweJKmGTZj+lg=="], + "@types/bun": ["@types/bun@1.2.8", "", { "dependencies": { "bun-types": "1.2.7" } }, "sha512-t8L1RvJVUghW5V+M/fL3Thbxcs0HwNsXsnTEBEfEVqGteiJToOlZ/fyOEaR1kZsNqnu+3XA4RI/qmnX4w6+S+w=="], "@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=="], - "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "async": ["async@0.2.10", "", {}, "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="], + "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "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.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="], "caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="], @@ -53,8 +49,6 @@ "flac-stream-tagger": ["flac-stream-tagger@1.0.10", "", { "dependencies": { "imageinfo": "^1.0.4" } }, "sha512-RqXyWnwx5xGTUuTCxzCCKK085sVxZzVCPwe1LShj1bDAn8zglhYcAPBorL4R0pK6+IIJtJsUuief3AawpNwIjw=="], - "fluent-ffmpeg": ["fluent-ffmpeg@2.1.3", "", { "dependencies": { "async": "^0.2.9", "which": "^1.1.1" } }, "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q=="], - "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=="], @@ -63,8 +57,6 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "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=="], @@ -83,10 +75,6 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "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=="], } } diff --git a/index.ts b/index.ts index 7fb8d46..2599ebe 100644 --- a/index.ts +++ b/index.ts @@ -6,13 +6,21 @@ const utils = new Utils(await Auth()); Bun.serve({ routes: { - "/api/track/:id": async req => { - const trackId = parseInt(req.params.id) + "/track": async (req: { url: string }) => { + const url = new URL(req.url) - /*const file = Bun.file(`downloaded/${trackId}.flac`) + 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(await file.arrayBuffer(), { + return new Response(file, { headers: { "Content-Type": "audio/flac", "Content-Disposition": `attachment; filename="${trackId}.flac"`, @@ -20,39 +28,32 @@ Bun.serve({ "ETag": trackId.toString(), } }) - }*/ + } - const { manifestMimeType, manifest } = await utils.fetchTrack(trackId) + 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 new Response(audio, { + await Bun.write(`downloaded/${trackId}.flac`, audio) + + return Response.json(audio, { headers: { "Content-Type": "audio/flac", - //"Content-Disposition": `attachment; filename="${trackId}.flac"`, + "Content-Disposition": `attachment; filename="${trackId}.flac"`, "Cache-Control": "public, max-age=31536000", "ETag": trackId.toString(), } }) }, - - "/api/@me/tracks": async () => { + "/tracks": async () => { const tracks = await utils.fetchTracks(); - return new Response(Bun.gzipSync(JSON.stringify(tracks)), { - headers: { - "Content-Type": "application/json", - "Content-Encoding": "gzip", - } - }) + return Response.json(tracks); } }, development: true }) -/* - const tracks = await utils.fetchTracks(); for await (const track of tracks.items) { @@ -79,5 +80,4 @@ for await (const track of tracks.items) { } } -console.log("Done") -*/ \ No newline at end of file +console.log("Done") \ No newline at end of file diff --git a/package.json b/package.json index 0c78252..516d598 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,10 @@ "start": "bun index.ts" }, "dependencies": { - "@liveinstantly/dash-mpd-parser": "^0.5.0", "@tidal-music/auth": "^1.3.4", "@types/fluent-ffmpeg": "^2.1.27", "ffmpeg-static": "^5.2.0", - "flac-stream-tagger": "^1.0.10", - "fluent-ffmpeg": "^2.1.3" + "flac-stream-tagger": "^1.0.10" }, "devDependencies": { "@types/bun": "latest" diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index fa483e2..87b6bba 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -1,11 +1,6 @@ import { FlacStreamTagger } from "flac-stream-tagger"; import Ffmpeg from "fluent-ffmpeg"; import ffmpegPath from "ffmpeg-static"; -import { PassThrough } from "stream"; -import { DashMPD } from '@liveinstantly/dash-mpd-parser'; - - -Ffmpeg.setFfmpegPath(ffmpegPath as string); export default class { authHeaders: { [key: string]: string } @@ -15,40 +10,18 @@ export default class { } async convertAacToFlac(buffer: ArrayBuffer) { - const inputStream = new PassThrough(); - inputStream.end(Buffer.from(buffer)); + const fileId = Bun.nanoseconds().toString(36); - const outputChunks: Buffer[] = []; + Bun.write(`tmp/${fileId}.aac`, Buffer.from(buffer)) - await new Promise((resolve, reject) => { - Ffmpeg(inputStream) - .format("flac") - .on("error", reject) - .on("end", resolve) - .pipe() - .on("data", chunk => outputChunks.push(chunk)); - }); + await Bun.$`${ffmpegPath} -i tmp/${fileId}.aac -c:a flac tmp/${fileId}.flac`.quiet() - return Buffer.concat(outputChunks);; - } + await Bun.file(`tmp/${fileId}.aac`).delete(); + const flac = Bun.file(`tmp/${fileId}.flac`) + const audioBuffer = await flac.arrayBuffer(); + await flac.delete() - 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); + return audioBuffer; } async fetchTrack(id: number) { @@ -84,30 +57,19 @@ export default class { async downloadFlac(manifestMimeType: string, manifest: string) { const id = Bun.nanoseconds().toString(36); if (manifestMimeType === "application/dash+xml") { - const mpd = new DashMPD(); - mpd.parse(Buffer.from(manifest, "base64").toString("utf-8")) + await Bun.write(`tmp/${id}.mpd`, 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 = []; + await Bun.$`${ffmpegPath} -protocol_whitelist https,file,tls,tcp -i tmp/${id}.mpd -c:a copy tmp/${id}.flac`.quiet() - const initialization = await fetch(segmentTemplate["@initialization"]) - const initializationBuffer = await initialization.arrayBuffer() - segmentBuffers.push(Buffer.from(initializationBuffer)); + await Bun.file(`tmp/${id}.mpd`).delete(); - const segmentPromises = []; - for (let i = 0; i < 1 + segmentCount; i++) { - const segmentUrl = segmentTemplate["@media"].replace("$Number$", segmentTemplate["@startNumber"] + i); - segmentPromises.push(fetch(segmentUrl).then(segment => segment.arrayBuffer()).then(segmentBuffer => Buffer.from(segmentBuffer))); - } + const flac = Bun.file(`tmp/${id}.flac`) - const segmentBuffersArray = await Promise.all(segmentPromises); - segmentBuffers.push(...segmentBuffersArray); + const audioBuffer = await flac.arrayBuffer(); - const flac = await this.convertMp4ToFlac(Buffer.concat(segmentBuffers)) + await flac.delete() - return { buffer: flac, mimeType: "audio/flac" } + return { buffer: audioBuffer, mimeType: "audio/flac" } } else if (manifestMimeType === "application/vnd.tidal.bts") { const data = JSON.parse(Buffer.from(manifest, "base64").toString("utf-8")) as { "mimeType": string, diff --git a/tmp.ts b/tmp.ts deleted file mode 100644 index 5220a86..0000000 --- a/tmp.ts +++ /dev/null @@ -1,12 +0,0 @@ -Bun.serve({ - routes: { - "/test.mpd": async () => { - return new Response(Bun.file("tmp/fpwp4o.mpd"), { - headers: { - // cors allow all - "Access-Control-Allow-Origin": "*", - } - }) - }, - } -}) \ No newline at end of file