initial commit

This commit is contained in:
Brandon4466
2025-03-31 04:45:14 -07:00
commit ff59c4c0b8
2298 changed files with 478992 additions and 0 deletions

604
main.js Normal file
View File

@@ -0,0 +1,604 @@
const { app, BrowserWindow, ipcMain } = require('electron');
const { exec } = require('child_process');
const axios = require('axios');
const Store = require('electron-store').default; // Ensure .default is used for ES module compatibility if needed
const store = new Store();
let win;
let lastSong = "";
// Load stored otherUsername if available.
let otherUsername = store.get('otherUsername', "");
const username = "brandon"; // Your own username
const apiUrl = "https://ms.bbrunson.com"; // Your API endpoint
// ------------------------------
// Main Page HTML
// ------------------------------
// Function to generate main page HTML dynamically based on otherUsername state
function getMainPageHTML() {
return `<!DOCTYPE html>
<html>
<head>
<title>MusicShare Electron</title>
<style>
body { font-family: sans-serif; margin: 0; padding: 20px; background: #222; color: #fff; }
.header { display: flex; justify-content: space-between; align-items: center; }
h1 { margin: 0; }
/* Removed .gear style */
.container { display: flex; flex-direction: column; align-items: center; }
.song-container { position: relative; width: 300px; height: 300px; margin: 20px; background-size: cover; background-position: center; border: 2px solid #fff; } /* Default border */
.song-info { position: absolute; bottom: 0; width: 100%; background: rgba(0,0,0,0.5); padding: 10px; text-align: center; font-size: 16px; }
.pfp { position: absolute; top: 5px; left: 5px; width: 50px; height: 50px; border-radius: 50%; cursor: pointer; border: 3px solid #fff; /* Default border, width adjusted */ object-fit: cover; /* Ensure image covers the area */ background-color: #555; /* BG if image fails */ }
</style>
</head>
<body>
<div class="header">
</div>
<div class="container">
<div id="mySongContainer" class="song-container">
<img id="myProfilePic" class="pfp" src="${apiUrl}/pfp/${username}" alt="My Profile Picture" onerror="this.style.visibility='hidden';">
<div class="song-info" id="mySongInfo">Now Playing: Waiting for song...</div>
</div>
<div id="otherSongContainer" class="song-container">
${otherUsername ? `<img id="otherProfilePic" class="pfp" src="${apiUrl}/pfp/${otherUsername}" alt="Other Profile Picture" onerror="this.style.visibility='hidden';">` : '<div style="padding: 10px; text-align: center;">Click profile picture to set tracked user.</div>'}
<div class="song-info" id="otherSongInfo">${otherUsername ? "Other user's song: Loading..." : "No other user set"}</div>
</div>
</div>
<script>
const { ipcRenderer } = require('electron');
const localApiUrl = "${apiUrl}"; // Make apiUrl available in this script scope
const localUsername = "${username}";
let localOtherUsername = "${otherUsername || ''}"; // Keep track locally
// --- NEW: Function to fetch color and apply border ---
async function fetchAndApplyColor(user, elementId) {
if (!user) return; // Don't fetch if username is empty
const element = document.getElementById(elementId);
if (!element) return; // Don't fetch if element doesn't exist
try {
const response = await fetch(\`\${localApiUrl}/pfpColor/\${user}\`);
if (response.ok) {
const data = await response.json();
if (data && data.colorHexData) {
element.style.borderColor = data.colorHexData;
console.log(\`Set border color for \${user} (\${elementId}) to \${data.colorHexData}\`);
} else {
element.style.borderColor = '#fff'; // Reset to default if no color found
console.warn(\`No color found for \${user}, using default border.\`);
}
} else {
console.error(\`Failed to fetch color for \${user}. Status: \${response.status}\`);
element.style.borderColor = '#fff'; // Reset to default on error
}
} catch (error) {
console.error(\`Error fetching or applying color for \${user}:\`, error);
element.style.borderColor = '#fff'; // Reset to default on error
}
}
// --- END NEW ---
// --- Run color fetch on initial load ---
document.addEventListener('DOMContentLoaded', () => {
fetchAndApplyColor(localUsername, 'myProfilePic');
if (localOtherUsername) {
fetchAndApplyColor(localOtherUsername, 'otherProfilePic');
}
});
// --- END ---
// Open my profile page when my profile picture is clicked.
document.getElementById('myProfilePic').addEventListener('click', () => {
ipcRenderer.send('open-profile', { user: localUsername, isOwn: true });
});
// Open other user's profile if set, otherwise open it to allow setting the user.
const otherPfp = document.getElementById('otherProfilePic');
const otherContainer = document.getElementById('otherSongContainer');
const openOtherProfile = () => {
// Always open the profile for the 'other' user slot, passing the current tracked username (or empty)
ipcRenderer.send('open-profile', { user: localOtherUsername, isOwn: false });
};
if (otherPfp) {
otherPfp.addEventListener('click', openOtherProfile);
} else {
// If there's no PFP (meaning no otherUsername is set), make the container clickable
otherContainer.style.cursor = 'pointer';
otherContainer.addEventListener('click', openOtherProfile);
}
// Function to update UI elements when otherUsername changes
ipcRenderer.on('other-user-updated', (event, newOtherUsername) => {
localOtherUsername = newOtherUsername; // Update local variable
const otherInfo = document.getElementById('otherSongInfo');
let otherPfpElement = document.getElementById('otherProfilePic'); // Use let
const container = document.getElementById('otherSongContainer');
if (newOtherUsername) {
otherInfo.innerText = "Other user's song: Loading...";
if (!otherPfpElement) {
// Add the img element if it doesn't exist
const img = document.createElement('img');
img.id = 'otherProfilePic';
img.className = 'pfp';
img.src = \`\${localApiUrl}/pfp/\${newOtherUsername}\`;
img.alt = 'Other Profile Picture';
img.onerror = function() { this.style.visibility='hidden'; }; // Add onerror handler
img.addEventListener('click', openOtherProfile); // Re-add listener
container.insertBefore(img, container.firstChild); // Add it inside
container.style.cursor = 'default'; // Reset cursor
// Remove the placeholder text if it exists
const placeholder = container.querySelector('div[style*="padding"]');
if(placeholder) placeholder.remove();
otherPfpElement = img; // Assign the newly created element
// --- NEW: Fetch color for the new PFP ---
fetchAndApplyColor(newOtherUsername, 'otherProfilePic');
// --- END NEW ---
} else {
otherPfpElement.src = \`\${localApiUrl}/pfp/\${newOtherUsername}\`;
otherPfpElement.style.visibility = 'visible'; // Ensure it's visible
otherPfpElement.style.borderColor = '#fff'; // Reset border until fetched
const placeholder = container.querySelector('div[style*="padding"]');
if(placeholder) placeholder.remove();
// --- NEW: Fetch color for the updated PFP ---
fetchAndApplyColor(newOtherUsername, 'otherProfilePic');
// --- END NEW ---
}
// Ensure the click listener is on the pfp if it exists
if(otherPfpElement) {
container.style.cursor = 'default';
container.removeEventListener('click', openOtherProfile); // remove container listener
}
} else {
otherInfo.innerText = "No other user set";
if (otherPfpElement) {
otherPfpElement.remove(); // Remove the pfp image
}
if (!container.querySelector('div[style*="padding"]')) {
const placeholder = document.createElement('div');
placeholder.style.padding = '10px';
placeholder.style.textAlign = 'center';
placeholder.innerText = 'Click here to set tracked user.';
container.insertBefore(placeholder, otherInfo); // Add placeholder text
}
container.style.cursor = 'pointer'; // Make container clickable again
container.addEventListener('click', openOtherProfile);
}
});
</script>
</body>
</html>`;
}
// ------------------------------
// Settings Page HTML (REMOVED)
// ------------------------------
// function getSettingsPageHTML() { ... } // This function is no longer needed
// ------------------------------
// Profile Page HTML (for both own and other user)
// ------------------------------
// Added currentOtherUsername parameter
function getProfilePageHTML(user, isOwn, currentOtherUsername) {
const displayUser = isOwn ? user : (currentOtherUsername || "Not Set"); // Show current tracked user or "Not Set"
const pfpUser = isOwn ? user : currentOtherUsername; // Use actual username for PFP if set
// --- NEW: Fetch color logic for profile page PFP ---
const profilePfpColorScript = pfpUser ? `
fetch(\`\${apiUrl}/pfpColor/${pfpUser}\`)
.then(response => response.ok ? response.json() : Promise.reject('Failed to fetch'))
.then(data => {
if (data && data.colorHexData) {
const pfpContainer = document.getElementById('profilePicContainer');
if(pfpContainer) pfpContainer.style.borderColor = data.colorHexData;
}
})
.catch(err => console.error('Error fetching profile pfp color:', err));
` : '';
// --- END NEW ---
return `<!DOCTYPE html>
<html>
<head>
<title>Profile - ${displayUser}</title>
<style>
body { font-family: sans-serif; margin: 0; padding: 0; background: #222; color: #fff; }
.banner { position: relative; width: 100%; height: 250px; overflow: hidden; background: #444; /* Default background */ }
.banner img, .banner video { width: 100%; height: 100%; object-fit: cover; }
.pfp-container { position: absolute; top: 10px; left: 10px; width: 80px; height: 80px; border: 3px solid #fff; border-radius: 50%; overflow: hidden; cursor: ${isOwn ? 'pointer' : 'default'}; background: #555; /* Default bg for pfp */ } /* Default border */
.pfp-container img { width: 100%; height: 100%; display: block; /* Prevents small gap */ object-fit: cover; }
.profile-info { padding: 20px; }
.editable { background: #333; padding: 10px; margin-bottom: 10px; border-radius: 5px; }
.back { cursor: pointer; padding: 10px; color: #fff; font-size: 24px; position: absolute; top: 5px; right: 10px; z-index: 10; }
button { padding: 5px 10px; font-size: 16px; margin-top: 5px; cursor: pointer; }
input[type="text"], input[type="color"], textarea { padding: 5px; font-size: 16px; width: calc(100% - 12px); margin-top: 5px; background: #444; border: 1px solid #555; color: #fff; }
label { display: block; margin-bottom: 3px; }
</style>
</head>
<body>
<div class="back" id="backBtn">&#8592; Back</div>
<div class="banner" id="bannerArea">
${pfpUser ? `
<div class="pfp-container" id="profilePicContainer">
<img id="profilePic" src="${apiUrl}/pfp/${pfpUser}" alt="Profile Picture" onerror="this.style.display='none'; this.parentElement.style.background='#555';">
</div>` : `
<div class="pfp-container" id="profilePicContainer">
</div>`}
</div>
<div class="profile-info">
<h2 id="usernameDisplay">${displayUser}</h2>
<p id="profileBio">Loading bio...</p>
<p id="profileSong">Loading profile song...</p>
${!isOwn ? `
<div class="editable">
<label for="otherUsernameInput">Tracked Username:</label>
<input type="text" id="otherUsernameInput" placeholder="Enter username to track" value="${currentOtherUsername || ''}">
<button id="saveOtherUsername">Save Tracked User</button>
<p id="saveStatus" style="margin-top: 5px; font-size: 14px;"></p>
</div>
` : ''}
${isOwn ? `
<div class="editable">
<label>Change Bio:</label>
<textarea id="bioInput" rows="3" cols="40"></textarea><br>
<button id="saveBio">Save Bio</button>
</div>
<div class="editable">
<label>Change Profile Song:</label>
<input type="text" id="songInput" placeholder="Enter song ID"><br>
<button id="saveSong">Save Profile Song</button>
</div>
<div class="editable">
<label>Change Profile Picture:</label>
<input type="file" id="pfpInput" accept="image/*"><br>
<button id="uploadPfp">Upload PFP</button>
</div>
<div class="editable">
<label>Change Banner:</label>
<input type="file" id="bannerInput" accept="image/*,video/mp4"><br>
<button id="uploadBanner">Upload Banner</button>
</div>
<div class="editable">
<label>Set Accent Color:</label>
<input type="color" id="pfpColorInput"><br>
<button id="savePfpColor">Save Color</button>
</div>
` : ''}
</div>
<script>
const apiUrl = "${apiUrl}"; // Ensure apiUrl is available for profile.js context if needed via global scope or pass explicitly
// Assume profile.js is in the same directory or accessible
// Using require here relies on nodeIntegration: true and contextIsolation: false
try {
const { loadProfile, setupEventListeners } = require('./profile.js');
document.addEventListener('DOMContentLoaded', () => {
const userToLoad = "${isOwn ? user : currentOtherUsername}"; // Load data for the actual user if set
if (userToLoad) {
loadProfile(userToLoad, ${isOwn});
// --- NEW: Execute profile pfp color fetch ---
${profilePfpColorScript}
// --- END NEW ---
} else {
// Handle case where no other user is set yet - maybe clear fields
document.getElementById('profileBio').innerText = "Bio: Not available";
document.getElementById('profileSong').innerText = "Profile Song: Not available";
// Clear banner/pfp if needed
const bannerArea = document.getElementById('bannerArea');
const pfpContainer = document.getElementById('profilePicContainer');
if (pfpContainer) pfpContainer.innerHTML = ''; // Clear PFP content
}
// Pass the currentOtherUsername to setupEventListeners
setupEventListeners("${user}", ${isOwn}, "${currentOtherUsername || ''}");
});
} catch (e) {
console.error("Failed to load profile script:", e);
alert("Error loading profile functionality. Please ensure profile.js is present.");
}
</script>
</body>
</html>`;
}
// ------------------------------
// Main Window & IPC handlers
// ------------------------------
function createWindow() {
win = new BrowserWindow({
width: 400,
height: 800,
webPreferences: {
nodeIntegration: true,
contextIsolation: false, // Keep false if using require in renderer as shown
// preload: path.join(__dirname, 'preload.js') // Consider using preload for better security later
}
});
// win.webContents.openDevTools();
win.removeMenu(); // Remove menu bar for cleaner UI
win.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(getMainPageHTML())); // Use dynamic HTML generation
// Handle window close event if needed
win.on('closed', () => {
win = null; // Dereference the window object
});
}
// REMOVED: ipcMain.on('open-settings', ...);
ipcMain.on('back-to-main', () => {
if (win && !win.isDestroyed()) {
// Reload the main page HTML dynamically
win.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(getMainPageHTML()));
}
});
ipcMain.on('set-other-user', (event, user) => {
const oldUsername = otherUsername;
otherUsername = user.trim(); // Store the new username (trim whitespace)
store.set('otherUsername', otherUsername);
console.log("Other username set to:", otherUsername);
// Optionally, send a message back to the renderer process (profile page) to confirm
event.reply('other-user-set-confirm', { success: true, newUser: otherUsername });
// Send update to main window's renderer process
if (win && !win.isDestroyed() && win.webContents) {
// No need to check URL, send the update regardless. The main page renderer script will handle it if it's active.
win.webContents.send('other-user-updated', otherUsername);
}
// If the username actually changed, trigger an immediate check for the new user's song
if (otherUsername !== oldUsername) {
checkOtherUserSong(); // Check the new user's song immediately
}
});
ipcMain.on('open-profile', (event, data) => {
// data: { user, isOwn }
// When opening the 'other' profile, 'user' might be empty if none is set yet.
// We pass the current `otherUsername` from the main process to the HTML generator.
if (win && !win.isDestroyed()) {
// Pass the ACTUAL user associated with the profile being opened (data.user)
// and the currently tracked otherUsername (needed for the input field on the 'other' profile page)
win.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(getProfilePageHTML(data.user, data.isOwn, otherUsername)));
}
});
app.whenReady().then(() => {
createWindow();
// Initial check
checkNowPlaying();
// Existing polling for currently playing song (and fetching other user's song info) remains:
setInterval(checkNowPlaying, 5000); // Check every 5 seconds
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
// ------------------------------
// Existing functions for song updates (modified checkNowPlaying)
// ------------------------------
function lookupAppleMusicInfo(title, artist) {
// Use try-catch for robustness
try {
const query = encodeURIComponent(`${artist} ${title}`);
const url = `https://itunes.apple.com/search?term=${query}&entity=song&limit=1`;
return axios.get(url)
.then(response => {
if (response.data.results && response.data.results.length > 0) {
const result = response.data.results[0];
return {
trackId: result.trackId?.toString() || "", // Handle potential missing ID
albumArt: result.artworkUrl100 ? result.artworkUrl100.replace('100x100', '512x512') : "" // Handle missing art
};
}
return { trackId: "", albumArt: "" };
})
.catch(err => {
console.error("Error looking up Apple Music info:", err.message);
return { trackId: "", albumArt: "" };
});
} catch (error) {
console.error("Error constructing Apple Music lookup URL:", error);
return Promise.resolve({ trackId: "", albumArt: "" }); // Return a resolved promise
}
}
function lookupAppleMusicInfoById(trackId) {
if (!trackId) {
return Promise.resolve({ albumArt: "" }); // No ID, no lookup
}
try {
const url = `https://itunes.apple.com/lookup?id=${trackId}`;
return axios.get(url)
.then(response => {
if (response.data.results && response.data.results.length > 0) {
const result = response.data.results[0];
return {
albumArt: result.artworkUrl100 ? result.artworkUrl100.replace('100x100', '512x512') : "" // Handle missing art
};
}
return { albumArt: "" };
})
.catch(err => {
console.error("Error looking up Apple Music info by ID:", err.message);
return { albumArt: "" };
});
} catch (error) {
console.error("Error constructing Apple Music lookup URL by ID:", error);
return Promise.resolve({ albumArt: "" }); // Return a resolved promise
}
}
// Split checking own song and other user's song
function checkOwnSong() {
return new Promise((resolve, reject) => {
exec(`powershell -ExecutionPolicy Bypass -File get-media.ps1`, (error, stdout) => {
if (error) {
console.error("Error executing PowerShell:", error);
// Don't reject, just resolve with no update status
resolve({ updated: false });
return;
}
try {
const data = JSON.parse(stdout);
if (!data.title && !data.artist) { // Check for empty strings specifically
if (lastSong !== "No song playing") {
lastSong = "No song playing";
updateMySong("No song playing", "", "");
sendSongUpdate("", "", ""); // Send empty update to clear server status
resolve({ updated: true });
} else {
resolve({ updated: false }); // No change
}
} else {
const currentSong = `${data.title} - ${data.artist}`;
if (currentSong !== lastSong) {
lastSong = currentSong;
lookupAppleMusicInfo(data.title, data.artist).then(info => {
sendSongUpdate(data.title, data.artist, info.trackId);
updateMySong(currentSong, info.albumArt, info.trackId);
resolve({ updated: true, song: currentSong });
});
} else {
resolve({ updated: false }); // No change
}
}
} catch (err) {
console.error("Error parsing PowerShell output:", stdout, err);
resolve({ updated: false }); // Resolve anyway
}
});
});
}
function checkOtherUserSong() {
if (!otherUsername) {
updateOtherSong("No other user set", ""); // Clear other user info if username is removed
return Promise.resolve(); // Nothing to do
}
return axios.post(apiUrl, {
action: "fetch",
user: otherUsername
})
.then(async response => { // Make async to use await
const data = response.data;
let otherSongText = `No song playing for ${otherUsername}`;
let albumArtUrl = "";
let amId = "";
if (data && (data.title || data.artist)) { // Check if data exists and has title/artist
otherSongText = `${data.title || 'Unknown Title'} - ${data.artist || 'Unknown Artist'}`;
amId = data.amUri || ""; // Get AM ID if present
if (amId) {
const info = await lookupAppleMusicInfoById(amId); // Wait for lookup
albumArtUrl = info.albumArt;
// Optionally add AM ID to text: otherSongText += ` (AM ID: ${amId})`;
}
} else if (data && data.amUri) { // Handle case where only AM URI might be stored (e.g., from profile song)
amId = data.amUri;
const info = await lookupAppleMusicInfoById(amId);
albumArtUrl = info.albumArt;
// Try to get song name/artist from ID lookup if possible (Apple API might provide it)
// This part is complex; for now, just show ID if title/artist missing
otherSongText = `Song (AM ID: ${amId})`; // Placeholder text
}
updateOtherSong(otherSongText, albumArtUrl);
})
.catch(err => {
console.error(`Error fetching song info for ${otherUsername}:`, err.response ? err.response.data : err.message);
// Display error state in UI for other user
updateOtherSong(`Error loading song for ${otherUsername}`, "");
});
}
// Combined check function called by interval
async function checkNowPlaying() {
await checkOwnSong(); // Wait for own song check to complete
await checkOtherUserSong(); // Then check other user's song
}
function sendSongUpdate(title, artist, amUri) {
axios.post(apiUrl, {
action: "write",
user: username,
title: title || "", // Send empty strings if null/undefined
artist: artist || "",
amUri: amUri || ""
})
.then(() => {
// console.log(`Sent song update: ${title} by ${artist} (AM ID: ${amUri})`); // Less verbose logging
})
.catch(error => {
console.error("Error sending song update:", error.message);
});
}
// Removed getPfpColor as it's handled in the renderer now
function updateMySong(songText, albumArtUrl, trackId) {
if (win && !win.isDestroyed() && win.webContents) {
const jsCode = `
(function() {
const container = document.getElementById('mySongContainer');
const info = document.getElementById('mySongInfo');
if (container && info) {
container.style.backgroundImage = "url('${albumArtUrl || ''}')";
info.textContent = "Now Playing: ${songText.replace(/'/g, "\\'")}${trackId ? `` : ''}"; // Removed AM ID display here for cleaner look
}
})();
`;
win.webContents.executeJavaScript(jsCode).catch(e => console.error("Error executing JS for my song:", e));
}
}
function updateOtherSong(songText, albumArtUrl) {
if (win && !win.isDestroyed() && win.webContents) {
const jsCode = `
(function() {
const container = document.getElementById('otherSongContainer');
const info = document.getElementById('otherSongInfo');
if (container && info) {
// Only update background if URL is provided
if ('${albumArtUrl || ''}') {
container.style.backgroundImage = "url('${albumArtUrl}')";
} else {
container.style.backgroundImage = "none"; // Clear background if no art
}
info.textContent = "${songText.replace(/'/g, "\\'")}";
}
})();
`;
win.webContents.executeJavaScript(jsCode).catch(e => console.error("Error executing JS for other song:", e));
}
}
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});