Refactor authentication and utility functions for improved handling of audio formats and API responses

This commit is contained in:
Seth 2025-05-05 20:58:55 -04:00
parent d055deec29
commit 5325bbc34a
3 changed files with 27 additions and 23 deletions

View file

@ -9,7 +9,7 @@ Bun.serve({
"/api/track/:id": async req => { "/api/track/:id": async req => {
const trackId = parseInt(req.params.id) const trackId = parseInt(req.params.id)
/*const file = Bun.file(`downloaded/${trackId}.flac`) const file = Bun.file(`downloaded/${trackId}.flac`)
if (await file.exists()) { if (await file.exists()) {
return new Response(await file.arrayBuffer(), { return new Response(await file.arrayBuffer(), {
@ -20,7 +20,7 @@ Bun.serve({
"ETag": trackId.toString(), "ETag": trackId.toString(),
} }
}) })
}*/ }
const { manifestMimeType, manifest } = await utils.fetchTrack(trackId) const { manifestMimeType, manifest } = await utils.fetchTrack(trackId)
@ -50,9 +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) {
@ -67,15 +65,18 @@ for await (const track of tracks.items) {
} }
const trackData = await utils.fetchTrack(parseInt(id)) const trackData = await utils.fetchTrack(parseInt(id))
if (trackData?.status === 404 || trackData === null) {
console.error(`track ${trackId} not available`)
continue
}
try { try {
const audio = await utils.tagFlac(trackId, await utils.downloadFlac(trackData.manifestMimeType, trackData.manifest)) const audio = await utils.tagFlac(trackId, await utils.downloadFlac(trackData.manifestMimeType, trackData.manifest))
await Bun.write(`downloaded/${trackId}.flac`, audio) //await Bun.write(`downloaded/${trackId}.flac`, audio)
await utimes(`downloaded/${trackId}.flac`, createdAt, createdAt) //await utimes(`downloaded/${trackId}.flac`, createdAt, createdAt)
//console.log(`Downloaded ${trackId}.flac`) //console.log(`Downloaded ${trackId}.flac`)
} catch (e) { } catch (e) {
console.error(`Failed to download ${trackId}.flac`) console.error(`Failed to download ${trackId}.flac`, e)
} }
} }

View file

@ -1,27 +1,27 @@
import "./localStorage"; import "./localStorage";
import { init, initializeDeviceLogin, finalizeDeviceLogin, credentialsProvider } from "@tidal-music/auth"; import { init, initializeLogin, finalizeLogin, credentialsProvider } from "@tidal-music/auth";
const clientId = "zU4XHVVkc2tDPo4t"; const clientId = "mhPVJJEBNRzVjr2p";
const clientSecret = "VJKhDFqJPqvsPVNBV6ukXTJmwlvbttP7wlMlrc72se4=";
export default async () => { export default async () => {
await init({ await init({
clientId, clientId,
clientSecret,
credentialsStorageKey: "tidal-credentials", credentialsStorageKey: "tidal-credentials",
scopes: ["r_usr", "w_usr", "w_sub"], scopes: ["r_usr", "w_usr"],
}) })
const credentials = await credentialsProvider.getCredentials(); const credentials = await credentialsProvider.getCredentials();
if (typeof credentials.userId !== "string" || (credentials.expires || 0) < Date.now()) { if (typeof credentials.userId !== "string" || (credentials.expires || 0) < Date.now()) {
const response = await initializeDeviceLogin(); const response = await initializeLogin({
redirectUri: "https://desktop.tidal.com/login/auth"
});
console.log(`Please open https://${response.verificationUriComplete} to login.`) console.log(`Please open ${response} to login.`)
await finalizeDeviceLogin(); await finalizeLogin(new URL(prompt("Enter the URL you were redirected to: ") || "").search)
} }
return { return {

View file

@ -14,9 +14,9 @@ export default class {
this.authHeaders = authHeaders this.authHeaders = authHeaders
} }
async convertAacToFlac(buffer: ArrayBuffer) { async convertAacToFlac(buffer: Buffer<ArrayBuffer> | ArrayBuffer) {
const inputStream = new PassThrough(); const inputStream = new PassThrough();
inputStream.end(Buffer.from(buffer)); inputStream.end(Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer));
const outputChunks: Buffer[] = []; const outputChunks: Buffer[] = [];
@ -52,10 +52,12 @@ export default class {
} }
async fetchTrack(id: number) { 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`, { 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,
@ -70,6 +72,7 @@ export default class {
trackPeakAmplitude: number, trackPeakAmplitude: number,
bitDepth: number, bitDepth: number,
sampleRate: number, sampleRate: number,
status?: number,
} }
} }
@ -82,7 +85,6 @@ export default class {
} }
async downloadFlac(manifestMimeType: string, manifest: string) { async downloadFlac(manifestMimeType: string, manifest: string) {
const id = Bun.nanoseconds().toString(36);
if (manifestMimeType === "application/dash+xml") { if (manifestMimeType === "application/dash+xml") {
const mpd = new DashMPD(); const mpd = new DashMPD();
mpd.parse(Buffer.from(manifest, "base64").toString("utf-8")) mpd.parse(Buffer.from(manifest, "base64").toString("utf-8"))
@ -124,11 +126,11 @@ export default class {
return { buffer: new ArrayBuffer(0), mimeType: "" } // TODO: Handle other mime types return { buffer: new ArrayBuffer(0), mimeType: "" } // TODO: Handle other mime types
} }
async tagFlac(id: number, audioBuffer: { buffer: ArrayBuffer, mimeType: string }) { async tagFlac(id: number, audioBuffer: { buffer: Buffer<ArrayBuffer> | ArrayBuffer, mimeType: string }) {
const fileId = Bun.nanoseconds().toString(36); const fileId = Bun.nanoseconds().toString(36);
if (audioBuffer.mimeType === "audio/mp4") { if (audioBuffer.mimeType === "audio/mp4") {
audioBuffer.buffer = await this.convertAacToFlac(audioBuffer.buffer) as ArrayBuffer; audioBuffer.buffer = await this.convertAacToFlac(audioBuffer.buffer);
} }
const trackReq = await fetch(`https://api.tidal.com/v1/tracks/${id}/?countryCode=US`, { const trackReq = await fetch(`https://api.tidal.com/v1/tracks/${id}/?countryCode=US`, {
@ -156,6 +158,7 @@ export default class {
title: string, title: string,
cover: string, cover: string,
}, },
status?: string,
}; };
const alubmReq = await fetch(`https://api.tidal.com/v1/albums/${track.album.id}/?countryCode=US`, { const alubmReq = await fetch(`https://api.tidal.com/v1/albums/${track.album.id}/?countryCode=US`, {
@ -175,7 +178,7 @@ export default class {
], ],
} }
return FlacStreamTagger.fromBuffer(Buffer.from(audioBuffer.buffer), { return FlacStreamTagger.fromBuffer(Buffer.from(Buffer.isBuffer(audioBuffer.buffer) ? audioBuffer.buffer : Buffer.from(audioBuffer.buffer)), {
tagMap: { tagMap: {
title: track.title, title: track.title,
trackNumber: track.trackNumber.toString(), trackNumber: track.trackNumber.toString(),