Files
spotify-gui-electron/main.js
2025-04-08 14:44:20 -07:00

455 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { app, BrowserWindow, ipcMain, screen } = require('electron');
const { exec } = require('child_process');
const https = require('https');
const path = require('path');
let win;
let lastSong = "";
let lastPaused = true;
if (process.platform === 'win32') {
getMediaCommand = 'powershell -ExecutionPolicy Bypass -File get-media.ps1';
mediaControlCommand = 'MediaControl.exe';
} else if (process.platform === 'darwin') {
getMediaCommand = './get-media.sh'; // Adjust this path/command to your macOS script
mediaControlCommand = 'osascript MediaControl.scpt';
} else {
console.error("Unsupported OS");
resolve({ updated: false });
return;
}
app.whenReady().then(() => {
createWindow();
checkNowPlaying();
setInterval(checkNowPlaying, 2500);
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
function createWindow() {
const displays = screen.getAllDisplays();
const externalDisplay = displays[3] || displays[0];
const x = externalDisplay.bounds.x;
const y = externalDisplay.bounds.y;
win = new BrowserWindow({
x: x,
y: y,
fullscreen: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
}
});
win.removeMenu();
// win.webContents.openDevTools();
win.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(getMainPageHTML()));
win.on('closed', () => {
win = null;
});
}
async function checkNowPlaying() {
const result = await checkOwnSong();
if (result.updated && win) {
win.webContents.send('song-update', result.data);
}
}
function checkOwnSong() {
return new Promise((resolve) => {
exec(getMediaCommand, async (error, stdout) => {
if (error) {
console.error("PowerShell error:", error);
resolve({ updated: false });
return;
}
try {
const data = JSON.parse(stdout);
// If there's NO valid data => user is paused/stopped
if (!data.title && !data.artist) {
// If we weren't already paused, now we are => update
if (!lastPaused) {
lastPaused = true;
resolve({
updated: true,
data: { paused: true }
});
} else {
// Still paused, no change
resolve({ updated: false });
}
} else {
// We DO have valid song data => the user is playing something
const currentSong = `${data.title} - ${data.artist}`;
// If we were paused, or the song changed, send an update
if (lastPaused || currentSong !== lastSong) {
lastPaused = false;
lastSong = currentSong;
const albumArt = await fetchAlbumArt(data.title, data.artist);
resolve({
updated: true,
data: {
paused: false,
title: data.title,
artist: data.artist,
albumArt
}
});
} else {
// No change in paused/playing state, no change in track
resolve({ updated: false });
}
}
} catch (err) {
console.error("JSON parse error:", stdout, err);
resolve({ updated: false });
}
});
});
}
function fetchAlbumArt(title, artist) {
return new Promise((resolve) => {
const query = encodeURIComponent(`${artist} ${title}`);
const url = `https://itunes.apple.com/search?term=${query}&limit=1`;
https.get(url, (res) => {
let body = '';
res.on('data', chunk => (body += chunk));
res.on('end', () => {
try {
const results = JSON.parse(body).results;
if (results.length > 0) {
// Replace 100x100 with 300x300 for better quality
const art = results[0].artworkUrl100.replace('100x100bb', '300x300bb');
resolve(art);
} else {
resolve("https://via.placeholder.com/100");
}
} catch (e) {
console.error("Album art fetch error:", e);
resolve("https://via.placeholder.com/100");
}
});
}).on('error', () => {
resolve("https://via.placeholder.com/100");
});
});
}
ipcMain.on('media-control', (event, command) => {
if (command) {
exec(`${mediaControlCommand} ${command}`, (error, stdout, stderr) => {
if (error) {
console.error(`Error running MediaControl.exe:`, error);
} else {
console.log(`Media command executed: ${stdout || command}`);
}
});
}
});
ipcMain.on('minimize-app', (event) => {
const window = BrowserWindow.getFocusedWindow();
if (window) window.minimize();
});
ipcMain.on('close-app', (event) => {
// If you have a reference to the BrowserWindow (e.g. mainWindow),
// you can do something like:
win.close();
// or app.quit();
});
function getMainPageHTML() {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Now Playing</title>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #121212;
color: #ffffff;
overflow: hidden; /* hide scrollbars if the card moves */
}
/* Close and minimize buttons (top-right of the window) */
.close-button,
.minimize-button {
position: fixed;
top: 10px;
background: none;
border: none;
color: #ffffff;
font-size: 24px;
cursor: pointer;
opacity: 0; /* hidden by default */
transition: opacity 0.3s ease;
z-index: 9999; /* ensure it's on top */
pointer-events: auto; /* ensure clickability */
}
.minimize-button {
right: 50px; /* place it left of the close button */
}
.close-button {
right: 10px;
}
/* Show the buttons when hovering anywhere on the body */
body:hover .close-button,
body:hover .minimize-button {
opacity: 1;
}
/* Container that spans the full window */
.container {
position: relative;
width: 100vw;
height: 100vh;
}
/* The card is centered and will have transitions */
.card {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #1e1e1e;
border-radius: 16px;
padding: 50px 60px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
/* Center content vertically and horizontally */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
min-width: 700px;
max-width: 900px;
/* We'll allow a smooth transition for the content inside it */
transition: top 0.3s ease, transform 0.3s ease;
}
/* When .container has class .show-lyrics, move the card closer to the top. */
.container.show-lyrics .card {
top: 20%;
transform: translate(-50%, -20%);
}
/* The info section (album art + text) */
.info {
display: flex;
align-items: center;
gap: 40px;
transition: transform 0.3s ease;
}
/* Slide the .info section up when the body is hovered */
body:hover .info {
transform: translateY(-20px);
}
.album-art {
width: 150px;
height: 150px;
background: #333;
border-radius: 12px;
object-fit: cover;
}
.text-info {
display: flex;
flex-direction: column;
justify-content: center;
text-align: left;
}
.title {
font-size: 32px;
font-weight: bold;
margin-bottom: 8px;
}
.artist {
font-size: 28px;
color: #bbbbbb;
}
/* Hide controls by default; show them on hover of the body */
.controls {
display: flex;
gap: 20px;
/* Hidden initially */
opacity: 0;
pointer-events: none;
transform: translateY(20px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
body:hover .controls {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
.controls button {
background-color: #333;
border: none;
color: white;
padding: 10px 16px;
font-size: 24px;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.controls button:hover {
background-color: #555;
}
/* Single-line lyrics container at the bottom (hidden by default). */
.lyrics {
position: absolute;
bottom: 100px; /* push it above the arrow */
width: 100%;
text-align: center;
font-size: 24px;
color: #bbbbbb;
opacity: 0;
transition: opacity 0.3s ease;
}
/* When .container is .show-lyrics, reveal the lyrics. */
.container.show-lyrics .lyrics {
opacity: 1;
}
/* The arrow button at the bottom center of the window. */
.arrow-button {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: none;
border: none;
font-size: 36px;
color: #ffffff;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s ease;
}
/* Show the arrow button when hovering anywhere on body */
body:hover .arrow-button {
// opacity: 1;
opacity: 0;
}
</style>
</head>
<body>
<!-- Fixed minimize and close buttons in top-right -->
<button class="minimize-button" onclick="minimizeApp()"></button>
<button class="close-button" onclick="closeApp()">✕</button>
<div class="container" id="container">
<div class="card">
<div class="info">
<img class="album-art" id="albumArt" src="https://via.placeholder.com/150" alt="Album Art">
<div class="text-info">
<div class="title" id="songTitle">Loading...</div>
<div class="artist" id="songArtist"></div>
</div>
</div>
<div class="controls">
<button onclick="sendControl('previous')">⏮</button>
<button id="playPauseButton" onclick="sendControl('playpause')">⏯</button>
<button onclick="sendControl('next')">⏭</button>
</div>
</div>
<!-- Single line of lyrics that appears in the bottom half -->
<div class="lyrics" id="lyricsLine">
“Hello, is it me youre looking for?”
</div>
<!-- Arrow at the bottom -->
<button class="arrow-button" id="arrowButton" onclick="toggleLyrics()">▲</button>
</div>
<script>
const { ipcRenderer } = require('electron');
ipcRenderer.on('song-update', (event, data) => {
// Elements on the UI
const songTitleElem = document.getElementById('songTitle');
const songArtistElem = document.getElementById('songArtist');
const albumArtElem = document.getElementById('albumArt');
const playPauseButton = document.getElementById('playPauseButton');
if (data.paused) {
// Player is paused => show "Play" button
playPauseButton.textContent = "⏵︎";
} else {
// Player is playing => show "Pause" button and update track info
playPauseButton.textContent = "⏸︎";
songTitleElem.innerText = data.title ?? "Unknown Title";
songArtistElem.innerText = data.artist ?? "Unknown Artist";
albumArtElem.src = data.albumArt ?? "https://via.placeholder.com/150";
}
});
// Sends commands (previous, playpause, next) to main process
function sendControl(command) {
ipcRenderer.send('media-control', command);
}
function closeApp() {
ipcRenderer.send('close-app');
}
function minimizeApp() {
ipcRenderer.send('minimize-app');
}
// Toggle lyrics arrow logic
function toggleLyrics() {
const container = document.getElementById('container');
const arrowBtn = document.getElementById('arrowButton');
container.classList.toggle('show-lyrics');
if (container.classList.contains('show-lyrics')) {
arrowBtn.textContent = '▼';
} else {
arrowBtn.textContent = '▲';
}
}
</script>
</body>
</html>
`;
}