543 lines
19 KiB
JavaScript
543 lines
19 KiB
JavaScript
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 you’re 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>
|
||
`;
|
||
}
|