Skip to content

Commit

Permalink
Typescript
Browse files Browse the repository at this point in the history
  • Loading branch information
Inrixia committed Oct 19, 2023
1 parent 903741a commit 82244d2
Show file tree
Hide file tree
Showing 24 changed files with 401 additions and 184 deletions.
18 changes: 0 additions & 18 deletions lib/AudioQuality.js

This file was deleted.

38 changes: 38 additions & 0 deletions lib/AudioQuality.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ValueOf } from "@inrixia/helpers/ts";

export type PlaybackContext = {
actualAudioQuality: AudioQuality;
actualProductId: number;
};

export type AudioQuality = ValueOf<typeof PlaybackContextAudioQuality>;

export enum PlaybackContextAudioQuality {
HiRes = "HI_RES_LOSSLESS",
MQA = "HI_RES",
High = "LOSSLESS",
}

export enum MediaMetadataQuality {
High = "LOSSLESS",
MQA = "MQA",
HiRes = "HIRES_LOSSLESS",
Atmos = "DOLBY_ATMOS",
Sony360 = "SONY_360RA",
}

export const QualityMeta = {
[MediaMetadataQuality.MQA]: { className: "quality-tag", textContent: "MQA", color: "#F9BA7A" },
[MediaMetadataQuality.HiRes]: { className: "quality-tag", textContent: "HiRes", color: "#ffd432" },
[MediaMetadataQuality.Atmos]: { className: "quality-tag", textContent: "Atmos", color: "#0052a3" },
[MediaMetadataQuality.Sony360]: undefined,
[MediaMetadataQuality.High]: undefined,
} as const;

export const validQualities = Object.values(PlaybackContextAudioQuality);
export const validQualitiesSet = new Set(validQualities);

// Dont show MQA as a option as if HiRes is avalible itl always be served even if MQA is requested.
export const validQualitiesSettings = [PlaybackContextAudioQuality.HiRes, PlaybackContextAudioQuality.High];

export const AudioQualityInverse = Object.fromEntries(Object.entries(PlaybackContextAudioQuality).map(([key, value]) => [value, key]));
7 changes: 4 additions & 3 deletions lib/decryptKeyId.js → lib/decryptKeyId.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
const crypto = require("crypto");
import type crypto from "crypto";
const { createDecipheriv } = <typeof crypto>require("crypto");

// Do not change this
const mastKey = "UIlTTEMmmLfGowo/UC60x2H45W6MdGgTRfo/umg4754=";

