Files
Aurora-SpotiRip/helpers/useSpotifyDownload.tsx
R3ap3r 3178b8f4f7 Aurora SpotiRip desktop and web app
- Spotify playlist, library, album and artist browsing with in-app playback
- Song, playlist and album downloads via spotdl with YouTube and Bandcamp fallbacks
- Electron desktop app (castLabs Widevine) with a permanent sidebar layout
- Linux and Windows packaging scripts
2026-09-25 23:20:57 -05:00

104 lines
3.5 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { postSpotifyDownload, type InputType } from "../endpoints/spotify-download_POST.schema";
import {
getSpotifyDownloadStatus,
spotifyDownloadFileUrl,
} from "../endpoints/spotify-download-status_GET.schema";
import { getSpotifySession } from "../endpoints/spotify-session_GET.schema";
import { getDesktopBridge } from "./desktopBridge";
const POLL_INTERVAL_MS = 1500;
let canDownloadPromise: Promise<boolean> | undefined;
function loadCanDownload() {
canDownloadPromise ??= getSpotifySession()
.then((session) => {
const allowed = Boolean(session.connected && session.canDownload);
if (!allowed) canDownloadPromise = undefined;
return allowed;
})
.catch(() => {
canDownloadPromise = undefined;
return false;
});
return canDownloadPromise;
}
function saveFromServer(jobId: string) {
const anchor = document.createElement("a");
anchor.href = spotifyDownloadFileUrl(jobId);
anchor.rel = "noopener";
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
}
export function useSpotifyDownload() {
const [busyKeys, setBusyKeys] = useState<Set<string>>(new Set());
const [canDownload, setCanDownload] = useState(false);
const mounted = useRef(true);
useEffect(() => {
mounted.current = true;
loadCanDownload().then((allowed) => {
if (mounted.current) setCanDownload(allowed);
});
return () => {
mounted.current = false;
};
}, []);
function setBusy(key: string, busy: boolean) {
setBusyKeys((current) => {
const next = new Set(current);
if (busy) next.add(key);
else next.delete(key);
return next;
});
}
async function startDownload(key: string, label: string, request: InputType) {
if (busyKeys.has(key)) return;
setBusy(key, true);
const toastId = toast.loading(`Preparing ${label}…`);
try {
const { jobId } = await postSpotifyDownload(request);
while (mounted.current) {
await new Promise((resolve) => window.setTimeout(resolve, POLL_INTERVAL_MS));
const status = await getSpotifyDownloadStatus(jobId);
if (status.status === "failed") throw new Error(status.error || "Download failed.");
if (status.status === "ready") {
const skipped = status.failed ? ` (${status.failed} track${status.failed === 1 ? "" : "s"} could not be found)` : "";
const desktop = getDesktopBridge();
if (desktop && status.savedPath) {
const savedPath = status.savedPath;
toast.success(`Saved ${label}${skipped}`, {
id: toastId,
description: savedPath,
action: { label: "Show in folder", onClick: () => desktop.showInFolder(savedPath) },
});
} else {
saveFromServer(jobId);
toast.success(`Downloading ${status.fileName}${skipped}`, { id: toastId, description: undefined });
}
return;
}
if (status.total > 1) {
toast.loading(`Downloading ${label}: ${status.done} of ${status.total}`, {
id: toastId,
description: status.currentTitle,
});
}
}
toast.dismiss(toastId);
} catch (reason) {
toast.error(reason instanceof Error ? reason.message : "Download failed.", { id: toastId });
} finally {
if (mounted.current) setBusy(key, false);
}
}
return { busyKeys, canDownload, startDownload };
}