Files
spotify-gui-electron/main-old.js
2025-07-25 16:11:37 -07:00

543 lines
19 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 http = require('http');
const path = require('path');
let win;
let lastSong = "";
let lastPaused = true;
app.whenReady().then(() => {
createWindow();
checkNowPlaying();
setInterval(checkNowPlaying, 2500);
// Start polling for Plex now-playing details
checkPlexNowPlaying();
setInterval(checkPlexNowPlaying, 3000);
// Start an HTTP server to receive notification requests (POST /notify)
http.createServer((req, res) => {
if(req.method === 'POST' && req.url === '/notify'){
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const notifData = JSON.parse(body);
if(win){
win.webContents.send('notification', notifData);
}
res.writeHead(200, {"Content-Type": "application/json"});
res.end(JSON.stringify({status:"ok"}));
} catch(e){
console.error("Notification parse error:", e);
res.writeHead(400);
res.end();
}
});
} else {
res.writeHead(404);
res.end();
}
}).listen(3000, () => {
console.log("Notification server listening on port 3000");
});
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(`powershell -ExecutionPolicy Bypass -File get-media.ps1`, 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 checkPlexNowPlaying() {
const plexToken = "kbGwoiA_QEGzw7MgSZrY"; // <-- replace with your Plex token
const plexHost = "spyro.corp.bbrunson.com"; // <-- adjust if needed
const url = `http://${plexHost}:32400/status/sessions?X-Plex-Token=${plexToken}`;
http.get(url, (res) => {
let data = "";
res.on('data', chunk => data += chunk);
res.on('end', () => {
// For simplicity we check for a known tag in the XML response.
let nowPlaying = "No media playing";
if(data.includes("MediaContainer") && data.includes("Video")) {
nowPlaying = "Plex is playing media";
}
if(win){
win.webContents.send('plex-update', { details: nowPlaying });
}
});
}).on('error', err => {
console.error("Error fetching Plex now playing:", err);
});
}
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(`MediaControl.exe ${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) => {
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;
}
/* Close and minimize buttons styles... */
.close-button,
.minimize-button {
position: fixed;
top: 10px;
background: none;
border: none;
color: #ffffff;
font-size: 24px;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 9999;
pointer-events: auto;
}
.minimize-button { right: 50px; }
.close-button { right: 10px; }
body:hover .close-button,
body:hover .minimize-button {
opacity: 1;
}
/* Top bar (Plex or Notification) */
#plexSection {
position: fixed;
top: 0;
width: 100%;
background: #333;
color: #fff;
padding: 10px;
text-align: center;
z-index: 1000;
}
/* Container */
.container {
position: relative;
width: 100vw;
height: 100vh;
}
/* Card styles note the constant translate for centering */
.card {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -60%);
background-color: #1e1e1e;
border-radius: 16px;
padding: 50px 60px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
min-width: 700px;
max-width: 900px;
transition: top 0.3s ease;
}
/* New fade animations (only opacity changes) */
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-out {
animation: fadeOut 0.3s ease-out forwards;
}
.fade-in {
animation: fadeIn 0.3s ease-out forwards;
}
/* Show lyrics state */
.container.show-lyrics .card {
top: 20%;
transform: translate(-50%, -20%);
}
/* Info and album art */
.info {
display: flex;
align-items: center;
gap: 40px;
transition: transform 0.3s ease;
}
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; }
/* Controls */
.controls {
display: flex;
gap: 20px;
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; }
/* Lyrics */
.lyrics {
position: absolute;
bottom: 100px;
width: 100%;
text-align: center;
font-size: 24px;
color: #bbbbbb;
opacity: 0;
transition: opacity 0.3s ease;
}
.container.show-lyrics .lyrics { opacity: 1; }
/* Arrow button */
.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;
}
body:hover .arrow-button { opacity: 0; }
/* Notification overlay */
.notification-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
color: #fff;
text-align: center;
z-index: 10000;
opacity: 0;
transition: opacity 0.5s ease;
pointer-events: none;
}
.notification-overlay.show {
opacity: 1;
pointer-events: auto;
}
</style>
</head>
<body>
<!-- Top bar for Plex Now Playing or Notification -->
<div id="plexSection">
Plex Now Playing: <span id="plexNowPlaying">Loading...</span>
</div>
<!-- Fixed minimize and close buttons -->
<button class="minimize-button" onclick="minimizeApp()"></button>
<button class="close-button" onclick="closeApp()">✕</button>
<div class="container" id="container" style="padding-top: 60px;">
<div class="card" id="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>
<div class="lyrics" id="lyricsLine">
“Hello, is it me youre looking for?”
</div>
<button class="arrow-button" id="arrowButton" onclick="toggleLyrics()">▲</button>
</div>
<div class="notification-overlay" id="notificationOverlay">
<div class="notification-content">
<h1 id="notificationTitle"></h1>
<p id="notificationMessage"></p>
</div>
</div>
<script>
const { ipcRenderer } = require('electron');
let notificationActive = false;
let notificationTimeoutId = null;
ipcRenderer.on('song-update', (event, data) => {
const songTitleElem = document.getElementById('songTitle');
const songArtistElem = document.getElementById('songArtist');
const albumArtElem = document.getElementById('albumArt');
const playPauseButton = document.getElementById('playPauseButton');
const cardElem = document.getElementById('card');
if (data.paused) {
playPauseButton.textContent = "▶️";
} else {
playPauseButton.textContent = "⏸️";
// Start fade-out animation
cardElem.classList.remove('fade-in', 'fade-out');
cardElem.classList.add('fade-out');
cardElem.addEventListener('animationend', function handler(e) {
if (e.animationName === 'fadeOut') {
// Update the content once fade-out completes
songTitleElem.innerText = data.title ?? "Unknown Title";
songArtistElem.innerText = data.artist ?? "Unknown Artist";
albumArtElem.src = data.albumArt ?? "https://via.placeholder.com/150";
// Fade the card in with the new data
cardElem.classList.remove('fade-out');
cardElem.classList.add('fade-in');
cardElem.removeEventListener('animationend', handler);
}
});
}
});
ipcRenderer.on('notification', (event, data) => {
const overlay = document.getElementById('notificationOverlay');
const titleElem = document.getElementById('notificationTitle');
const messageElem = document.getElementById('notificationMessage');
titleElem.innerText = data.title || "Notification";
messageElem.innerText = data.message || "";
overlay.classList.add('show');
// Hide the overlay after 5 seconds (unchanged)
setTimeout(() => {
overlay.classList.remove('show');
}, 5000);
// Show notification on top bar for 60 seconds and reposition card
const plexSection = document.getElementById('plexSection');
notificationActive = true;
plexSection.innerHTML = (data.title || "Notification") + ' - ' + (data.message || "");
plexSection.style.display = "block";
// Clear any pre-existing timer for top bar notification
if (notificationTimeoutId) {
clearTimeout(notificationTimeoutId);
}
notificationTimeoutId = setTimeout(() => {
plexSection.style.display = "none";
notificationActive = false;
// Re-position the card in the middle of the screen
const cardElem = document.getElementById('card');
cardElem.style.top = "50%";
cardElem.style.transform = "translate(-50%, -60%)";
}, 60000);
});
// Prevent plex updates while a notification is actively displayed on the top bar
ipcRenderer.on('plex-update', (event, data) => {
if(!notificationActive){
const plexElem = document.getElementById('plexNowPlaying');
plexElem.textContent = data.details;
}
});
function sendControl(command) {
ipcRenderer.send('media-control', command);
}
function closeApp() {
ipcRenderer.send('close-app');
}
function minimizeApp() {
ipcRenderer.send('minimize-app');
}
function toggleLyrics() {
const container = document.getElementById('container');
const arrowBtn = document.getElementById('arrowButton');
container.classList.toggle('show-lyrics');
arrowBtn.textContent = container.classList.contains('show-lyrics') ? '▼' : '▲';
}
</script>
</body>
</html>
`;
}