154 lines
4.3 KiB
JavaScript
154 lines
4.3 KiB
JavaScript
// 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.");
|
||
|
|
})();
|