164 lines
No EOL
5.6 KiB
TypeScript
164 lines
No EOL
5.6 KiB
TypeScript
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()
|
|
}
|
|
} |