- 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
143 lines
4.5 KiB
TypeScript
143 lines
4.5 KiB
TypeScript
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();
|
|
});
|