From b005f1ccfd20be343f50d78c6d18f73313349890 Mon Sep 17 00:00:00 2001 From: Inrixia Date: Wed, 26 Jun 2024 03:18:14 +1200 Subject: [PATCH] Initial migration to native modules --- build.js | 37 +++++++++++- package.json | 2 +- plugins/LastFM/src/LastFM.ts | 6 +- plugins/NoBuffer/src/VoidStream.native.ts | 1 + plugins/NoBuffer/src/index.ts | 12 ++-- plugins/Shazam/src/shazamApi/fetch.ts | 8 +-- plugins/SongDownloader/src/addMetadata.ts | 21 ++++--- plugins/SongDownloader/src/index.ts | 28 ++++------ plugins/SongDownloader/src/lib/fileName.ts | 2 +- plugins/SongDownloader/src/lib/saveFile.ts | 5 +- plugins/Testing/src/index.ts | 1 - plugins/_lib/Caches/AlbumCache.ts | 2 - plugins/_lib/Caches/ExtendedTrackItem.ts | 5 +- plugins/_lib/Caches/PlaybackInfoCache.ts | 50 ++++------------- plugins/_lib/Caches/PlaybackInfoTypes.ts | 33 +++++++++++ plugins/_lib/Caches/PlaylistItemCache.ts | 1 - plugins/_lib/Caches/TrackInfoCache.ts | 56 +------------------ plugins/_lib/api/musicbrainz/index.ts | 6 +- .../_lib/api/musicbrainz/types/ReleaseData.ts | 12 ++-- plugins/_lib/api/musicbrainz/types/UPCData.ts | 16 +++--- plugins/_lib/api/musicbrainz/types/index.ts | 4 ++ plugins/_lib/api/requestCache.ts | 6 +- plugins/_lib/api/tidal/auth.ts | 8 +-- plugins/_lib/api/tidal/index.ts | 2 + plugins/_lib/api/tidal/isrc.ts | 3 +- plugins/_lib/api/tidal/types/index.ts | 1 + plugins/_lib/fetch.ts | 10 ++++ plugins/_lib/nativeBridge/index.ts | 26 +++++++++ .../_lib/nativeBridge/native/dasha.native.ts | 1 + .../native/getTrackInfo.native.ts | 54 ++++++++++++++++++ plugins/_lib/nativeBridge/native/index.ts | 3 + .../native/request/decrypt.native.ts} | 25 +++++++-- .../native/request/helpers.native.ts} | 43 ++------------ .../_lib/nativeBridge/native/request/index.ts | 2 + .../request/requestDecodedStream.native.ts} | 15 +++-- .../native/request/requestJson.native.ts | 7 +++ .../request/requestSegmentsStream.native.ts} | 8 +-- .../native/request/requestStream.native.ts | 18 ++++++ .../native/request/requestTrack.native.ts | 27 +++++++++ plugins/_lib/nativeBridge/nativeBridge.d.ts | 6 ++ .../_lib/nativeBridge/nativeBridge.native.ts | 5 ++ plugins/_lib/trackBytes/decryptBuffer.ts | 6 -- plugins/_lib/trackBytes/download.ts | 52 ----------------- 43 files changed, 346 insertions(+), 290 deletions(-) create mode 100644 plugins/NoBuffer/src/VoidStream.native.ts create mode 100644 plugins/_lib/Caches/PlaybackInfoTypes.ts create mode 100644 plugins/_lib/api/musicbrainz/types/index.ts create mode 100644 plugins/_lib/api/tidal/index.ts create mode 100644 plugins/_lib/api/tidal/types/index.ts create mode 100644 plugins/_lib/fetch.ts create mode 100644 plugins/_lib/nativeBridge/index.ts create mode 100644 plugins/_lib/nativeBridge/native/dasha.native.ts create mode 100644 plugins/_lib/nativeBridge/native/getTrackInfo.native.ts create mode 100644 plugins/_lib/nativeBridge/native/index.ts rename plugins/_lib/{trackBytes/decryptKeyId.ts => nativeBridge/native/request/decrypt.native.ts} (52%) rename plugins/_lib/{fetch/index.ts => nativeBridge/native/request/helpers.native.ts} (51%) create mode 100644 plugins/_lib/nativeBridge/native/request/index.ts rename plugins/_lib/{fetch/requestDecodedStream.ts => nativeBridge/native/request/requestDecodedStream.native.ts} (75%) create mode 100644 plugins/_lib/nativeBridge/native/request/requestJson.native.ts rename plugins/_lib/{fetch/requestSegmentsStream.ts => nativeBridge/native/request/requestSegmentsStream.native.ts} (86%) create mode 100644 plugins/_lib/nativeBridge/native/request/requestStream.native.ts create mode 100644 plugins/_lib/nativeBridge/native/request/requestTrack.native.ts create mode 100644 plugins/_lib/nativeBridge/nativeBridge.d.ts create mode 100644 plugins/_lib/nativeBridge/nativeBridge.native.ts delete mode 100644 plugins/_lib/trackBytes/decryptBuffer.ts delete mode 100644 plugins/_lib/trackBytes/download.ts diff --git a/build.js b/build.js index a1d893c7..7c0bc5f6 100644 --- a/build.js +++ b/build.js @@ -13,6 +13,42 @@ for (const plugin of plugins) { esbuild .build({ entryPoints: ["./" + path.join(pluginPath, pluginManifest.main ?? "index.js")], + plugins: [ + { + name: "neptuneNativeImports", + setup(build) { + build.onLoad({ filter: /.*[\/\\].+\.native\.[a-z]+/g }, async (args) => { + const result = await esbuild.build({ + entryPoints: [args.path], + bundle: true, + minify: true, + platform: "node", + format: "iife", + globalName: "neptuneExports", + write: false, + }); + + const outputCode = result.outputFiles[0].text; + + // HATE! WHY WHY WHY WHY WHY (globalName breaks metafile. crying emoji) + const { metafile } = await esbuild.build({ + entryPoints: [args.path], + platform: "node", + write: false, + metafile: true, + }); + + const builtExports = Object.values(metafile.outputs)[0].exports; + + return { + contents: `import {addUnloadable} from "@plugin";const contextId=NeptuneNative.createEvalScope(${JSON.stringify(outputCode)});${builtExports + .map((e) => `export ${e == "default" ? "default " : `const ${e} =`} NeptuneNative.getNativeValue(contextId,${JSON.stringify(e)})`) + .join(";")};addUnloadable(() => NeptuneNative.deleteEvalScope(contextId))`, + }; + }); + }, + }, + ], bundle: true, minify: true, format: "esm", @@ -36,7 +72,6 @@ for (const plugin of plugins) { hash: this.read(), }) ); - console.log("Built " + pluginManifest.name + "!"); }); }); diff --git a/package.json b/package.json index 3bcb5aec..3d2c4eec 100644 --- a/package.json +++ b/package.json @@ -29,4 +29,4 @@ "ext": "*", "exec": "npm run build" } -} \ No newline at end of file +} diff --git a/plugins/LastFM/src/LastFM.ts b/plugins/LastFM/src/LastFM.ts index 9209333a..27179ded 100644 --- a/plugins/LastFM/src/LastFM.ts +++ b/plugins/LastFM/src/LastFM.ts @@ -1,7 +1,6 @@ import { findModuleFunction } from "@inrixia/lib/findModuleFunction"; import type crypto from "crypto"; const { createHash } = require("crypto"); -import { requestStream, toJson } from "@inrixia/lib/fetch"; import storage from "./storage"; @@ -13,6 +12,7 @@ if (lastFmApiKey === undefined) throw new Error("Last.fm API key not found"); import { NowPlaying } from "./types/lastfm/NowPlaying"; import { Scrobble } from "./types/lastfm/Scrobble"; +import { requestJson } from "@inrixia/lib/nativeBridge"; export type NowPlayingOpts = { track: string; @@ -61,14 +61,14 @@ export class LastFM { params.format = "json"; params.api_sig = this.generateApiSignature(params); - const data = await requestStream(`https://ws.audioscrobbler.com/2.0/`, { + const data = await requestJson>(`https://ws.audioscrobbler.com/2.0/`, { headers: { "Content-type": "application/x-www-form-urlencoded", "Accept-Charset": "utf-8", }, method: "POST", body: new URLSearchParams(params).toString(), - }).then(toJson>); + }); if (data.message) throw new Error(data.message); else return data; diff --git a/plugins/NoBuffer/src/VoidStream.native.ts b/plugins/NoBuffer/src/VoidStream.native.ts new file mode 100644 index 00000000..57cef9be --- /dev/null +++ b/plugins/NoBuffer/src/VoidStream.native.ts @@ -0,0 +1 @@ +export { Writable } from "stream"; diff --git a/plugins/NoBuffer/src/index.ts b/plugins/NoBuffer/src/index.ts index f81acfb1..b717e26f 100644 --- a/plugins/NoBuffer/src/index.ts +++ b/plugins/NoBuffer/src/index.ts @@ -1,13 +1,11 @@ import { intercept, store } from "@neptune"; import { TrackItemCache } from "@inrixia/lib/Caches/TrackItemCache"; -import { fetchTrack } from "@inrixia/lib/trackBytes/download"; - -import * as stream from "stream"; -const { Writable } = require("stream"); +// import { fetchTrack } from "@inrixia/lib/trackBytes/download.native"; +import getPlaybackControl from "@inrixia/lib/getPlaybackControl"; +import { Writable } from "./VoidStream.native"; import { Tracer } from "@inrixia/lib/trace"; -import getPlaybackControl from "@inrixia/lib/getPlaybackControl"; const trace = Tracer("[NoBuffer]"); let unblocking = false; @@ -20,11 +18,11 @@ export const onUnload = intercept("playbackControls/SET_PLAYBACK_STATE", ([state const trackItem = await TrackItemCache.current(playbackContext); if (trackItem === undefined) return; trace.msg.log(`Playback stalled for ${trackItem?.title} - Kicking tidal CDN`); - const { stream } = await fetchTrack({ trackId: trackItem.id!, desiredQuality: playbackContext.actualAudioQuality }); + // const { stream } = await fetchTrack({ trackId: trackItem.id!, desiredQuality: playbackContext.actualAudioQuality }); const voidStream = new Writable({ write: (_: any, __: any, cb: () => void) => cb(), }); - stream.pipe(voidStream); + // stream.pipe(voidStream); await new Promise((res) => voidStream.on("end", res)); unblocking = false; })(); diff --git a/plugins/Shazam/src/shazamApi/fetch.ts b/plugins/Shazam/src/shazamApi/fetch.ts index b6e15159..fa561e69 100644 --- a/plugins/Shazam/src/shazamApi/fetch.ts +++ b/plugins/Shazam/src/shazamApi/fetch.ts @@ -1,15 +1,13 @@ -import { rejectNotOk, requestStream, toJson } from "@inrixia/lib/fetch"; +import { requestJson } from "@inrixia/lib/nativeBridge"; import { ShazamData } from "./shazamTypes"; import { v4 } from "uuid"; export const fetchShazamData = async (signature: { samplems: number; uri: string }) => - requestStream( + requestJson( `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/SongDownloader/src/addMetadata.ts b/plugins/SongDownloader/src/addMetadata.ts index d940713f..ed395b08 100644 --- a/plugins/SongDownloader/src/addMetadata.ts +++ b/plugins/SongDownloader/src/addMetadata.ts @@ -1,9 +1,8 @@ import { utils } from "@neptune"; import { TrackItem } from "neptune-types/tidal"; import { fullTitle } from "@inrixia/lib/fullTitle"; -import { ExtendedPlaybackInfoWithBytes } from "@inrixia/lib/trackBytes/download"; -import { rejectNotOk, requestStream, toBuffer } from "@inrixia/lib/fetch"; -import { ManifestMimeType } from "@inrixia/lib/Caches/PlaybackInfoCache"; +import type { ExtendedPlaybackInfoWithBytes } from "@inrixia/lib/trackBytes/download"; +import { ManifestMimeType } from "@inrixia/lib/Caches/PlaybackInfoTypes"; import { actions } from "@neptune"; import { interceptPromise } from "@inrixia/lib/intercept/interceptPromise"; @@ -58,13 +57,13 @@ async function makeTags(track: TrackItem) { } let picture; - if (cover !== undefined) { - try { - picture = { - pictureType: PictureType.FrontCover, - buffer: await requestStream(utils.getMediaURLFromID(cover)).then(rejectNotOk).then(toBuffer), - }; - } catch {} - } + // if (cover !== undefined) { + // try { + // picture = { + // pictureType: PictureType.FrontCover, + // buffer: await requestStream(utils.getMediaURLFromID(cover)).then(rejectNotOk).then(toBuffer), + // }; + // } catch {} + // } return { tagMap, picture }; } diff --git a/plugins/SongDownloader/src/index.ts b/plugins/SongDownloader/src/index.ts index 63fbf71e..faac8ee2 100644 --- a/plugins/SongDownloader/src/index.ts +++ b/plugins/SongDownloader/src/index.ts @@ -1,17 +1,10 @@ -import { intercept, actions, store } from "@neptune"; - import "./styles"; -import { fetchTrack, DownloadTrackOptions, TrackOptions } from "@inrixia/lib/trackBytes/download"; -import { ItemId, MediaItem, TrackItem, VideoItem } from "neptune-types/tidal"; +import { TrackItem } from "neptune-types/tidal"; import { saveFile, saveFileNode } from "./lib/saveFile"; -import { interceptPromise } from "@inrixia/lib/intercept/interceptPromise"; - import { addMetadata } from "./addMetadata"; import { fileNameFromInfo } from "./lib/fileName"; -import { toBuffer } from "@inrixia/lib/fetch"; -import { TrackItemCache } from "@inrixia/lib/Caches/TrackItemCache"; import { Tracer } from "@inrixia/lib/trace"; const trace = Tracer("[SongDownloader]"); @@ -80,19 +73,18 @@ ContextMenu.onOpen(async (contextSource, contextMenu, trackItems) => { prep(); for (const trackItem of trackItems) { if (trackItem.id === undefined) continue; - await downloadTrack(trackItem, { trackId: trackItem.id, desiredQuality: settings.desiredDownloadQuality }, { onProgress }).catch(trace.msg.err.withContext("Error downloading track")); + // await downloadTrack(trackItem, { trackId: trackItem.id, desiredQuality: settings.desiredDownloadQuality }, { onProgress }).catch(trace.msg.err.withContext("Error downloading track")); } clear(); }); contextMenu.appendChild(downloadButton); }); -export const downloadTrack = async (track: TrackItem, trackOptions: TrackOptions, options?: DownloadTrackOptions) => { - // Download the bytes - const trackInfo = await fetchTrack(trackOptions, options); - const streamWithTags = await addMetadata(trackInfo, track); - const fileName = fileNameFromInfo(track, trackInfo); - - if (settings.defaultDownloadPath !== "") return saveFileNode(streamWithTags ?? trackInfo.stream, settings.defaultDownloadPath, fileName); - return saveFile(streamWithTags ?? trackInfo.stream, fileName); -}; +// export const downloadTrack = async (track: TrackItem, trackOptions: TrackOptions, options?: DownloadTrackOptions) => { +// // Download the bytes +// const trackInfo = await fetchTrack(trackOptions, options); +// const streamWithTags = await addMetadata(trackInfo, track); +// const fileName = fileNameFromInfo(track, trackInfo); +// if (settings.defaultDownloadPath !== "") return saveFileNode(streamWithTags ?? trackInfo.stream, settings.defaultDownloadPath, fileName); +// return saveFile(streamWithTags ?? trackInfo.stream, fileName); +// }; diff --git a/plugins/SongDownloader/src/lib/fileName.ts b/plugins/SongDownloader/src/lib/fileName.ts index 1da73d95..bbec6118 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 "@inrixia/lib/Caches/PlaybackInfoCache"; +import { type ExtendedPlayackInfo, ManifestMimeType } from "@inrixia/lib/Caches/PlaybackInfoTypes"; import { fullTitle } from "@inrixia/lib/fullTitle"; export const fileNameFromInfo = (track: TrackItem, { manifest, manifestMimeType }: ExtendedPlayackInfo): string => { diff --git a/plugins/SongDownloader/src/lib/saveFile.ts b/plugins/SongDownloader/src/lib/saveFile.ts index d39e8340..cde0293c 100644 --- a/plugins/SongDownloader/src/lib/saveFile.ts +++ b/plugins/SongDownloader/src/lib/saveFile.ts @@ -1,4 +1,3 @@ -import { toBlob } from "@inrixia/lib/fetch"; import type * as fs from "fs/promises"; const { writeFile } = require("fs/promises"); @@ -11,9 +10,9 @@ export const saveFileNode = (stream: Readable, path: string, fileName: string) = return writeFile(`${path}/${sanitizeFilename(fileName)}`, stream); }; -export const saveFile = async (blob: Readable, fileName: string) => { +export const saveFile = async (blob: Blob, fileName: string) => { // Create a new Object URL for the Blob - const objectUrl = URL.createObjectURL(await toBlob(blob)); + const objectUrl = URL.createObjectURL(blob); // Create a link element const a = document.createElement("a"); diff --git a/plugins/Testing/src/index.ts b/plugins/Testing/src/index.ts index b6196022..a7ccea88 100644 --- a/plugins/Testing/src/index.ts +++ b/plugins/Testing/src/index.ts @@ -1,3 +1,2 @@ import { interceptActions } from "@inrixia/lib/intercept/interceptActions"; - export const onUnload = interceptActions(/.*/, console.log); diff --git a/plugins/_lib/Caches/AlbumCache.ts b/plugins/_lib/Caches/AlbumCache.ts index 6698a47a..6014fcce 100644 --- a/plugins/_lib/Caches/AlbumCache.ts +++ b/plugins/_lib/Caches/AlbumCache.ts @@ -1,8 +1,6 @@ import { actions, store } from "@neptune"; import type { Album, ItemId, MediaItem, TrackItem } from "neptune-types/tidal"; import { interceptPromise } from "../intercept/interceptPromise"; -import { SharedObjectStoreExpirable } from "../storage/SharedObjectStoreExpirable"; -import { retryPending } from "./retryPending"; import { libTrace } from "../trace"; import { SharedObjectStore } from "../storage/SharedObjectStore"; diff --git a/plugins/_lib/Caches/ExtendedTrackItem.ts b/plugins/_lib/Caches/ExtendedTrackItem.ts index d101d4b8..59e1e4a1 100644 --- a/plugins/_lib/Caches/ExtendedTrackItem.ts +++ b/plugins/_lib/Caches/ExtendedTrackItem.ts @@ -1,12 +1,9 @@ import type { ItemId, TrackItem, Album } from "neptune-types/tidal"; import type { PlaybackContext } from "../AudioQualityTypes"; -import { MusicBrainz } from "../api/musicbrainz"; -import { Recording } from "../api/musicbrainz/types/Recording"; -import { Release } from "../api/musicbrainz/types/UPCData"; +import { MusicBrainz, Release, Recording } from "../api/musicbrainz"; import { TrackItemCache } from "./TrackItemCache"; import { AlbumCache } from "./AlbumCache"; import { libTrace } from "../trace"; -import { store } from "@neptune"; import getPlaybackControl from "../getPlaybackControl"; export class ExtendedTrackItem { diff --git a/plugins/_lib/Caches/PlaybackInfoCache.ts b/plugins/_lib/Caches/PlaybackInfoCache.ts index e073dede..1172582d 100644 --- a/plugins/_lib/Caches/PlaybackInfoCache.ts +++ b/plugins/_lib/Caches/PlaybackInfoCache.ts @@ -1,45 +1,13 @@ -import { getHeaders } from "../fetch"; import { audioQualities, AudioQuality } from "../AudioQualityTypes"; -import { TrackItem } from "neptune-types/tidal"; -import type { Manifest as DashManifest } from "dasha"; - import { Semaphore } from "../Semaphore"; -import { SharedObjectStore } from "../storage/SharedObjectStore"; -import type * as dasha from "dasha"; import { SharedObjectStoreExpirable } from "../storage/SharedObjectStoreExpirable"; -const { parse } = require("dasha"); - -export enum ManifestMimeType { - Tidal = "application/vnd.tidal.bts", - Dash = "application/dash+xml", -} - -export type PlaybackInfo = { - trackId: number; - assetPresentation: string; - audioMode: NonNullable; - audioQuality: NonNullable; - manifestMimeType: ManifestMimeType; - manifestHash: string; - manifest: string; - albumReplayGain: number; - albumPeakAmplitude: number; - trackReplayGain: number; - trackPeakAmplitude: number; -}; - -export type TidalManifest = { - mimeType: string; - codecs: string; - encryptionType: string; - keyId: string; - urls: string[]; -}; +import { parseDasha } from "../nativeBridge"; +import { findModuleFunction } from "../findModuleFunction"; +import { ExtendedPlayackInfo, PlaybackInfo, ManifestMimeType, TidalManifest } from "./PlaybackInfoTypes"; -export type ExtendedPlayackInfo = - | { playbackInfo: PlaybackInfo; manifestMimeType: ManifestMimeType.Dash; manifest: DashManifest } - | { playbackInfo: PlaybackInfo; manifestMimeType: ManifestMimeType.Tidal; manifest: TidalManifest }; +const getCredentials = findModuleFunction<() => Promise<{ token: string; clientId: string }>>("getCredentials", "function"); +if (getCredentials === undefined) throw new Error("getCredentials method not found"); export class PlaybackInfoCache { private static readonly _store: SharedObjectStoreExpirable<[ExtendedPlayackInfo["playbackInfo"]["trackId"], ExtendedPlayackInfo["playbackInfo"]["audioQuality"]], ExtendedPlayackInfo> = @@ -62,8 +30,12 @@ export class PlaybackInfoCache { try { const url = `https://desktop.tidal.com/v1/tracks/${trackId}/playbackinfo?audioquality=${audioQuality}&playbackmode=STREAM&assetpresentation=FULL`; + const { clientId, token } = await getCredentials!(); const playbackInfo: PlaybackInfo = await fetch(url, { - headers: await getHeaders(), + headers: { + Authorization: `Bearer ${token}`, + "x-tidal-token": clientId, + }, }).then((r) => { if (r.status === 401) { alert("Failed to fetch Stream Info... Invalid OAuth Access Token!"); @@ -81,7 +53,7 @@ export class PlaybackInfoCache { return extPlaybackInfo; } case ManifestMimeType.Dash: { - const manifest = await parse(atob(playbackInfo.manifest), "https://sp-ad-cf.audio.tidal.com"); + const manifest = await parseDasha(atob(playbackInfo.manifest), "https://sp-ad-cf.audio.tidal.com"); return { playbackInfo, manifestMimeType: playbackInfo.manifestMimeType, manifest }; } default: { diff --git a/plugins/_lib/Caches/PlaybackInfoTypes.ts b/plugins/_lib/Caches/PlaybackInfoTypes.ts new file mode 100644 index 00000000..3cdb6449 --- /dev/null +++ b/plugins/_lib/Caches/PlaybackInfoTypes.ts @@ -0,0 +1,33 @@ +import { TrackItem } from "neptune-types/tidal"; +import type { DashManifest } from "../nativeBridge"; + +export enum ManifestMimeType { + Tidal = "application/vnd.tidal.bts", + Dash = "application/dash+xml", +} + +export type PlaybackInfo = { + trackId: number; + assetPresentation: string; + audioMode: NonNullable; + audioQuality: NonNullable; + manifestMimeType: ManifestMimeType; + manifestHash: string; + manifest: string; + albumReplayGain: number; + albumPeakAmplitude: number; + trackReplayGain: number; + trackPeakAmplitude: number; +}; + +export type TidalManifest = { + mimeType: string; + codecs: string; + encryptionType: string; + keyId: string; + urls: string[]; +}; + +export type ExtendedPlayackInfo = + | { playbackInfo: PlaybackInfo; manifestMimeType: ManifestMimeType.Dash; manifest: DashManifest } + | { playbackInfo: PlaybackInfo; manifestMimeType: ManifestMimeType.Tidal; manifest: TidalManifest }; diff --git a/plugins/_lib/Caches/PlaylistItemCache.ts b/plugins/_lib/Caches/PlaylistItemCache.ts index fc1b26ce..b4e351ee 100644 --- a/plugins/_lib/Caches/PlaylistItemCache.ts +++ b/plugins/_lib/Caches/PlaylistItemCache.ts @@ -2,7 +2,6 @@ import { actions } from "@neptune"; import type { ItemId, MediaItem, TrackItem } from "neptune-types/tidal"; import { interceptPromise } from "../intercept/interceptPromise"; import { SharedObjectStoreExpirable } from "../storage/SharedObjectStoreExpirable"; -import { retryPending } from "./retryPending"; import { libTrace } from "../trace"; diff --git a/plugins/_lib/Caches/TrackInfoCache.ts b/plugins/_lib/Caches/TrackInfoCache.ts index 9803cf82..96287527 100644 --- a/plugins/_lib/Caches/TrackInfoCache.ts +++ b/plugins/_lib/Caches/TrackInfoCache.ts @@ -1,25 +1,11 @@ -import { fetchTrack } from "../trackBytes/download"; import { AudioQuality, PlaybackContext } from "../AudioQualityTypes"; -import { ManifestMimeType } from "./PlaybackInfoCache"; -import { SharedObjectStore } from "../storage/SharedObjectStore"; - import { Tracer } from "../trace"; const tracer = Tracer("TrackInfoCache"); -import type { parseStream as ParseStreamType } from "music-metadata"; import { SharedObjectStoreExpirable } from "../storage/SharedObjectStoreExpirable"; -const { parseStream } = <{ parseStream: typeof ParseStreamType }>require("music-metadata/lib/core"); +import { type TrackInfo, getTrackInfo } from "../nativeBridge"; +import { PlaybackInfoCache } from "./PlaybackInfoCache"; -export type TrackInfo = { - trackId: PlaybackContext["actualProductId"]; - audioQuality: PlaybackContext["actualAudioQuality"]; - bitDepth: PlaybackContext["bitDepth"]; - sampleRate: PlaybackContext["sampleRate"]; - codec: PlaybackContext["codec"]; - duration: PlaybackContext["actualDuration"]; - bytes?: number; - bitrate?: number; -}; const WEEK = 7 * 24 * 60 * 60 * 1000; export class TrackInfoCache { private static readonly _listeners: Record void)[]> = {}; @@ -53,43 +39,7 @@ export class TrackInfoCache { return trackInfo; } private static async update(playbackContext: PlaybackContext): Promise { - let { actualProductId: trackId, actualAudioQuality: audioQuality, bitDepth, sampleRate, codec, actualDuration: duration } = playbackContext; - - const trackInfo: TrackInfo = { - trackId, - audioQuality, - bitDepth, - sampleRate, - codec, - duration, - }; - - // Fallback to parsing metadata if info is not in context - if (bitDepth === null || sampleRate === null || duration === null) { - const { stream, playbackInfo, manifestMimeType, manifest } = await fetchTrack( - { trackId: +trackId, desiredQuality: audioQuality }, - { bytesWanted: 256, onProgress: ({ total }) => (trackInfo.bytes = total) } - ); - // note that you cannot trust bytes to be populated until the stream is finished. parseStream will read the entire stream ensuring this - const { format } = await parseStream(stream, { mimeType: manifestMimeType === ManifestMimeType.Tidal ? manifest.mimeType : "audio/mp4" }); - - trackInfo.bitDepth ??= format.bitsPerSample! ?? 16; - trackInfo.sampleRate ??= format.sampleRate!; - trackInfo.codec ??= format.codec?.toLowerCase()!; - trackInfo.duration ??= format.duration!; - trackInfo.audioQuality = playbackInfo.audioQuality; - - if (manifestMimeType === ManifestMimeType.Dash) { - trackInfo.bitrate = manifest.tracks.audios[0].bitrate.bps; - trackInfo.bytes = manifest.tracks.audios[0].size?.b; - } - } else { - const { playbackInfo } = await fetchTrack({ trackId: +trackId, desiredQuality: audioQuality }, { requestOptions: { method: "HEAD" }, onProgress: ({ total }) => (trackInfo.bytes = total) }); - trackInfo.audioQuality = playbackInfo.audioQuality ?? audioQuality; - } - - trackInfo.bitrate ??= !!trackInfo.bytes ? (trackInfo.bytes / duration) * 8 : undefined; - + const trackInfo = await getTrackInfo(playbackContext, await PlaybackInfoCache.ensure(+playbackContext.actualProductId, playbackContext.actualAudioQuality)); this.put(trackInfo); return trackInfo; } diff --git a/plugins/_lib/api/musicbrainz/index.ts b/plugins/_lib/api/musicbrainz/index.ts index 28c49bd6..5b289db4 100644 --- a/plugins/_lib/api/musicbrainz/index.ts +++ b/plugins/_lib/api/musicbrainz/index.ts @@ -1,7 +1,7 @@ import { requestCached } from "../requestCache"; -import type { ISRCData } from "./types/ISRCData"; -import type { ReleaseData } from "./types/ReleaseData"; -import type { UPCData, Release } from "./types/UPCData"; +import type { ISRCData, UPCData, ReleaseData } from "./types"; + +export * from "./types"; export class MusicBrainz { public static async getRecording(isrc?: string) { diff --git a/plugins/_lib/api/musicbrainz/types/ReleaseData.ts b/plugins/_lib/api/musicbrainz/types/ReleaseData.ts index 937f89ae..6aef0efa 100644 --- a/plugins/_lib/api/musicbrainz/types/ReleaseData.ts +++ b/plugins/_lib/api/musicbrainz/types/ReleaseData.ts @@ -19,7 +19,7 @@ export interface ReleaseData { title?: string; } -export interface CoverArtArchive { +interface CoverArtArchive { count?: number; darkened?: boolean; artwork?: boolean; @@ -27,7 +27,7 @@ export interface CoverArtArchive { back?: boolean; } -export interface Media { +interface Media { "format-id"?: string; title?: string; format?: string; @@ -37,7 +37,7 @@ export interface Media { "track-count"?: number; } -export interface Track { +interface Track { position?: number; length?: number; number?: string; @@ -46,12 +46,12 @@ export interface Track { id?: string; } -export interface ReleaseEvent { +interface ReleaseEvent { date?: Date; area?: Area; } -export interface Area { +interface Area { "sort-name"?: string; "iso-3166-1-codes"?: string[]; name?: string; @@ -61,7 +61,7 @@ export interface Area { disambiguation?: string; } -export interface TextRepresentation { +interface TextRepresentation { script?: string; language?: string; } diff --git a/plugins/_lib/api/musicbrainz/types/UPCData.ts b/plugins/_lib/api/musicbrainz/types/UPCData.ts index d40ad491..a9dc7dfb 100644 --- a/plugins/_lib/api/musicbrainz/types/UPCData.ts +++ b/plugins/_lib/api/musicbrainz/types/UPCData.ts @@ -29,34 +29,34 @@ export interface Release { tags?: Tag[]; } -export interface ArtistCredit { +interface ArtistCredit { name?: string; artist?: Artist; } -export interface Artist { +interface Artist { id?: string; name?: string; "sort-name"?: string; disambiguation?: string; } -export interface LabelInfo { +interface LabelInfo { label?: Label; } -export interface Label { +interface Label { id?: string; name?: string; } -export interface Media { +interface Media { format?: string; "disc-count"?: number; "track-count"?: number; } -export interface ReleaseGroup { +interface ReleaseGroup { id?: string; "type-id"?: string; "primary-type-id"?: string; @@ -64,12 +64,12 @@ export interface ReleaseGroup { "primary-type"?: string; } -export interface Tag { +interface Tag { count?: number; name?: string; } -export interface TextRepresentation { +interface TextRepresentation { language?: string; script?: string; } diff --git a/plugins/_lib/api/musicbrainz/types/index.ts b/plugins/_lib/api/musicbrainz/types/index.ts new file mode 100644 index 00000000..03290d03 --- /dev/null +++ b/plugins/_lib/api/musicbrainz/types/index.ts @@ -0,0 +1,4 @@ +export * from "./ISRCData"; +export * from "./Recording"; +export * from "./ReleaseData"; +export * from "./UPCData"; diff --git a/plugins/_lib/api/requestCache.ts b/plugins/_lib/api/requestCache.ts index 2e1a1ea4..e6eb4e8d 100644 --- a/plugins/_lib/api/requestCache.ts +++ b/plugins/_lib/api/requestCache.ts @@ -1,4 +1,4 @@ -import { rejectNotOk, ExtendedRequestOptions, requestStream, toJson } from "../fetch"; +import { requestJson, ExtendedRequestOptions } from "../nativeBridge"; import { SharedObjectStore } from "../storage/SharedObjectStore"; import { Tracer } from "../trace"; @@ -8,9 +8,7 @@ export const requestCache = new SharedObjectStore("requestCache"); export const requestCached = async (url: string, options?: ExtendedRequestOptions): Promise => { let apiRes = await requestCache.get(url).catch(trace.err.withContext("get")); if (apiRes !== undefined) return apiRes; - apiRes = await requestStream(url, options) - .then(rejectNotOk) - .then(toJson); + apiRes = await requestJson(url, options); requestCache.put(apiRes, url).catch(trace.err.withContext("put")); return apiRes; }; diff --git a/plugins/_lib/api/tidal/auth.ts b/plugins/_lib/api/tidal/auth.ts index 1ea34ab2..b76da41e 100644 --- a/plugins/_lib/api/tidal/auth.ts +++ b/plugins/_lib/api/tidal/auth.ts @@ -1,4 +1,4 @@ -import { rejectNotOk, requestStream, toJson } from "../../fetch"; +import { requestJson } from "../../nativeBridge"; import { Semaphore } from "../../Semaphore"; const CLIENT_ID = "tzecdDS3Bbx00rMP"; @@ -19,7 +19,7 @@ export const getToken = async () => { await authSema.obtain(); try { if (tokenStore.expiresAt > Date.now()) return tokenStore.token; - const { access_token, expires_in } = await requestStream("https://auth.tidal.com/v1/oauth2/token", { + const { access_token, expires_in } = await requestJson("https://auth.tidal.com/v1/oauth2/token", { method: "POST", headers: { Authorization: `Basic ${btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)}`, @@ -28,9 +28,7 @@ export const getToken = async () => { body: new URLSearchParams({ grant_type: "client_credentials", }).toString(), - }) - .then(rejectNotOk) - .then(toJson); + }); tokenStore.token = access_token; tokenStore.expiresAt = Date.now() + (expires_in - 60) * 1000; diff --git a/plugins/_lib/api/tidal/index.ts b/plugins/_lib/api/tidal/index.ts new file mode 100644 index 00000000..010b2ae4 --- /dev/null +++ b/plugins/_lib/api/tidal/index.ts @@ -0,0 +1,2 @@ +export * from "./auth"; +export * from "./isrc"; diff --git a/plugins/_lib/api/tidal/isrc.ts b/plugins/_lib/api/tidal/isrc.ts index 83625db4..0db3e254 100644 --- a/plugins/_lib/api/tidal/isrc.ts +++ b/plugins/_lib/api/tidal/isrc.ts @@ -1,5 +1,4 @@ -import { Datum, ISRCResponse } from "./types/ISRC"; -import { rejectNotOk, toJson } from "../../fetch"; +import { Datum, ISRCResponse } from "./types"; import { getToken } from "./auth"; import { requestCached } from "../requestCache"; diff --git a/plugins/_lib/api/tidal/types/index.ts b/plugins/_lib/api/tidal/types/index.ts new file mode 100644 index 00000000..7e209be0 --- /dev/null +++ b/plugins/_lib/api/tidal/types/index.ts @@ -0,0 +1 @@ +export * from "./ISRC"; diff --git a/plugins/_lib/fetch.ts b/plugins/_lib/fetch.ts new file mode 100644 index 00000000..4c779665 --- /dev/null +++ b/plugins/_lib/fetch.ts @@ -0,0 +1,10 @@ +// export type TrackOptions = { +// trackId: number; +// desiredQuality: AudioQuality; +// }; +// export interface DownloadTrackOptions extends FetchyOptions { +// playbackInfo?: ExtendedPlayackInfo; +// } +// console.log("Ensuring..."); +// const { playbackInfo, manifest, manifestMimeType } = options?.playbackInfo ?? (await PlaybackInfoCache.ensure(trackId, desiredQuality)); +// console.log("Ensured", playbackInfo, manifest, manifestMimeType); diff --git a/plugins/_lib/nativeBridge/index.ts b/plugins/_lib/nativeBridge/index.ts new file mode 100644 index 00000000..652344f3 --- /dev/null +++ b/plugins/_lib/nativeBridge/index.ts @@ -0,0 +1,26 @@ +import type * as nativeBridge from "./native"; +import "./nativeBridge.native"; + +type UnsafeReturnType = T extends (...args: any[]) => infer R ? R : any; +type UnsafeParameters = T extends (...args: infer P) => any ? P : never; +type PromisefulModule = { + [K in keyof M]: M[K] extends Function + ? UnsafeReturnType extends PromiseLike + ? (...args: UnsafeParameters) => UnsafeReturnType + : (...args: UnsafeParameters) => Promise> + : () => Promise; +}; + +const invoke: (method: string, ...args: any[]) => Promise = (window).electron.ipcRenderer.invoke; +module.exports = new Proxy(>{}, { + get: + (_, key: string, __) => + (...args: any[]) => + invoke("___nativeBridge___", key, ...args).catch((err: Error) => { + err.message = err.message.replaceAll("Error invoking remote method '___nativeBridge___': ", ""); + throw err; + }), + set: () => { + throw new Error("You cannot set properties of nativeBridge"); + }, +}); diff --git a/plugins/_lib/nativeBridge/native/dasha.native.ts b/plugins/_lib/nativeBridge/native/dasha.native.ts new file mode 100644 index 00000000..03e221b0 --- /dev/null +++ b/plugins/_lib/nativeBridge/native/dasha.native.ts @@ -0,0 +1 @@ +export { parse as parseDasha, type Manifest as DashManifest } from "dasha"; diff --git a/plugins/_lib/nativeBridge/native/getTrackInfo.native.ts b/plugins/_lib/nativeBridge/native/getTrackInfo.native.ts new file mode 100644 index 00000000..14a75b8d --- /dev/null +++ b/plugins/_lib/nativeBridge/native/getTrackInfo.native.ts @@ -0,0 +1,54 @@ +import { parseStream } from "music-metadata"; +import { AudioQuality, PlaybackContext } from "../../AudioQualityTypes"; +import { type ExtendedPlayackInfo, ManifestMimeType } from "../../Caches/PlaybackInfoTypes"; +import { requestTrackStream } from "./request/requestTrack.native"; + +export type TrackInfo = { + trackId: PlaybackContext["actualProductId"]; + audioQuality: PlaybackContext["actualAudioQuality"]; + bitDepth: PlaybackContext["bitDepth"]; + sampleRate: PlaybackContext["sampleRate"]; + codec: PlaybackContext["codec"]; + duration: PlaybackContext["actualDuration"]; + bytes?: number; + bitrate?: number; +}; + +export const getTrackInfo = async (playbackContext: PlaybackContext, extPlaybackInfo: ExtendedPlayackInfo): Promise => { + let { actualProductId: trackId, actualAudioQuality: audioQuality, bitDepth, sampleRate, codec, actualDuration: duration } = playbackContext; + const { manifestMimeType, manifest, playbackInfo } = extPlaybackInfo; + + const trackInfo: TrackInfo = { + trackId, + audioQuality, + bitDepth, + sampleRate, + codec, + duration, + }; + + // Fallback to parsing metadata if info is not in context + if (bitDepth === null || sampleRate === null || duration === null) { + const stream = await requestTrackStream(extPlaybackInfo, { bytesWanted: 256, onProgress: ({ total }) => (trackInfo.bytes = total) }); + // note that you cannot trust bytes to be populated until the stream is finished. parseStream will read the entire stream ensuring this + const { format } = await parseStream(stream, { mimeType: manifestMimeType === ManifestMimeType.Tidal ? manifest.mimeType : "audio/mp4" }); + + trackInfo.bitDepth ??= format.bitsPerSample! ?? 16; + trackInfo.sampleRate ??= format.sampleRate!; + trackInfo.codec ??= format.codec?.toLowerCase()!; + trackInfo.duration ??= format.duration!; + trackInfo.audioQuality = playbackInfo.audioQuality; + + if (manifestMimeType === ManifestMimeType.Dash) { + trackInfo.bitrate = manifest.tracks.audios[0].bitrate.bps; + trackInfo.bytes = manifest.tracks.audios[0].size?.b; + } + } else { + const stream = await requestTrackStream(extPlaybackInfo, { requestOptions: { method: "HEAD" }, onProgress: ({ total }) => (trackInfo.bytes = total) }); + trackInfo.audioQuality = playbackInfo.audioQuality ?? audioQuality; + } + + trackInfo.bitrate ??= !!trackInfo.bytes ? (trackInfo.bytes / duration) * 8 : undefined; + + return trackInfo; +}; diff --git a/plugins/_lib/nativeBridge/native/index.ts b/plugins/_lib/nativeBridge/native/index.ts new file mode 100644 index 00000000..dac94ccd --- /dev/null +++ b/plugins/_lib/nativeBridge/native/index.ts @@ -0,0 +1,3 @@ +export * from "./request"; +export * from "./dasha.native"; +export * from "./getTrackInfo.native"; diff --git a/plugins/_lib/trackBytes/decryptKeyId.ts b/plugins/_lib/nativeBridge/native/request/decrypt.native.ts similarity index 52% rename from plugins/_lib/trackBytes/decryptKeyId.ts rename to plugins/_lib/nativeBridge/native/request/decrypt.native.ts index a27fdc24..2e147be6 100644 --- a/plugins/_lib/trackBytes/decryptKeyId.ts +++ b/plugins/_lib/nativeBridge/native/request/decrypt.native.ts @@ -1,16 +1,16 @@ -import type crypto from "crypto"; -const { createDecipheriv } = require("crypto"); +import { createDecipheriv } from "crypto"; +import type { TidalManifest } from "../../../Caches/PlaybackInfoTypes"; // Do not change this const mastKey = "UIlTTEMmmLfGowo/UC60x2H45W6MdGgTRfo/umg4754="; const mastKeyBuffer = Buffer.from(mastKey, "base64"); -export type DecryptedKey = { +type DecryptedKey = { key: Buffer; nonce: Buffer; }; -export const decryptKeyId = (keyId: string): DecryptedKey => { +const decryptKeyId = (keyId: string): DecryptedKey => { // Decode the base64 strings to buffers const keyIdBuffer = Buffer.from(keyId, "base64"); @@ -30,3 +30,20 @@ export const decryptKeyId = (keyId: string): DecryptedKey => { return { key, nonce }; }; + +// Extend nonce to 16 bytes (nonce + counter) +const makeDecipheriv = ({ key, nonce }: DecryptedKey) => createDecipheriv("aes-128-ctr", key, Buffer.concat([nonce, Buffer.alloc(8, 0)])); + +export const makeDecipher = (manifest: TidalManifest) => { + switch (manifest.encryptionType) { + case "OLD_AES": { + return makeDecipheriv(decryptKeyId(manifest.keyId)); + } + case "NONE": { + return undefined; + } + default: { + throw new Error(`Unexpected manifest encryption type ${manifest.encryptionType}`); + } + } +}; diff --git a/plugins/_lib/fetch/index.ts b/plugins/_lib/nativeBridge/native/request/helpers.native.ts similarity index 51% rename from plugins/_lib/fetch/index.ts rename to plugins/_lib/nativeBridge/native/request/helpers.native.ts index aa5a0004..796e83aa 100644 --- a/plugins/_lib/fetch/index.ts +++ b/plugins/_lib/nativeBridge/native/request/helpers.native.ts @@ -1,36 +1,19 @@ -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, - }; -}; +import type { TidalManifest } from "../../../Caches/PlaybackInfoTypes"; +import { ExtendedRequestOptions } from "./requestStream.native"; export type OnProgress = (info: { total: number; downloaded: number; percent: number }) => void; export interface FetchyOptions { onProgress?: OnProgress; bytesWanted?: number; - getDecipher?: () => Promise; - requestOptions?: RequestOptions; + manifest?: TidalManifest; + requestOptions?: ExtendedRequestOptions; poke?: true; } export const rejectNotOk = (res: IncomingMessage) => { const OK = res.statusCode !== undefined && res.statusCode >= 200 && res.statusCode < 300; - if (res.statusCode === undefined) console.log(res); if (!OK) throw new Error(`Status code is ${res.statusCode}`); return res; }; @@ -50,24 +33,6 @@ export const toBlob = (stream: Readable) => stream.on("error", reject); }); -export type ExtendedRequestOptions = RequestOptions & { body?: string; poke?: true }; -export const requestStream = (url: string, options: ExtendedRequestOptions = {}) => - new Promise((resolve, reject) => { - const body = options.body; - delete options.body; - options.headers ??= {}; - options.headers["user-agent"] ??= navigator.userAgent; - const req = request(url, options, (res) => { - const statusMsg = res.statusMessage !== "" ? ` - ${res.statusMessage}` : ""; - console.debug(`[${res.statusCode}${statusMsg}] (${req.method})`, url, res); - if (options.poke) req.destroy(); - resolve(res); - }); - 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 diff --git a/plugins/_lib/nativeBridge/native/request/index.ts b/plugins/_lib/nativeBridge/native/request/index.ts new file mode 100644 index 00000000..77beaaa5 --- /dev/null +++ b/plugins/_lib/nativeBridge/native/request/index.ts @@ -0,0 +1,2 @@ +export type { ExtendedRequestOptions } from "./requestStream.native"; +export { requestJson } from "./requestJson.native"; diff --git a/plugins/_lib/fetch/requestDecodedStream.ts b/plugins/_lib/nativeBridge/native/request/requestDecodedStream.native.ts similarity index 75% rename from plugins/_lib/fetch/requestDecodedStream.ts rename to plugins/_lib/nativeBridge/native/request/requestDecodedStream.native.ts index 3b337cde..92f9fca5 100644 --- a/plugins/_lib/fetch/requestDecodedStream.ts +++ b/plugins/_lib/nativeBridge/native/request/requestDecodedStream.native.ts @@ -1,12 +1,13 @@ import type { Readable } from "stream"; -import { FetchyOptions, requestStream, rejectNotOk, parseTotal } from "."; +import { FetchyOptions, rejectNotOk, parseTotal } from "./helpers.native"; +import { requestStream } from "./requestStream.native"; -import type stream from "stream"; -const { Transform } = require("stream"); +import { Transform } from "stream"; +import { makeDecipher } from "./decrypt.native"; export const requestDecodedStream = async (url: string, options?: FetchyOptions): Promise => new Promise(async (resolve, reject) => { - const { onProgress, bytesWanted, getDecipher, poke } = options ?? {}; + const { onProgress, bytesWanted, manifest } = options ?? {}; const reqOptions = { ...(options?.requestOptions ?? {}) }; if (bytesWanted !== undefined) { reqOptions.headers ??= {}; @@ -14,14 +15,16 @@ export const requestDecodedStream = async (url: string, options?: FetchyOptions) } 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(); + const decipher = manifest ? makeDecipher(manifest) : undefined; + + if (decipher !== undefined) { resolve( res.pipe( new Transform({ diff --git a/plugins/_lib/nativeBridge/native/request/requestJson.native.ts b/plugins/_lib/nativeBridge/native/request/requestJson.native.ts new file mode 100644 index 00000000..994f86b4 --- /dev/null +++ b/plugins/_lib/nativeBridge/native/request/requestJson.native.ts @@ -0,0 +1,7 @@ +import { rejectNotOk, toJson } from "./helpers.native"; +import { ExtendedRequestOptions, requestStream } from "./requestStream.native"; + +export const requestJson = async (url: string, options: ExtendedRequestOptions = {}) => + requestStream(url, options) + .then(rejectNotOk) + .then(toJson); diff --git a/plugins/_lib/fetch/requestSegmentsStream.ts b/plugins/_lib/nativeBridge/native/request/requestSegmentsStream.native.ts similarity index 86% rename from plugins/_lib/fetch/requestSegmentsStream.ts rename to plugins/_lib/nativeBridge/native/request/requestSegmentsStream.native.ts index 13bc60e0..8e9ac7fe 100644 --- a/plugins/_lib/fetch/requestSegmentsStream.ts +++ b/plugins/_lib/nativeBridge/native/request/requestSegmentsStream.native.ts @@ -1,8 +1,6 @@ -import type { Readable } from "stream"; -import { FetchyOptions, requestStream, rejectNotOk, parseTotal } from "."; - -import type stream from "stream"; -const { Transform, PassThrough } = require("stream"); +import { type Readable, PassThrough } from "stream"; +import { FetchyOptions, rejectNotOk, parseTotal } from "./helpers.native"; +import { requestStream } from "./requestStream.native"; export const requestSegmentsStream = async (segments: string[], options: FetchyOptions = {}) => new Promise(async (resolve, reject) => { diff --git a/plugins/_lib/nativeBridge/native/request/requestStream.native.ts b/plugins/_lib/nativeBridge/native/request/requestStream.native.ts new file mode 100644 index 00000000..424cb681 --- /dev/null +++ b/plugins/_lib/nativeBridge/native/request/requestStream.native.ts @@ -0,0 +1,18 @@ +import type { IncomingMessage } from "http"; +import { RequestOptions, request } from "https"; + +export type ExtendedRequestOptions = RequestOptions & { body?: string; poke?: true }; +export const requestStream = (url: string, options: ExtendedRequestOptions = {}) => + new Promise((resolve, reject) => { + const body = options.body; + delete options.body; + const req = request(url, options, (res) => { + const statusMsg = res.statusMessage !== "" ? ` - ${res.statusMessage}` : ""; + console.debug(`[${res.statusCode}${statusMsg}] (${req.method})`, url, res); + if (options.poke) req.destroy(); + resolve(res); + }); + req.on("error", reject); + if (body !== undefined) req.write(body); + req.end(); + }); diff --git a/plugins/_lib/nativeBridge/native/request/requestTrack.native.ts b/plugins/_lib/nativeBridge/native/request/requestTrack.native.ts new file mode 100644 index 00000000..9e2587ee --- /dev/null +++ b/plugins/_lib/nativeBridge/native/request/requestTrack.native.ts @@ -0,0 +1,27 @@ +import type { Readable } from "stream"; +import type { FetchyOptions } from "./helpers.native"; + +import { type ExtendedPlayackInfo, ManifestMimeType } from "../../../Caches/PlaybackInfoTypes"; +import { requestDecodedStream } from "./requestDecodedStream.native"; +import { requestSegmentsStream } from "./requestSegmentsStream.native"; + +export type ExtendedPlaybackInfoWithBytes = ExtendedPlayackInfo & { stream: Readable }; +export const requestTrackStream = async ({ manifestMimeType, manifest }: ExtendedPlayackInfo, fetchyOptions: FetchyOptions): Promise => { + switch (manifestMimeType) { + case ManifestMimeType.Tidal: { + const stream = await requestDecodedStream(manifest.urls[0], { ...fetchyOptions, manifest }); + return stream; + } + case ManifestMimeType.Dash: { + const trackManifest = manifest.tracks.audios[0]; + const stream = await requestSegmentsStream( + trackManifest.segments.map((segment) => segment.url), + fetchyOptions + ); + return stream; + } + default: { + throw new Error(`Unsupported Stream Info manifest mime type: ${manifestMimeType}`); + } + } +}; diff --git a/plugins/_lib/nativeBridge/nativeBridge.d.ts b/plugins/_lib/nativeBridge/nativeBridge.d.ts new file mode 100644 index 00000000..4a2769aa --- /dev/null +++ b/plugins/_lib/nativeBridge/nativeBridge.d.ts @@ -0,0 +1,6 @@ +import * as nativeBridge from "./native"; + +// Augment the module with the inferred types +declare module "." { + export = nativeBridge; +} diff --git a/plugins/_lib/nativeBridge/nativeBridge.native.ts b/plugins/_lib/nativeBridge/nativeBridge.native.ts new file mode 100644 index 00000000..555e1527 --- /dev/null +++ b/plugins/_lib/nativeBridge/nativeBridge.native.ts @@ -0,0 +1,5 @@ +import * as nativeBridge from "./native"; +// @ts-ignore +electron.ipcMain.removeAllListeners("___nativeBridge___"); +// @ts-ignore +electron.ipcMain.handle("___nativeBridge___", (_, method: string, ...args) => nativeBridge[method](...args)); diff --git a/plugins/_lib/trackBytes/decryptBuffer.ts b/plugins/_lib/trackBytes/decryptBuffer.ts deleted file mode 100644 index a6dec7df..00000000 --- a/plugins/_lib/trackBytes/decryptBuffer.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { DecryptedKey } from "./decryptKeyId"; -import type crypto from "crypto"; -const { createDecipheriv } = require("crypto"); - -// Extend nonce to 16 bytes (nonce + counter) -export const makeDecipheriv = async ({ key, nonce }: DecryptedKey) => createDecipheriv("aes-128-ctr", key, Buffer.concat([nonce, Buffer.alloc(8, 0)])); diff --git a/plugins/_lib/trackBytes/download.ts b/plugins/_lib/trackBytes/download.ts deleted file mode 100644 index 53a5b359..00000000 --- a/plugins/_lib/trackBytes/download.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { ExtendedPlayackInfo, PlaybackInfoCache, ManifestMimeType, TidalManifest } from "../Caches/PlaybackInfoCache"; -import { makeDecipheriv } from "./decryptBuffer"; -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"; - -export type TrackOptions = { - trackId: number; - desiredQuality: AudioQuality; -}; - -export type ExtendedPlaybackInfoWithBytes = ExtendedPlayackInfo & { stream: Readable }; - -export interface DownloadTrackOptions extends FetchyOptions { - playbackInfo?: ExtendedPlayackInfo; -} - -const makeGetDeciper = (manifest: TidalManifest) => { - switch (manifest.encryptionType) { - case "OLD_AES": { - return () => makeDecipheriv(decryptKeyId(manifest.keyId)); - } - case "NONE": { - return undefined; - } - default: { - throw new Error(`Unexpected manifest encryption type ${manifest.encryptionType}`); - } - } -}; - -export const fetchTrack = async ({ trackId, desiredQuality }: TrackOptions, options?: DownloadTrackOptions): Promise => { - const { playbackInfo, manifest, manifestMimeType } = options?.playbackInfo ?? (await PlaybackInfoCache.ensure(trackId, desiredQuality)); - - switch (manifestMimeType) { - case ManifestMimeType.Tidal: { - const stream = await requestDecodedStream(manifest.urls[0], { ...options, getDecipher: makeGetDeciper(manifest) }); - return { playbackInfo, manifest, manifestMimeType, stream }; - } - case ManifestMimeType.Dash: { - const trackManifest = manifest.tracks.audios[0]; - const stream = await requestSegmentsStream( - trackManifest.segments.map((segment) => segment.url), - options - ); - return { playbackInfo, manifest, manifestMimeType, stream }; - } - } -};