From d81db90ed5077d768d08ecc137060856cdf4eaf0 Mon Sep 17 00:00:00 2001 From: Inrixia Date: Thu, 20 Jun 2024 17:36:05 +1200 Subject: [PATCH] Cleanup Request methods & migrate Tidal/Shazam API calls --- lib/fetch/index.ts | 66 ++++++++ lib/fetch/requestDecodedStream.ts | 49 ++++++ lib/fetch/requestSegmentsStream.ts | 46 ++++++ lib/fetchy.ts | 142 ------------------ lib/tidalDevApi/auth.ts | 34 +++++ lib/tidalDevApi/isrc.ts | 13 ++ .../tidalDevApi/types/ISRC.ts | 0 lib/{ => trackBytes}/decryptBuffer.ts | 0 lib/{ => trackBytes}/decryptKeyId.ts | 0 lib/{ => trackBytes}/download.ts | 6 +- lib/{ => trackBytes}/getPlaybackInfo.ts | 6 +- plugins/LastFM/src/LastFM.ts | 47 +++--- plugins/LastFM/src/index.ts | 17 +-- plugins/LastFM/src/types/lastfm/NowPlaying.ts | 21 +++ plugins/LastFM/src/types/lastfm/Scrobble.ts | 32 ++++ .../src/types/{ => musicbrainz}/ISRCData.ts | 0 .../src/types/{ => musicbrainz}/Recording.ts | 0 .../types/{ => musicbrainz}/ReleaseData.ts | 0 .../src/types/{ => musicbrainz}/UPCData.ts | 0 plugins/Shazam/package-lock.json | 26 +++- plugins/Shazam/package.json | 8 +- plugins/Shazam/src/fetch.ts | 20 --- plugins/Shazam/src/index.ts | 3 +- plugins/Shazam/src/shazamApi/fetch.ts | 15 ++ .../src/{types => shazamApi}/shazamTypes.ts | 0 plugins/SongDownloader/src/addMetadata.ts | 9 +- plugins/SongDownloader/src/index.ts | 4 +- plugins/SongDownloader/src/lib/fileName.ts | 2 +- plugins/SongDownloader/src/lib/toBuffer.ts | 9 -- plugins/TidalTags/src/index.ts | 5 +- plugins/TidalTags/src/lib/TrackInfoCache.ts | 4 +- plugins/TidalTags/src/setFLACInfo.ts | 2 +- 32 files changed, 361 insertions(+), 225 deletions(-) create mode 100644 lib/fetch/index.ts create mode 100644 lib/fetch/requestDecodedStream.ts create mode 100644 lib/fetch/requestSegmentsStream.ts delete mode 100644 lib/fetchy.ts create mode 100644 lib/tidalDevApi/auth.ts create mode 100644 lib/tidalDevApi/isrc.ts rename plugins/Shazam/src/types/isrcTypes.ts => lib/tidalDevApi/types/ISRC.ts (100%) rename lib/{ => trackBytes}/decryptBuffer.ts (100%) rename lib/{ => trackBytes}/decryptKeyId.ts (100%) rename lib/{ => trackBytes}/download.ts (87%) rename lib/{ => trackBytes}/getPlaybackInfo.ts (94%) create mode 100644 plugins/LastFM/src/types/lastfm/NowPlaying.ts create mode 100644 plugins/LastFM/src/types/lastfm/Scrobble.ts rename plugins/LastFM/src/types/{ => musicbrainz}/ISRCData.ts (100%) rename plugins/LastFM/src/types/{ => musicbrainz}/Recording.ts (100%) rename plugins/LastFM/src/types/{ => musicbrainz}/ReleaseData.ts (100%) rename plugins/LastFM/src/types/{ => musicbrainz}/UPCData.ts (100%) delete mode 100644 plugins/Shazam/src/fetch.ts create mode 100644 plugins/Shazam/src/shazamApi/fetch.ts rename plugins/Shazam/src/{types => shazamApi}/shazamTypes.ts (100%) delete mode 100644 plugins/SongDownloader/src/lib/toBuffer.ts diff --git a/lib/fetch/index.ts b/lib/fetch/index.ts new file mode 100644 index 0000000..aa154e2 --- /dev/null +++ b/lib/fetch/index.ts @@ -0,0 +1,66 @@ +import type https from "https"; +const { request } = 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> => { + 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; + 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 = (res: IncomingMessage): Promise => toBuffer(res).then((buffer) => JSON.parse(buffer.toString())); +export const toBuffer = (stream: Readable) => + new Promise((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((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; +}; diff --git a/lib/fetch/requestDecodedStream.ts b/lib/fetch/requestDecodedStream.ts new file mode 100644 index 0000000..0ecf5ca --- /dev/null +++ b/lib/fetch/requestDecodedStream.ts @@ -0,0 +1,49 @@ +import type { Readable } from "stream"; +import { FetchyOptions, requestStream, rejectNotOk, parseTotal } from "."; + +import type stream from "stream"; +const { Transform, PassThrough } = require("stream"); + +export const requestDecodedStream = async (url: string, options?: FetchyOptions): Promise => + 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(err); + } + }, + async flush(callback) { + try { + callback(null, decipher.final()); + } catch (err) { + callback(err); + } + }, + }) + ) + ); + } + resolve(res); + }); diff --git a/lib/fetch/requestSegmentsStream.ts b/lib/fetch/requestSegmentsStream.ts new file mode 100644 index 0000000..13bc60e --- /dev/null +++ b/lib/fetch/requestSegmentsStream.ts @@ -0,0 +1,46 @@ +import type { Readable } from "stream"; +import { FetchyOptions, requestStream, rejectNotOk, parseTotal } from "."; + +import type stream from "stream"; +const { Transform, PassThrough } = require("stream"); + +export const requestSegmentsStream = async (segments: string[], options: FetchyOptions = {}) => + new Promise(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((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); + }); diff --git a/lib/fetchy.ts b/lib/fetchy.ts deleted file mode 100644 index 993788a..0000000 --- a/lib/fetchy.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type https from "https"; -const { request } = require("https"); -import type stream from "stream"; -const { Transform, PassThrough } = require("stream"); - -import { modules } from "@neptune"; -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> => { - 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; - requestOptions?: RequestOptions; -} - -export const rejectNotOk = async (resP: IncomingMessage | Promise) => { - const res = await resP; - 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 requestStream = (url: string, options: RequestOptions = {}) => - new Promise((resolve, reject) => { - options.headers ??= {}; - options.headers["user-agent"] = navigator.userAgent; - const req = request(url, options, resolve); - req.on("error", reject); - req.end(); - }); - -export const requestSegmentsStream = async (segments: string[], options: FetchyOptions = {}) => - new Promise(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((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); - }); - -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; -}; - -export const requestDecodedStream = async (url: string, options?: FetchyOptions): Promise => - 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, encoding, callback) { - try { - downloaded += chunk.length; - onProgress?.({ total, downloaded, percent: (downloaded / total) * 100 }); - callback(null, decipher.update(chunk)); - } catch (err) { - callback(err); - } - }, - async flush(callback) { - try { - callback(null, decipher.final()); - } catch (err) { - callback(err); - } - }, - }) - ) - ); - } - resolve(res); - }); diff --git a/lib/tidalDevApi/auth.ts b/lib/tidalDevApi/auth.ts new file mode 100644 index 0000000..0a1bf6a --- /dev/null +++ b/lib/tidalDevApi/auth.ts @@ -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); + + tokenStore.token = access_token; + tokenStore.expiresAt = Date.now() + (expires_in - 60) * 1000; + return tokenStore.token; +}; diff --git a/lib/tidalDevApi/isrc.ts b/lib/tidalDevApi/isrc.ts new file mode 100644 index 0000000..bcd30bc --- /dev/null +++ b/lib/tidalDevApi/isrc.ts @@ -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); diff --git a/plugins/Shazam/src/types/isrcTypes.ts b/lib/tidalDevApi/types/ISRC.ts similarity index 100% rename from plugins/Shazam/src/types/isrcTypes.ts rename to lib/tidalDevApi/types/ISRC.ts diff --git a/lib/decryptBuffer.ts b/lib/trackBytes/decryptBuffer.ts similarity index 100% rename from lib/decryptBuffer.ts rename to lib/trackBytes/decryptBuffer.ts diff --git a/lib/decryptKeyId.ts b/lib/trackBytes/decryptKeyId.ts similarity index 100% rename from lib/decryptKeyId.ts rename to lib/trackBytes/decryptKeyId.ts diff --git a/lib/download.ts b/lib/trackBytes/download.ts similarity index 87% rename from lib/download.ts rename to lib/trackBytes/download.ts index 480fcc6..aee9fe3 100644 --- a/lib/download.ts +++ b/lib/trackBytes/download.ts @@ -1,7 +1,9 @@ import { ExtendedPlayackInfo, getPlaybackInfo, ManifestMimeType, TidalManifest } from "./getPlaybackInfo"; import { makeDecipheriv } from "./decryptBuffer"; -import { FetchyOptions, requestDecodedStream, requestSegmentsStream } from "./fetchy"; -import { AudioQuality } from "./AudioQualityTypes"; +import { FetchyOptions } from "../fetch"; +import { requestDecodedStream } from "../fetch/requestDecodedStream"; +import { requestSegmentsStream } from "../fetch/requestSegmentsStream"; +import { AudioQuality } from "../AudioQualityTypes"; import { decryptKeyId } from "./decryptKeyId"; import type { Readable } from "stream"; diff --git a/lib/getPlaybackInfo.ts b/lib/trackBytes/getPlaybackInfo.ts similarity index 94% rename from lib/getPlaybackInfo.ts rename to lib/trackBytes/getPlaybackInfo.ts index 09b1d09..5ea3eb5 100644 --- a/lib/getPlaybackInfo.ts +++ b/lib/trackBytes/getPlaybackInfo.ts @@ -1,9 +1,9 @@ -import { getHeaders } from "./fetchy"; -import { audioQualities, AudioQuality } from "./AudioQualityTypes"; +import { getHeaders } from "../fetch"; +import { audioQualities, AudioQuality } from "../AudioQualityTypes"; import { TrackItem } from "neptune-types/tidal"; import type { Manifest as DashManifest } from "dasha"; import type dasha from "dasha"; -import { Mutex } from "./mutex"; +import { Mutex } from "../mutex"; const { parse } = require("dasha"); export enum ManifestMimeType { diff --git a/plugins/LastFM/src/LastFM.ts b/plugins/LastFM/src/LastFM.ts index 997c604..7245822 100644 --- a/plugins/LastFM/src/LastFM.ts +++ b/plugins/LastFM/src/LastFM.ts @@ -1,7 +1,6 @@ -import { requestStream } from "../../../lib/fetchy"; import { findModuleFunction } from "../../../lib/findModuleFunction"; import type crypto from "crypto"; -import { toBuffer } from "../../SongDownloader/src/lib/toBuffer"; +import { requestStream, toBuffer, toJson } from "../../../lib/fetch"; const { createHash } = require("crypto"); const lastFmSecret = findModuleFunction("lastFmSecret", "string"); @@ -12,6 +11,8 @@ if (lastFmApiKey === undefined) throw new Error("Last.fm API key not found"); // @ts-expect-error Remove this when types are available import { storage } from "@plugin"; +import { NowPlaying } from "./types/lastfm/NowPlaying"; +import { Scrobble } from "./types/lastfm/Scrobble"; export type NowPlayingOpts = { track: string; @@ -36,8 +37,11 @@ type LastFmSession = { subscriber: number; }; -import type https from "https"; -const { request } = require("https"); +type ResponseType = + | (T & { message?: undefined }) + | { + message: string; + }; export class LastFM { private static generateApiSignature = (params: Record) => { @@ -50,45 +54,40 @@ export class LastFM { return createHash("md5").update(sig, "utf8").digest("hex"); }; - private static sendRequest = async (method: string, params?: Record, reqMethod = "GET") => { + private static sendRequest = async (method: string, params?: Record, reqMethod = "GET") => { params ??= {}; params.method = method; params.api_key = lastFmApiKey!; params.format = "json"; params.api_sig = this.generateApiSignature(params); - const data = await new Promise((resolve, reject) => { - const req = request(`https://ws.audioscrobbler.com/2.0/`, { - headers: { - "Content-type": "application/x-www-form-urlencoded", - "Accept-Charset": "utf-8", - "User-Agent": navigator.userAgent, - }, - method: "POST", - }); - req.on("error", reject); - req.on("response", (res) => toBuffer(res).then((buffer) => resolve(JSON.parse(buffer.toString())))); - req.write(new URLSearchParams(params).toString()); - req.end(); - }); + const data = await requestStream(`https://ws.audioscrobbler.com/2.0/`, { + headers: { + "Content-type": "application/x-www-form-urlencoded", + "Accept-Charset": "utf-8", + "User-Agent": navigator.userAgent, + }, + method: "POST", + body: new URLSearchParams(params).toString(), + }).then(toJson>); if (data.message) throw new Error(data.message); - else return data; + else return data; }; private static getSession = async (): Promise => { if (storage.lastFmSession !== undefined) return storage.lastFmSession; - const { token } = await this.sendRequest("auth.getToken"); + const { token } = await this.sendRequest<{ token: string }>("auth.getToken"); window.open(`https://www.last.fm/api/auth/?api_key=${lastFmApiKey}&token=${token}`, "_blank"); const result = window.confirm("Continue with last.fm authentication? Ensure you have given TIDAL permission on the opened page."); if (!result) throw new Error("Authentication cancelled"); - const { session } = await this.sendRequest("auth.getSession", { token }); + const { session } = await this.sendRequest<{ session: LastFmSession }>("auth.getSession", { token }); return (storage.lastFmSession = session); }; public static async updateNowPlaying(opts: NowPlayingOpts) { const session = await this.getSession(); - return this.sendRequest( + return this.sendRequest( "track.updateNowPlaying", { ...opts, @@ -100,7 +99,7 @@ export class LastFM { public static async scrobble(opts?: ScrobbleOpts) { const session = await this.getSession(); - return this.sendRequest( + return this.sendRequest( "track.scrobble", { ...opts, diff --git a/plugins/LastFM/src/index.ts b/plugins/LastFM/src/index.ts index 746e5db..eedea95 100644 --- a/plugins/LastFM/src/index.ts +++ b/plugins/LastFM/src/index.ts @@ -1,6 +1,6 @@ import { actions, intercept, store } from "@neptune"; import { PlaybackContext } from "../../../lib/AudioQualityTypes"; -import { rejectNotOk, requestStream } from "../../../lib/fetchy"; +import { rejectNotOk, requestStream, toJson } from "../../../lib/fetch"; import { LastFM, ScrobbleOpts } from "./LastFM"; @@ -8,13 +8,11 @@ import type { Album, MediaItem, TrackItem } from "neptune-types/tidal"; import { messageError, messageInfo } from "../../../lib/messageLogging"; import { interceptPromise } from "../../../lib/interceptPromise"; -import { toBuffer } from "../../SongDownloader/src/lib/toBuffer"; - -import type { Release, UPCData } from "./types/UPCData"; -import type { ISRCData } from "./types/ISRCData"; -import type { ReleaseData } from "./types/ReleaseData"; +import type { Release, UPCData } from "./types/musicbrainz/UPCData"; +import type { ISRCData } from "./types/musicbrainz/ISRCData"; +import type { ReleaseData } from "./types/musicbrainz/ReleaseData"; import { fullTitle } from "../../../lib/fullTitle"; -import { Recording } from "./types/Recording"; +import { Recording } from "./types/musicbrainz/Recording"; export { Settings } from "./Settings"; @@ -151,8 +149,9 @@ const _jsonCache: Record = {}; const fetchJson = async (url: string): Promise => { const jsonData = _jsonCache[url]; if (jsonData !== undefined) return jsonData as T; - const res = await requestStream(url).then(rejectNotOk); - return (_jsonCache[url] = JSON.parse((await toBuffer(res)).toString())); + return (_jsonCache[url] = await requestStream(url) + .then(rejectNotOk) + .then(toJson)); }; const mbidFromIsrc = async (isrc?: string) => { if (isrc !== undefined) return undefined; diff --git a/plugins/LastFM/src/types/lastfm/NowPlaying.ts b/plugins/LastFM/src/types/lastfm/NowPlaying.ts new file mode 100644 index 0000000..d1548dd --- /dev/null +++ b/plugins/LastFM/src/types/lastfm/NowPlaying.ts @@ -0,0 +1,21 @@ +export interface NowPlaying { + nowplaying?: Nowplaying; +} + +export interface Nowplaying { + artist?: Album; + track?: Album; + ignoredMessage?: IgnoredMessage; + albumArtist?: Album; + album?: Album; +} + +export interface Album { + corrected?: string; + "#text"?: string; +} + +export interface IgnoredMessage { + code?: string; + "#text"?: string; +} diff --git a/plugins/LastFM/src/types/lastfm/Scrobble.ts b/plugins/LastFM/src/types/lastfm/Scrobble.ts new file mode 100644 index 0000000..65659c3 --- /dev/null +++ b/plugins/LastFM/src/types/lastfm/Scrobble.ts @@ -0,0 +1,32 @@ +export interface Scrobble { + scrobbles?: Scrobbles; +} + +export interface Scrobbles { + scrobble?: ScrobbleClass; + "@attr"?: Attr; +} + +export interface Attr { + ignored?: number; + accepted?: number; +} + +export interface ScrobbleClass { + artist?: Album; + album?: Album; + track?: Album; + ignoredMessage?: IgnoredMessage; + albumArtist?: Album; + timestamp?: string; +} + +export interface Album { + corrected?: string; + "#text"?: string; +} + +export interface IgnoredMessage { + code?: string; + "#text"?: string; +} diff --git a/plugins/LastFM/src/types/ISRCData.ts b/plugins/LastFM/src/types/musicbrainz/ISRCData.ts similarity index 100% rename from plugins/LastFM/src/types/ISRCData.ts rename to plugins/LastFM/src/types/musicbrainz/ISRCData.ts diff --git a/plugins/LastFM/src/types/Recording.ts b/plugins/LastFM/src/types/musicbrainz/Recording.ts similarity index 100% rename from plugins/LastFM/src/types/Recording.ts rename to plugins/LastFM/src/types/musicbrainz/Recording.ts diff --git a/plugins/LastFM/src/types/ReleaseData.ts b/plugins/LastFM/src/types/musicbrainz/ReleaseData.ts similarity index 100% rename from plugins/LastFM/src/types/ReleaseData.ts rename to plugins/LastFM/src/types/musicbrainz/ReleaseData.ts diff --git a/plugins/LastFM/src/types/UPCData.ts b/plugins/LastFM/src/types/musicbrainz/UPCData.ts similarity index 100% rename from plugins/LastFM/src/types/UPCData.ts rename to plugins/LastFM/src/types/musicbrainz/UPCData.ts diff --git a/plugins/Shazam/package-lock.json b/plugins/Shazam/package-lock.json index 7f63116..1c4a1ec 100644 --- a/plugins/Shazam/package-lock.json +++ b/plugins/Shazam/package-lock.json @@ -6,13 +6,37 @@ "": { "hasInstallScript": true, "dependencies": { - "shazamio-core": "^1.3.1" + "shazamio-core": "^1.3.1", + "uuid": "^10.0.0" + }, + "devDependencies": { + "@types/uuid": "^9.0.8" } }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true, + "license": "MIT" + }, "node_modules/shazamio-core": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/shazamio-core/-/shazamio-core-1.3.1.tgz", "integrity": "sha512-wzYxaL+Tzj4hv5UO1kCbJSjnL02rfcPqxtklf2vg1ykwE1U/FXpB83SRTS4/0OU2uTcVcvQN+xTqzwZQl4CIMg==" + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } } } } diff --git a/plugins/Shazam/package.json b/plugins/Shazam/package.json index 1f96f88..7a2f747 100644 --- a/plugins/Shazam/package.json +++ b/plugins/Shazam/package.json @@ -1,8 +1,12 @@ { "dependencies": { - "shazamio-core": "^1.3.1" + "shazamio-core": "^1.3.1", + "uuid": "^10.0.0" }, "scripts": { "postinstall": "node ./fixShazamio.js" + }, + "devDependencies": { + "@types/uuid": "^9.0.8" } -} \ No newline at end of file +} diff --git a/plugins/Shazam/src/fetch.ts b/plugins/Shazam/src/fetch.ts deleted file mode 100644 index 3d0e55c..0000000 --- a/plugins/Shazam/src/fetch.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ISRCResponse } from "./types/isrcTypes"; -import { ShazamData } from "./types/shazamTypes"; - -const parseResponse = async (responseP: Promise | Response): Promise => { - const response = await responseP; - if (!response.ok) throw new Error(`Status ${response.status}`); - return response.json(); -}; -export const fetchShazamData = async (signature: { samplems: number; uri: string }) => { - return parseResponse( - fetch(`https://shazamwow.com/shazam`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ signature }), - }) - ); -}; -export const fetchIsrc = async (isrc: string) => { - return parseResponse(fetch(`https://shazamwow.com/isrc?isrc=${isrc}&countryCode=US&limit=100`)); -}; diff --git a/plugins/Shazam/src/index.ts b/plugins/Shazam/src/index.ts index cfd59f9..2591351 100644 --- a/plugins/Shazam/src/index.ts +++ b/plugins/Shazam/src/index.ts @@ -6,10 +6,11 @@ import { actions, store } from "@neptune"; import { DecodedSignature } from "shazamio-core"; import { interceptPromise } from "../../../lib/interceptPromise"; import { messageError, messageWarn, messageInfo } from "../../../lib/messageLogging"; -import { fetchShazamData, fetchIsrc } from "./fetch"; +import { fetchShazamData } from "./shazamApi/fetch"; // @ts-expect-error Remove this when types are available import { storage } from "@plugin"; +import { fetchIsrc } from "../../../lib/tidalDevApi/isrc"; export { Settings } from "./Settings"; diff --git a/plugins/Shazam/src/shazamApi/fetch.ts b/plugins/Shazam/src/shazamApi/fetch.ts new file mode 100644 index 0000000..7b56a91 --- /dev/null +++ b/plugins/Shazam/src/shazamApi/fetch.ts @@ -0,0 +1,15 @@ +import { rejectNotOk, requestStream, toJson } from "../../../../lib/fetch"; +import { ShazamData } from "./shazamTypes"; +import { v4 } from "uuid"; + +export const fetchShazamData = async (signature: { samplems: number; uri: string }) => + requestStream( + `https://amp.shazam.com/discovery/v5/en-US/US/iphone/-/tag/${v4()}/${v4()}?sync=true&webv3=true&sampling=true&connected=&shazamapiversion=v3&sharehub=true&hubv5minorversion=v5.1&hidelb=true&video=v3`, + { + headers: { "Content-Type": "application/json" }, + method: "POST", + body: JSON.stringify({ signature }), + } + ) + .then(rejectNotOk) + .then(toJson); diff --git a/plugins/Shazam/src/types/shazamTypes.ts b/plugins/Shazam/src/shazamApi/shazamTypes.ts similarity index 100% rename from plugins/Shazam/src/types/shazamTypes.ts rename to plugins/Shazam/src/shazamApi/shazamTypes.ts diff --git a/plugins/SongDownloader/src/addMetadata.ts b/plugins/SongDownloader/src/addMetadata.ts index 70e236a..58fe39f 100644 --- a/plugins/SongDownloader/src/addMetadata.ts +++ b/plugins/SongDownloader/src/addMetadata.ts @@ -1,10 +1,9 @@ import { utils } from "@neptune"; import { TrackItem } from "neptune-types/tidal"; import { fullTitle } from "../../../lib/fullTitle"; -import { toBuffer } from "./lib/toBuffer"; -import { ExtendedPlaybackInfoWithBytes } from "../../../lib/download"; -import { rejectNotOk, requestStream } from "../../../lib/fetchy"; -import { ManifestMimeType } from "../../../lib/getPlaybackInfo"; +import { ExtendedPlaybackInfoWithBytes } from "../../../lib/trackBytes/download"; +import { rejectNotOk, requestStream, toBuffer } from "../../../lib/fetch"; +import { ManifestMimeType } from "../../../lib/trackBytes/getPlaybackInfo"; import { actions } from "@neptune"; import { interceptPromise } from "../../../lib/interceptPromise"; @@ -64,7 +63,7 @@ async function makeTags(track: TrackItem) { try { picture = { pictureType: PictureType.FrontCover, - buffer: await toBuffer(await requestStream(utils.getMediaURLFromID(cover)).then(rejectNotOk)), + buffer: await requestStream(utils.getMediaURLFromID(cover)).then(rejectNotOk).then(toBuffer), }; } catch {} } diff --git a/plugins/SongDownloader/src/index.ts b/plugins/SongDownloader/src/index.ts index 608c3d9..d651de9 100644 --- a/plugins/SongDownloader/src/index.ts +++ b/plugins/SongDownloader/src/index.ts @@ -6,7 +6,7 @@ import { storage } from "@plugin"; import "./styles"; export { Settings } from "./Settings"; -import { fetchTrack, DownloadTrackOptions, TrackOptions } from "../../../lib/download"; +import { fetchTrack, DownloadTrackOptions, TrackOptions } from "../../../lib/trackBytes/download"; import { ItemId, MediaItem, TrackItem, VideoItem } from "neptune-types/tidal"; import { saveFile } from "./lib/saveFile"; @@ -14,8 +14,8 @@ import { interceptPromise } from "../../../lib/interceptPromise"; import { messageError } from "../../../lib/messageLogging"; import { addMetadata } from "./addMetadata"; -import { toBuffer } from "./lib/toBuffer"; import { fileNameFromInfo } from "./lib/fileName"; +import { toBuffer } from "../../../lib/fetch"; type DownloadButtoms = Record; const downloadButtons: DownloadButtoms = {}; diff --git a/plugins/SongDownloader/src/lib/fileName.ts b/plugins/SongDownloader/src/lib/fileName.ts index c0b603e..8e02955 100644 --- a/plugins/SongDownloader/src/lib/fileName.ts +++ b/plugins/SongDownloader/src/lib/fileName.ts @@ -1,5 +1,5 @@ import { TrackItem } from "neptune-types/tidal"; -import { ExtendedPlayackInfo, ManifestMimeType } from "../../../../lib/getPlaybackInfo"; +import { ExtendedPlayackInfo, ManifestMimeType } from "../../../../lib/trackBytes/getPlaybackInfo"; import { fullTitle } from "../../../../lib/fullTitle"; export const fileNameFromInfo = (track: TrackItem, { manifest, manifestMimeType }: ExtendedPlayackInfo): string => { diff --git a/plugins/SongDownloader/src/lib/toBuffer.ts b/plugins/SongDownloader/src/lib/toBuffer.ts deleted file mode 100644 index 3dc686f..0000000 --- a/plugins/SongDownloader/src/lib/toBuffer.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Readable } from "stream"; - -export const toBuffer = (stream: Readable) => - new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - stream.on("data", (chunk) => chunks.push(chunk)); - stream.on("end", () => resolve(Buffer.concat(chunks))); - stream.on("error", reject); - }); diff --git a/plugins/TidalTags/src/index.ts b/plugins/TidalTags/src/index.ts index 4732d78..61a8266 100644 --- a/plugins/TidalTags/src/index.ts +++ b/plugins/TidalTags/src/index.ts @@ -1,4 +1,4 @@ -import { intercept, store } from "@neptune"; +import { intercept, store, actions } from "@neptune"; import { setFLACInfo } from "./setFLACInfo"; @@ -13,6 +13,9 @@ import { isElement } from "./lib/isElement"; import { setInfoColumnHeaders, setInfoColumns } from "./setInfoColumns"; import { TrackItemCache } from "./lib/TrackItemCache"; import { PlaybackContext } from "../../../lib/AudioQualityTypes"; +import { getHeaders } from "../../../lib/fetch"; + +getHeaders().then(console.log); /** * Flac Info diff --git a/plugins/TidalTags/src/lib/TrackInfoCache.ts b/plugins/TidalTags/src/lib/TrackInfoCache.ts index f5e5dcd..21f04df 100644 --- a/plugins/TidalTags/src/lib/TrackInfoCache.ts +++ b/plugins/TidalTags/src/lib/TrackInfoCache.ts @@ -1,9 +1,9 @@ -import { fetchTrack } from "../../../../lib/download"; +import { fetchTrack } from "../../../../lib/trackBytes/download"; import { AudioQuality, PlaybackContext } from "../../../../lib/AudioQualityTypes"; import type { parseStream as ParseStreamType } from "music-metadata"; -import { ManifestMimeType } from "../../../../lib/getPlaybackInfo"; +import { ManifestMimeType } from "../../../../lib/trackBytes/getPlaybackInfo"; const { parseStream } = <{ parseStream: typeof ParseStreamType }>require("music-metadata/lib/core"); export type TrackInfo = { diff --git a/plugins/TidalTags/src/setFLACInfo.ts b/plugins/TidalTags/src/setFLACInfo.ts index 144d1fa..4b94366 100644 --- a/plugins/TidalTags/src/setFLACInfo.ts +++ b/plugins/TidalTags/src/setFLACInfo.ts @@ -73,7 +73,7 @@ export const setFLACInfo = async ([{ playbackContext }]: [{ playbackContext?: Pl const [progressBar, tidalQualityElement] = await Promise.all([progressBarP, tidalQualityElementP]); await setupQualityElementContainer; - let { actualAudioQuality, bitDepth, sampleRate, actualDuration } = playbackContext; + let { actualAudioQuality, bitDepth, sampleRate } = playbackContext; switch (actualAudioQuality) { case AudioQuality.MQA: { const color = (tidalQualityElement.style.color = progressBar.style.color = QualityMeta[QualityTag.MQA].color);