Skip to content

Commit

Permalink
SongDownloader - Basic native functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
Inrixia committed Jun 25, 2024
1 parent ee9e421 commit 19887c8
Show file tree
Hide file tree
Showing 13 changed files with 830 additions and 65 deletions.
731 changes: 731 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"devDependencies": {
"@types/node": "^20.12.7",
"concurrently": "^8.2.2",
"electron": "^31.0.2",
"esbuild": "^0.20.2",
"http-server": "^14.1.1",
"neptune-types": "^1.0.0",
Expand Down
19 changes: 6 additions & 13 deletions plugins/NoBuffer/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { intercept, store } from "@neptune";
import { TrackItemCache } from "@inrixia/lib/Caches/TrackItemCache";
import { intercept } from "@neptune";

// 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 { voidTrack } from "@inrixia/lib/nativeBridge";
import { PlaybackInfoCache } from "@inrixia/lib/Caches/PlaybackInfoCache";
const trace = Tracer("[NoBuffer]");

let unblocking = false;
Expand All @@ -15,15 +14,9 @@ export const onUnload = intercept("playbackControls/SET_PLAYBACK_STATE", ([state
unblocking = true;
(async () => {
if (playbackContext === undefined) return;
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 voidStream = new Writable({
write: (_: any, __: any, cb: () => void) => cb(),
});
// stream.pipe(voidStream);
await new Promise((res) => voidStream.on("end", res));
const { actualProductId, actualAudioQuality } = playbackContext;
trace.msg.log(`Playback stalled... Kicking tidal CDN!`);
await voidTrack(await PlaybackInfoCache.ensure(+actualProductId, actualAudioQuality));
unblocking = false;
})();
}
Expand Down
14 changes: 10 additions & 4 deletions plugins/SongDownloader/src/Settings.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import { html } from "@neptune/voby";
import { getSettings } from "@inrixia/lib/storage";
import { AudioQualityInverse, AudioQuality, validQualitiesSettings } from "@inrixia/lib/AudioQualityTypes";
import { AudioQuality, validQualitiesSettings } from "@inrixia/lib/AudioQualityTypes";
import { DropdownSelect } from "@inrixia/lib/components/DropdownSelect";
import { TextInput } from "@inrixia/lib/components/TextInput";
import { SwitchSetting } from "@inrixia/lib/components/SwitchSetting";

export const settings = getSettings({
desiredDownloadQuality: AudioQuality.HiRes,
defaultDownloadPath: "",
alwaysUseDefaultPath: true,
});
export const Settings = () => html`<div>
<${DropdownSelect}
selected=${settings.desiredDownloadQuality}
onSelect=${(selected: AudioQuality) => (settings.desiredDownloadQuality = selected)}
options=${validQualitiesSettings}
title="Download Quality"
title="Download quality"
/>
<${TextInput} text=${settings.defaultDownloadPath} onText=${(text: string) => (settings.defaultDownloadPath = text)} title="Default save path" />
<${SwitchSetting}
checked=${settings.defaultDownloadPath !== "" && settings.alwaysUseDefaultPath}
onClick=${() => (settings.alwaysUseDefaultPath = !settings.alwaysUseDefaultPath)}
title="Always use default save path"
/>
<${TextInput} text=${settings.defaultDownloadPath} onText=${(text: string) => (settings.defaultDownloadPath = text)} title="Download Path" />
Specifying download path to save to will disable download prompt and save all files to the specified path.
</div>`;
55 changes: 38 additions & 17 deletions plugins/SongDownloader/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import "./styles";

import { TrackItem } from "neptune-types/tidal";
import { saveFile, saveFileNode } from "./lib/saveFile";

import { addMetadata } from "./addMetadata";
import { fileNameFromInfo } from "./lib/fileName";
import { parseExtension, parseFileName } from "./parseFileName";

import { Tracer } from "@inrixia/lib/trace";
const trace = Tracer("[SongDownloader]");
Expand All @@ -13,6 +10,8 @@ import safeUnload from "@inrixia/lib/safeUnload";

import { settings } from "./Settings";
import { ContextMenu } from "@inrixia/lib/ContextMenu";
import { PlaybackInfoCache } from "@inrixia/lib/Caches/PlaybackInfoCache";
import { downloadTrackStream, openDialog, saveDialog } from "@inrixia/lib/nativeBridge";
export { Settings } from "./Settings";

type DownloadButtoms = Record<string, HTMLButtonElement>;
Expand All @@ -21,6 +20,7 @@ const downloadButtons: DownloadButtoms = {};
interface ButtonMethods {
prep(): void;
onProgress(info: { total: number; downloaded: number; percent: number }): void;
set(textContent: string): void;
clear(): void;
}

Expand All @@ -38,6 +38,10 @@ const buttonMethods = (id: string): ButtonMethods => ({
const totalMB = (total / 1048576).toFixed(0);
downloadButton.textContent = `Downloading... ${downloadedMB}/${totalMB}MB ${percent.toFixed(0)}%`;
},
set: (textContent: string) => {
const downloadButton = downloadButtons[id];
downloadButton.textContent = textContent;
},
clear: () => {
const downloadButton = downloadButtons[id];
downloadButton.classList.remove("loading");
Expand Down Expand Up @@ -67,24 +71,41 @@ ContextMenu.onOpen(async (contextSource, contextMenu, trackItems) => {
downloadButton.classList.add("loading");
}
downloadButtons[context] = downloadButton;
const { prep, onProgress, clear } = buttonMethods(context);
const updateMethods = buttonMethods(context);
contextMenu.appendChild(downloadButton);

downloadButton.addEventListener("click", async () => {
if (context === undefined) return;
prep();
let filePath: string | undefined;
const defaultPath = settings.defaultDownloadPath !== "" ? settings.defaultDownloadPath : undefined;
if (trackItems.length > 1 && !(settings.alwaysUseDefaultPath && defaultPath !== undefined)) {
updateMethods.set("Prompting for download folder...");
const dialogResult = await openDialog({ properties: ["openDirectory", "createDirectory"], defaultPath });
filePath = dialogResult.filePaths[0];
}
updateMethods.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, updateMethods, filePath).catch(trace.msg.err.withContext("Error downloading track"));
}
clear();
updateMethods.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);
// };
const downloadTrack = async (track: TrackItem, updateMethods: ButtonMethods, filePath?: string) => {
updateMethods.set("Fetching playback info...");
const playbackInfo = await PlaybackInfoCache.ensure(track.id!, settings.desiredDownloadQuality);
const fileName = parseFileName(track, playbackInfo);
if (filePath !== undefined) {
filePath = `${filePath}\\${fileName}`;
} else {
updateMethods.set("Prompting for download path...");
const defaultPath = settings.defaultDownloadPath !== "" ? `${settings.defaultDownloadPath}\\${fileName}` : `${fileName}`;
const dialogResult = await saveDialog({ defaultPath, filters: [{ name: "", extensions: [parseExtension(fileName) ?? "*"] }] });
filePath = dialogResult?.filePath;
}
console.log(filePath);
updateMethods.set("Downloading...");
await downloadTrackStream(playbackInfo, filePath);
console.log("Done!");
};
29 changes: 0 additions & 29 deletions plugins/SongDownloader/src/lib/saveFile.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { TrackItem } from "neptune-types/tidal";
import { type ExtendedPlayackInfo, ManifestMimeType } from "@inrixia/lib/Caches/PlaybackInfoTypes";
import { fullTitle } from "@inrixia/lib/fullTitle";

export const fileNameFromInfo = (track: TrackItem, { manifest, manifestMimeType }: ExtendedPlayackInfo): string => {
const unsafeCharacters = /[\/:*?"<>|]/g;
const sanitizeFilename = (filename: string): string => filename.replace(unsafeCharacters, "_");

export const parseExtension = (filename: string) => filename.match(/\.([0-9a-z]+)(?:[\?#]|$)/i)?.[1] ?? undefined;
const fileNameFromInfo = (track: TrackItem, { manifest, manifestMimeType }: ExtendedPlayackInfo): string => {
const artistName = track.artists?.[0].name ?? "Unknown Artist";
const albumName = track.album?.title ?? "Unknown Album";
const title = fullTitle(track);
Expand All @@ -20,3 +24,4 @@ export const fileNameFromInfo = (track: TrackItem, { manifest, manifestMimeType
}
}
};
export const parseFileName = (track: TrackItem, extPlaybackInfo: ExtendedPlayackInfo) => sanitizeFilename(fileNameFromInfo(track, extPlaybackInfo));
4 changes: 4 additions & 0 deletions plugins/_lib/nativeBridge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ export const getTrackInfo = invoke("getTrackInfo");
export const parseDasha = invoke("parseDasha");
export const requestJson = invoke("requestJson");
export const hash = invoke("hash");
export const voidTrack = invoke("voidTrack");
export const downloadTrackStream = invoke("downloadTrackStream");
export const saveDialog = invoke("saveDialog");
export const openDialog = invoke("openDialog");
8 changes: 8 additions & 0 deletions plugins/_lib/nativeBridge/native/downloadTrack.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { requestTrackStream } from "./request/requestTrack.native";
import type { ExtendedPlayackInfo } from "../../Caches/PlaybackInfoTypes";
import { createWriteStream } from "fs";

export const downloadTrackStream = async (extPlaybackInfo: ExtendedPlayackInfo, filePath: string): Promise<void> => {
const stream = await requestTrackStream(extPlaybackInfo);
return new Promise((res) => stream.pipe(createWriteStream(filePath)).on("finish", res));
};
6 changes: 6 additions & 0 deletions plugins/_lib/nativeBridge/native/electron.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { OpenDialogOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogReturnValue } from "electron";
export type { OpenDialogOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogReturnValue };
// @ts-expect-error
const _dialog = electron.dialog;
export const openDialog = (openDialogOptions?: OpenDialogOptions): OpenDialogReturnValue => _dialog.showOpenDialog(openDialogOptions);
export const saveDialog = (openDialogOptions?: SaveDialogOptions): SaveDialogReturnValue => _dialog.showSaveDialog(openDialogOptions);
3 changes: 3 additions & 0 deletions plugins/_lib/nativeBridge/native/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ export * from "./request";
export * from "./dasha.native";
export * from "./getTrackInfo.native";
export * from "./crypto.native";
export * from "./voidTrack.native";
export * from "./downloadTrack.native";
export * from "./electron.native";
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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<Readable> => {
export const requestTrackStream = async ({ manifestMimeType, manifest }: ExtendedPlayackInfo, fetchyOptions: FetchyOptions = {}): Promise<Readable> => {
switch (manifestMimeType) {
case ManifestMimeType.Tidal: {
const stream = await requestDecodedStream(manifest.urls[0], { ...fetchyOptions, manifest });
Expand Down
16 changes: 16 additions & 0 deletions plugins/_lib/nativeBridge/native/voidTrack.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Writable } from "stream";
import { requestTrackStream } from "./request/requestTrack.native";
import type { ExtendedPlayackInfo } from "../../Caches/PlaybackInfoTypes";

export const voidTrack = (extPlaybackInfo: ExtendedPlayackInfo): Promise<void> =>
new Promise((res) =>
requestTrackStream(extPlaybackInfo).then((stream) =>
stream
.pipe(
new Writable({
write: (_: any, __: any, cb: () => void) => cb(),
})
)
.on("end", res)
)
);

0 comments on commit 19887c8

Please sign in to comment.