generated from uwu/neptune-template
-
-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support for Low quality track downloading & info
- Loading branch information
Showing
13 changed files
with
342 additions
and
148 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { DecryptedKey } from "./decryptKeyId"; | ||
import type crypto from "crypto"; | ||
const { createDecipheriv } = <typeof crypto>require("crypto"); | ||
|
||
export const decryptBuffer = async (encryptedBuffer: Buffer, { key, nonce }: DecryptedKey) => { | ||
// Extend nonce to 16 bytes (nonce + counter) | ||
const iv = Buffer.concat([nonce, Buffer.alloc(8, 0)]); | ||
|
||
// Initialize counter and file decryptor | ||
const decipher = createDecipheriv("aes-128-ctr", key, iv); | ||
|
||
// Decrypt the data | ||
const decryptedData = Buffer.concat([decipher.update(encryptedBuffer), decipher.final()]); | ||
|
||
return decryptedData; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,60 @@ | ||
import { getStreamInfo } from "./getStreamInfo"; | ||
import { ExtendedPlayackInfo, getPlaybackInfo, ManifestMimeType } from "./getStreamInfo"; | ||
import { decryptBuffer } from "./decryptBuffer"; | ||
import { OnProgress, fetchy } from "./fetchy"; | ||
import { FetchyOptions, OnProgress, fetchy } from "./fetchy"; | ||
import { saveFile } from "./saveFile"; | ||
import { AudioQualityInverse, AudioQuality } from "./AudioQuality"; | ||
import { AudioQualityEnum } from "./AudioQuality"; | ||
import { decryptKeyId } from "./decryptKeyId"; | ||
import { TrackItem } from "neptune-types/tidal"; | ||
|
||
export const downloadSong = async (songId: number, fileName: string, quality: AudioQuality, onProgress: OnProgress) => { | ||
const streamInfo = await getStreamInfo(songId, quality); | ||
|
||
const { key, nonce } = streamInfo.cryptKey; | ||
const url = streamInfo.manifest.urls[0]; | ||
export type TrackOptions = { | ||
songId: number; | ||
desiredQuality: AudioQualityEnum; | ||
}; | ||
|
||
const encryptedBuffer = await fetchy(url, onProgress); | ||
export const fileNameFromInfo = (track: TrackItem, { manifest, manifestMimeType }: ExtendedPlayackInfo): string => { | ||
const artistName = track.artists?.[0].name; | ||
const base = `${track.title} by ${artistName ?? "Unknown"}`; | ||
switch (manifestMimeType) { | ||
case ManifestMimeType.Tidal: { | ||
const codec = manifest.codecs !== "flac" ? `.${manifest.codecs}` : ""; | ||
return `${base}${codec}.flac`; | ||
} | ||
case ManifestMimeType.Dash: { | ||
const trackManifest = manifest.tracks.audios[0]; | ||
return `${base}.${trackManifest.codec}.mp4`; | ||
} | ||
} | ||
}; | ||
|
||
// Read the encrypted data from the Response object | ||
const decodedBuffer = await decryptBuffer(encryptedBuffer, key, nonce); | ||
export const saveTrack = async (track: TrackItem, trackOptions: TrackOptions, options?: DownloadTrackOptions) => { | ||
// Download the bytes | ||
const trackInfo = await downloadTrack(trackOptions, options); | ||
|
||
// Prompt the user to save the file | ||
saveFile(new Blob([decodedBuffer], { type: "application/octet-stream" }), `${fileName} [${AudioQualityInverse[streamInfo.audioQuality]}].flac`); | ||
saveFile(new Blob([trackInfo.buffer], { type: "application/octet-stream" }), fileNameFromInfo(track, trackInfo)); | ||
}; | ||
|
||
export const downloadBytes = async (songId: number, quality: AudioQuality, byteRangeStart = 0, byteRangeEnd: number, onProgress: OnProgress) => { | ||
const streamInfo = await getStreamInfo(songId, quality); | ||
|
||
const { key, nonce } = streamInfo.cryptKey; | ||
const url = streamInfo.manifest.urls[0]; | ||
|
||
const encryptedBuffer = await fetchy(url, onProgress, byteRangeStart, byteRangeEnd); | ||
|
||
// Read the encrypted data from the Response object | ||
return decryptBuffer(encryptedBuffer, key, nonce); | ||
export type ExtendedPlaybackInfoWithBytes = ExtendedPlayackInfo & { buffer: Buffer }; | ||
|
||
export interface DownloadTrackOptions extends FetchyOptions { | ||
playbackInfo?: ExtendedPlayackInfo; | ||
} | ||
|
||
export const downloadTrack = async ({ songId, desiredQuality }: TrackOptions, options?: DownloadTrackOptions): Promise<ExtendedPlaybackInfoWithBytes> => { | ||
const { playbackInfo, manifest, manifestMimeType } = options?.playbackInfo ?? (await getPlaybackInfo(songId, desiredQuality)); | ||
|
||
switch (manifestMimeType) { | ||
case ManifestMimeType.Tidal: { | ||
const encryptedBuffer = await fetchy(manifest.urls[0], options); | ||
const decryptedKey = await decryptKeyId(manifest.keyId); | ||
const buffer = await decryptBuffer(encryptedBuffer, decryptedKey); | ||
return { playbackInfo, manifest, manifestMimeType, buffer }; | ||
} | ||
case ManifestMimeType.Dash: { | ||
if (options?.headers?.["range"] !== undefined) throw new Error("Range header not supported for dash streams"); | ||
const trackManifest = manifest.tracks.audios[0]; | ||
const buffer = Buffer.concat(await Promise.all(trackManifest.segments.map(({ url }) => fetchy(url.replaceAll("amp;", ""), options)))); | ||
return { playbackInfo, manifest, manifestMimeType, buffer }; | ||
} | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.