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
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import "./env";
|
||||
import { app, BrowserWindow, components, dialog, ipcMain, session, shell } from "electron";
|
||||
import { join } from "node:path";
|
||||
import { startApiServer } from "./apiServer";
|
||||
import { PORT } from "./constants";
|
||||
import { applySettings, loadSettings, saveSettings } from "./settings";
|
||||
|
||||
const APP_URL = `http://127.0.0.1:${PORT}`;
|
||||
const LOGIN_HOSTS = ["spotify.com", "google.com", "facebook.com", "apple.com"];
|
||||
let mainWindow: BrowserWindow | undefined;
|
||||
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on("second-instance", () => {
|
||||
if (!mainWindow) return;
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.focus();
|
||||
});
|
||||
app.whenReady().then(start);
|
||||
}
|
||||
|
||||
async function start() {
|
||||
app.userAgentFallback = app.userAgentFallback.replace(/ (Electron|aurora-spotirip|Aurora SpotiRip)\/\S+/g, "");
|
||||
applySettings();
|
||||
registerIpc();
|
||||
|
||||
try {
|
||||
await startApiServer(PORT, join(app.getAppPath(), "dist"), { onSpotifyLogin: adoptSpotifyLogin });
|
||||
} catch (error) {
|
||||
const inUse = (error as NodeJS.ErrnoException).code === "EADDRINUSE";
|
||||
dialog.showErrorBox(
|
||||
"Aurora SpotiRip could not start",
|
||||
inUse
|
||||
? `Port ${PORT} is already in use by another program. Close it and start Aurora SpotiRip again.`
|
||||
: String(error),
|
||||
);
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await components.whenReady();
|
||||
} catch (error) {
|
||||
console.error("Widevine is unavailable; in-app playback will use Spotify Connect instead.", error);
|
||||
}
|
||||
|
||||
createWindow();
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1360,
|
||||
height: 880,
|
||||
minWidth: 960,
|
||||
minHeight: 600,
|
||||
backgroundColor: "#0f1318",
|
||||
title: "Aurora SpotiRip",
|
||||
icon: join(app.getAppPath(), "build", "icon.png"),
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, "preload.cjs"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
shell.openExternal(url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
mainWindow.webContents.on("will-navigate", (event, url) => {
|
||||
const { origin, hostname, pathname } = new URL(url);
|
||||
if (origin === APP_URL && pathname === "/_api/spotify-auth") {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
return;
|
||||
}
|
||||
if (origin === APP_URL || LOGIN_HOSTS.some((host) => hostname === host || hostname.endsWith(`.${host}`))) return;
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
});
|
||||
|
||||
mainWindow.webContents.on("console-message", (event) => {
|
||||
if (event.level === "warning" || event.level === "error") {
|
||||
console.log(`[page ${event.level}] ${event.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.loadURL(APP_URL);
|
||||
mainWindow.on("closed", () => {
|
||||
mainWindow = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
async function adoptSpotifyLogin(setCookies: string[]) {
|
||||
for (const header of setCookies) {
|
||||
const [pair, ...attributes] = header.split(";").map((part) => part.trim());
|
||||
const index = pair.indexOf("=");
|
||||
const name = pair.slice(0, index);
|
||||
const value = decodeURIComponent(pair.slice(index + 1));
|
||||
const maxAge = Number(attributes.find((attr) => /^max-age=/i.test(attr))?.split("=")[1]);
|
||||
if (!name || name === "spotirip_oauth_state") continue;
|
||||
await session.defaultSession.cookies.set({
|
||||
url: APP_URL,
|
||||
name,
|
||||
value,
|
||||
path: "/",
|
||||
secure: true,
|
||||
httpOnly: attributes.some((attr) => /^httponly$/i.test(attr)),
|
||||
sameSite: "lax",
|
||||
...(Number.isFinite(maxAge) ? { expirationDate: Date.now() / 1000 + maxAge } : {}),
|
||||
});
|
||||
}
|
||||
if (!mainWindow) createWindow();
|
||||
else {
|
||||
mainWindow.loadURL(APP_URL);
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function registerIpc() {
|
||||
ipcMain.handle("settings:get-download-folder", () => loadSettings().downloadFolder);
|
||||
ipcMain.handle("settings:choose-download-folder", async () => {
|
||||
const result = await dialog.showOpenDialog(mainWindow!, {
|
||||
title: "Choose download folder",
|
||||
defaultPath: loadSettings().downloadFolder,
|
||||
properties: ["openDirectory", "createDirectory"],
|
||||
});
|
||||
if (result.canceled || !result.filePaths[0]) return undefined;
|
||||
return saveSettings({ downloadFolder: result.filePaths[0] }).downloadFolder;
|
||||
});
|
||||
ipcMain.handle("shell:show-in-folder", (_event, path: string) => shell.showItemInFolder(path));
|
||||
}
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") app.quit();
|
||||
});
|
||||
Reference in New Issue
Block a user