generated from uwu/neptune-template
-
-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Cleanup Request methods & migrate Tidal/Shazam API calls
- Loading branch information
Showing
32 changed files
with
361 additions
and
225 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import type https from "https"; | ||
const { request } = <typeof https>require("https"); | ||
|
||
import { RequestOptions } from "https"; | ||
import type { Decipher } from "crypto"; | ||
import type { IncomingHttpHeaders, IncomingMessage } from "http"; | ||
import type { Readable } from "stream"; | ||
|
||
import { findModuleFunction } from "../findModuleFunction"; | ||
|
||
const getCredentials = findModuleFunction<() => Promise<{ token: string; clientId: string }>>("getCredentials", "function"); | ||
if (getCredentials === undefined) throw new Error("getCredentials method not found"); | ||
|
||
export const getHeaders = async (): Promise<Record<string, string>> => { | ||
const { clientId, token } = await getCredentials(); | ||
return { | ||
Authorization: `Bearer ${token}`, | ||
"x-tidal-token": clientId, | ||
}; | ||
}; | ||
|
||
export type OnProgress = (info: { total: number; downloaded: number; percent: number }) => void; | ||
export interface FetchyOptions { | ||
onProgress?: OnProgress; | ||
bytesWanted?: number; | ||
getDecipher?: () => Promise<Decipher>; | ||
requestOptions?: RequestOptions; | ||
} | ||
|
||
export const rejectNotOk = (res: IncomingMessage) => { | ||
const OK = res.statusCode !== undefined && res.statusCode >= 200 && res.statusCode < 300; | ||
if (!OK) throw new Error(`Status code is ${res.statusCode}`); | ||
return res; | ||
}; | ||
export const toJson = <T>(res: IncomingMessage): Promise<T> => toBuffer(res).then((buffer) => JSON.parse(buffer.toString())); | ||
export const toBuffer = (stream: Readable) => | ||
new Promise<Buffer>((resolve, reject) => { | ||
const chunks: Buffer[] = []; | ||
stream.on("data", (chunk) => chunks.push(chunk)); | ||
stream.on("end", () => resolve(Buffer.concat(chunks))); | ||
stream.on("error", reject); | ||
}); | ||
|
||
type RequestOptionsWithBody = RequestOptions & { body?: string }; | ||
export const requestStream = (url: string, options: RequestOptionsWithBody = {}) => | ||
new Promise<IncomingMessage>((resolve, reject) => { | ||
const body = options.body; | ||
delete options.body; | ||
options.headers ??= {}; | ||
options.headers["user-agent"] = navigator.userAgent; | ||
const req = request(url, options, resolve); | ||
req.on("error", reject); | ||
if (body !== undefined) req.write(body); | ||
req.end(); | ||
}); | ||
|
||
export const parseTotal = (headers: IncomingHttpHeaders) => { | ||
if (headers["content-range"]) { | ||
// Server supports byte range, parse total file size from header | ||
const match = /\/(\d+)$/.exec(headers["content-range"]); | ||
if (match) return parseInt(match[1], 10); | ||
} else { | ||
if (headers["content-length"] !== undefined) return parseInt(headers["content-length"], 10); | ||
} | ||
return -1; | ||
}; |
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,49 @@ | ||
import type { Readable } from "stream"; | ||
import { FetchyOptions, requestStream, rejectNotOk, parseTotal } from "."; | ||
|
||
import type stream from "stream"; | ||
const { Transform, PassThrough } = <typeof stream>require("stream"); | ||
|
||
export const requestDecodedStream = async (url: string, options?: FetchyOptions): Promise<Readable> => | ||
new Promise(async (resolve, reject) => { | ||
const { onProgress, bytesWanted, getDecipher } = options ?? {}; | ||
const reqOptions = { ...(options?.requestOptions ?? {}) }; | ||
if (bytesWanted !== undefined) { | ||
reqOptions.headers ??= {}; | ||
reqOptions.headers.Range = `bytes=0-${bytesWanted}`; | ||
} | ||
|
||
const res = await requestStream(url, reqOptions).then(rejectNotOk); | ||
res.on("error", reject); | ||
|
||
let downloaded = 0; | ||
const total = parseTotal(res.headers); | ||
if (total !== -1) onProgress?.({ total, downloaded, percent: (downloaded / total) * 100 }); | ||
|
||
if (getDecipher !== undefined) { | ||
const decipher = await getDecipher(); | ||
resolve( | ||
res.pipe( | ||
new Transform({ | ||
async transform(chunk, _, callback) { | ||
try { | ||
downloaded += chunk.length; | ||
onProgress?.({ total, downloaded, percent: (downloaded / total) * 100 }); | ||
callback(null, decipher.update(chunk)); | ||
} catch (err) { | ||
callback(<Error>err); | ||
} | ||
}, | ||
async flush(callback) { | ||
try { | ||
callback(null, decipher.final()); | ||
} catch (err) { | ||
callback(<Error>err); | ||
} | ||
}, | ||
}) | ||
) | ||
); | ||
} | ||
resolve(res); | ||
}); |
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,46 @@ | ||
import type { Readable } from "stream"; | ||
import { FetchyOptions, requestStream, rejectNotOk, parseTotal } from "."; | ||
|
||
import type stream from "stream"; | ||
const { Transform, PassThrough } = <typeof stream>require("stream"); | ||
|
||
export const requestSegmentsStream = async (segments: string[], options: FetchyOptions = {}) => | ||
new Promise<Readable>(async (resolve, reject) => { | ||
const combinedStream = new PassThrough(); | ||
|
||
let { onProgress, bytesWanted } = options ?? {}; | ||
let downloaded = 0; | ||
let total = 0; | ||
if (bytesWanted === undefined) { | ||
const buffers = await Promise.all( | ||
segments.map(async (url) => { | ||
const res = await requestStream(url).then(rejectNotOk); | ||
total += parseTotal(res.headers); | ||
const chunks: Buffer[] = []; | ||
res.on("data", (chunk) => { | ||
chunks.push(chunk); | ||
downloaded += chunk.length; | ||
onProgress?.({ total, downloaded, percent: (downloaded / total) * 100 }); | ||
}); | ||
res.on("error", reject); | ||
return new Promise<Buffer>((resolve) => res.on("end", () => resolve(Buffer.concat(chunks)))); | ||
}) | ||
); | ||
combinedStream.write(Buffer.concat(buffers)); | ||
} else { | ||
for (const url of segments) { | ||
const res = await requestStream(url).then(rejectNotOk); | ||
total += parseTotal(res.headers); | ||
res.on("data", (chunk) => { | ||
combinedStream.write(chunk); | ||
downloaded += chunk.length; | ||
onProgress?.({ total, downloaded, percent: (downloaded / total) * 100 }); | ||
}); | ||
res.on("error", reject); | ||
await new Promise((resolve) => res.on("end", resolve)); | ||
if (downloaded >= bytesWanted) break; | ||
} | ||
} | ||
combinedStream.end(); | ||
resolve(combinedStream); | ||
}); |
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,34 @@ | ||
import { rejectNotOk, requestStream, toJson } from "../fetch"; | ||
|
||
const CLIENT_ID = "tzecdDS3Bbx00rMP"; | ||
const CLIENT_SECRET = "zhRnKETi4FeXNGB72yAPJDssJ1U3BBGqmvYKcaw3kk8="; | ||
|
||
const tokenStore = { | ||
token: "", | ||
expiresAt: -1, | ||
}; | ||
type TokenInfo = { | ||
scope: string; | ||
token_type: string; | ||
access_token: string; | ||
expires_in: number; | ||
}; | ||
export const getToken = async () => { | ||
if (tokenStore.expiresAt > Date.now()) return tokenStore.token; | ||
const { access_token, expires_in } = await requestStream("https://auth.tidal.com/v1/oauth2/token", { | ||
method: "POST", | ||
headers: { | ||
Authorization: `Basic ${btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)}`, | ||
"Content-Type": "application/x-www-form-urlencoded", | ||
}, | ||
body: new URLSearchParams({ | ||
grant_type: "client_credentials", | ||
}).toString(), | ||
}) | ||
.then(rejectNotOk) | ||
.then(toJson<TokenInfo>); | ||
|
||
tokenStore.token = access_token; | ||
tokenStore.expiresAt = Date.now() + (expires_in - 60) * 1000; | ||
return tokenStore.token; | ||
}; |
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,13 @@ | ||
import { ISRCResponse } from "./types/ISRC"; | ||
import { requestStream, rejectNotOk, toJson } from "../fetch"; | ||
import { getToken } from "./auth"; | ||
|
||
export const fetchIsrc = async (isrc: string, limit?: number) => | ||
requestStream(`https://openapi.tidal.com/tracks/byIsrc?isrc=${isrc}&countryCode=US&limit=${limit ?? 100}`, { | ||
headers: { | ||
Authorization: `Bearer ${await getToken()}`, | ||
"Content-Type": "application/vnd.tidal.v1+json", | ||
}, | ||
}) | ||
.then(rejectNotOk) | ||
.then(toJson<ISRCResponse>); |
File renamed without changes.
File renamed without changes.
File renamed without changes.
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.