export const decryptKeyId = async (keyId) => {
export const decryptKeyId = async (keyId: string) => {
// Decode the base64 strings to buffers
const mastKeyBuffer = Buffer.from(mastKey, "base64");
const keyIdBuffer = Buffer.from(keyId, "base64");
Expand All @@ -13,7 +14,7 @@ export const decryptKeyId = async (keyId) => {
const keyIdEnc = keyIdBuffer.slice(16);

// Initialize decryptor
const decryptor = crypto.createDecipheriv("aes-256-cbc", mastKeyBuffer, iv);
const decryptor = createDecipheriv("aes-256-cbc", mastKeyBuffer, iv);

// Decrypt the security token
const keyIdDec = decryptor.update(keyIdEnc);
Expand Down
8 changes: 4 additions & 4 deletions lib/download.js → lib/download.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { getStreamInfo } from "./getStreamInfo";
import { decryptBuffer } from "./decryptBuffer";
import { fetchy } from "./fetchy";
import { OnProgress, fetchy } from "./fetchy";
import { saveFile } from "./saveFile";
import { AudioQualityInverse } from "./AudioQuality";
import { AudioQualityInverse, PlaybackContextAudioQuality } from "./AudioQuality";

export const downloadSong = async (songId, fileName, quality, onProgress) => {
export const downloadSong = async (songId: number, fileName: string, quality: PlaybackContextAudioQuality, onProgress: OnProgress) => {
const streamInfo = await getStreamInfo(songId, quality);

const { key, nonce } = streamInfo.cryptKey;
Expand All @@ -19,7 +19,7 @@ export const downloadSong = async (songId, fileName, quality, onProgress) => {
saveFile(new Blob([decodedBuffer], { type: "application/octet-stream" }), `${fileName} [${AudioQualityInverse[streamInfo.audioQuality]}].flac`);
};

export const downloadBytes = async (songId, quality, byteRangeStart = 0, byteRangeEnd, onProgress) => {
export const downloadBytes = async (songId: number, quality: PlaybackContextAudioQuality, byteRangeStart = 0, byteRangeEnd: number, onProgress: OnProgress) => {
const streamInfo = await getStreamInfo(songId, quality);

const { key, nonce } = streamInfo.cryptKey;
Expand Down
27 changes: 16 additions & 11 deletions lib/fetchy.js → lib/fetchy.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,46 @@
const https = require("https");
import { getState } from "@neptune/store";
import type https from "https";
const { request } = <typeof https>require("https");

export const getHeaders = () => {
const state = getState();
import { store } from "@neptune";

export const getHeaders = (): Record<string, string> => {
const state = store.getState();
return {
Authorization: `Bearer ${state.session.oAuthAccessToken}`,
"x-tidal-token": state.session.apiToken,
};
};

export const fetchy = (url, onProgress, byteRangeStart = 0, byteRangeEnd) =>
export type OnProgress = (info: { total: number; downloaded: number; percent: number }) => void;

export const fetchy = (url: string, onProgress: OnProgress, byteRangeStart = 0, byteRangeEnd?: number) =>
new Promise((resolve, reject) => {
const headers = getHeaders();
if (typeof byteRangeStart !== "number") throw new Error("byteRangeStart must be a number");
if (byteRangeEnd !== undefined) {
if (typeof byteRangeEnd !== "number") throw new Error("byteRangeEnd must be a number");
headers["Range"] = `bytes=${byteRangeStart}-${byteRangeEnd}`;
}
const req = https.request(
const req = request(
url,
{
headers,
},
(res) => {
let total;
let total = -1;

if (res.headers["content-range"]) {
// Server supports byte range, parse total file size from header
const match = /\/(\d+)$/.exec(res.headers["content-range"]);
total = match ? parseInt(match[1], 10) : null;
if (match) total = parseInt(match[1], 10);
} else {
total = parseInt(res.headers["content-length"], 10);
if (res.headers["content-length"] !== undefined) total = parseInt(res.headers["content-length"], 10);
}

let downloaded = 0;
const chunks = [];
const chunks: Buffer[] = [];

res.on("data", (chunk) => {
res.on("data", (chunk: Buffer) => {
chunks.push(chunk);
downloaded += chunk.length;
if (onProgress !== undefined) onProgress({ total, downloaded, percent: (downloaded / total) * 100 });
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion lib/saveFile.js → lib/saveFile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const saveFile = (blob, fileName) => {
export const saveFile = (blob: Blob, fileName: string) => {
// Create a new Object URL for the Blob
const objectUrl = URL.createObjectURL(blob);

Expand Down
144 changes: 143 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
"build": "node ./build.js"
},
"devDependencies": {
"esbuild": "^0.18.13"
"@types/node": "^20.8.7",
"esbuild": "^0.18.13",
"neptune-types": "^1.0.0",
"typescript": "^5.2.2"
},
"dependencies": {}
"dependencies": {
"@inrixia/helpers": "^2.0.10"
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { html } from "@neptune/voby";

// @ts-expect-error Remove this when types are available
import { storage } from "@plugin";
import { AudioQualityInverse, AudioQuality, validQualitiesSettings } from "../../../lib/AudioQuality";
import { AudioQualityInverse, PlaybackContextAudioQuality, validQualitiesSettings } from "../../../lib/AudioQuality";

storage.desiredDownloadQuality = AudioQuality.HiRes;
storage.desiredDownloadQuality = PlaybackContextAudioQuality.HiRes;
export const Settings = () => html`<div class="settings-section">
<h3 class="settings-header">Download Quality</h3>
<p class="settings-explainer">Select the desired max download quality:</p>
<select id="qualityDropdown" onChange=${(event) => (storage.desiredDownloadQuality = event.target.value)}>
<select id="qualityDropdown" onChange=${(event: { target: { value: unknown } }) => (storage.desiredDownloadQuality = event.target.value)}>
${validQualitiesSettings.map((quality) => html`<option value=${quality} selected=${storage.desiredDownloadQuality === quality}>${AudioQualityInverse[quality]}</option>`)}
</select>
</div>`;
Loading

0 comments on commit 82244d2

Please sign in to comment.