// YTMusic Now Playing - Content Script // Scrapes now-playing info from YouTube Music and sends it to a configurable server. (function () { "use strict"; const DEFAULT_SERVER_URL = "http://localhost:9095"; const POLL_INTERVAL_MS = 3000; let lastSentData = null; let serverUrl = DEFAULT_SERVER_URL; // Load server URL from storage chrome.storage.sync.get({ serverUrl: DEFAULT_SERVER_URL }, (result) => { serverUrl = result.serverUrl; }); // Listen for storage changes so the popup can update the URL live chrome.storage.onChanged.addListener((changes) => { if (changes.serverUrl) { serverUrl = changes.serverUrl.newValue || DEFAULT_SERVER_URL; } }); /** * Scrape the current now-playing information from the YouTube Music DOM. */ function scrapeNowPlaying() { // Song title const titleEl = document.querySelector("ytmusic-player-bar .title") || document.querySelector(".ytmusic-player-bar .title"); const title = titleEl ? titleEl.textContent.trim() : null; // Artist / byline const bylineEl = document.querySelector("ytmusic-player-bar .byline") || document.querySelector(".ytmusic-player-bar .byline"); const artist = bylineEl ? bylineEl.textContent.trim() : null; // Album art URL const artImg = document.querySelector( "ytmusic-player-bar .middle-controls .thumbnail-image-wrapper img" ) || document.querySelector("ytmusic-player-bar .image img") || document.querySelector("ytmusic-player-bar img.image"); const albumArt = artImg ? artImg.src : null; // Play state: the play-pause button's title attribute indicates the state. // When music is playing the button shows "Pause" (because clicking it would pause). const playPauseBtn = document.querySelector("#play-pause-button"); let isPlaying = false; if (playPauseBtn) { const btnTitle = ( playPauseBtn.getAttribute("title") || playPauseBtn.getAttribute("aria-label") || "" ).toLowerCase(); // If the button says "pause", music is currently playing isPlaying = btnTitle.includes("pause"); } // Time info: usually shows "elapsed / duration" like "1:23 / 3:45" const timeInfoEl = document.querySelector("ytmusic-player-bar .time-info") || document.querySelector(".ytmusic-player-bar .time-info"); let elapsed = null; let duration = null; if (timeInfoEl) { const timeText = timeInfoEl.textContent.trim(); const parts = timeText.split("/").map((s) => s.trim()); if (parts.length === 2) { elapsed = parts[0]; duration = parts[1]; } } // If there's no title, nothing is playing if (!title) { return null; } return { title, artist, albumArt, isPlaying, elapsed, duration, }; } /** * Check if two data objects are equal (shallow comparison of relevant fields). */ function dataChanged(a, b) { if (a === null && b === null) return false; if (a === null || b === null) return true; return ( a.title !== b.title || a.artist !== b.artist || a.albumArt !== b.albumArt || a.isPlaying !== b.isPlaying || a.elapsed !== b.elapsed || a.duration !== b.duration ); } /** * Send data to the server. */ async function sendToServer(data) { const url = `${serverUrl}/api/now-playing`; try { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!response.ok) { console.warn( `[YTMusic Now Playing] Server responded with ${response.status}` ); } } catch (err) { console.warn(`[YTMusic Now Playing] Failed to send data:`, err.message); } } /** * Main polling loop. */ function poll() { const data = scrapeNowPlaying(); if (dataChanged(data, lastSentData)) { lastSentData = data; if (data) { sendToServer(data); } else { // Send a "nothing playing" payload sendToServer({ isPlaying: false }); } } } // Start polling setInterval(poll, POLL_INTERVAL_MS); // Run once immediately poll(); console.log("[YTMusic Now Playing] Content script loaded."); })();