import { FlacStreamTagger } from "flac-stream-tagger"; import Ffmpeg from "fluent-ffmpeg"; import ffmpegPath from "ffmpeg-static"; export default class { authHeaders: { [key: string]: string } constructor(authHeaders: { [key: string]: string }) { this.authHeaders = authHeaders } async convertAacToFlac(buffer: ArrayBuffer) { const fileId = Bun.nanoseconds().toString(36); Bun.write(`tmp/${fileId}.aac`, Buffer.from(buffer)) await Bun.$`${ffmpegPath} -i tmp/${fileId}.aac -c:a flac tmp/${fileId}.flac`.quiet() await Bun.file(`tmp/${fileId}.aac`).delete(); const flac = Bun.file(`tmp/${fileId}.flac`) const audioBuffer = await flac.arrayBuffer(); await flac.delete() return audioBuffer; } 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.$`${ffmpegPath} -protocol_whitelist https,file,tls,tcp -i tmp/${id}.mpd -c:a copy tmp/${id}.flac`.quiet() await Bun.file(`tmp/${id}.mpd`).delete(); const flac = Bun.file(`tmp/${id}.flac`) const audioBuffer = await flac.arrayBuffer(); await flac.delete() 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, "codecs": string, "encryptionType": string, "urls": string[] } const flac = await fetch(data.urls[0]) return { buffer: await flac.arrayBuffer(), mimeType: data.mimeType } } return { buffer: new ArrayBuffer(0), mimeType: "" } // TODO: Handle other mime types } async tagFlac(id: number, audioBuffer: { buffer: ArrayBuffer, mimeType: string }) { const fileId = Bun.nanoseconds().toString(36); if (audioBuffer.mimeType === "audio/mp4") { audioBuffer.buffer = await this.convertAacToFlac(audioBuffer.buffer) as 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.buffer), { tagMap: { title: track.title, trackNumber: track.trackNumber.toString(), discNumber: track.volumeNumber.toString(), bpm: (track.bpm || 0).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() } }