Files
ext-amnp/content.js

222 lines
6.3 KiB
JavaScript

let pauseStartTime = null;
let endpoint = null;
let artworkEndpoint = null;
let token = null;
let intervalId = null;
let lastSentArtworkKey = null;
const defaultArtwork = "https://placehold.co/150/1E1F22/FFF?text=Nothing%20playing";
const pausedTimeout = 300000; // 5 minutes
const metadataFetchInterval = 1000; // 1 second
// Cache for artwork base64 by URL
const artworkBase64Cache = {
lastArtworkUrl: null,
lastArtworkBase64: null
};
async function imageUrlToBase64(url) {
try {
const response = await fetch(url);
const blob = await response.blob();
return await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (e) {
console.error("Failed to fetch and convert image:", e);
return null;
}
}
async function getNowPlayingData() {
const audio = document.querySelector('audio#apple-music-player');
// From PreMiD Apple Music Presence
const video = document.querySelector('apple-music-video-player')?.shadowRoot
?.querySelector('amp-window-takeover > .container > amp-video-player-internal')?.shadowRoot
?.querySelector('amp-video-player')?.shadowRoot
?.querySelector('div#video-container > video#apple-music-video-player');
const media = video || audio;
const timestampInput = document.querySelector('amp-lcd.lcd.lcd__music')?.shadowRoot
?.querySelector('input#playback-progress[aria-valuenow][aria-valuemax]');
if (!artworkBase64Cache.defaultArtworkBase64) {
artworkBase64Cache.defaultArtworkBase64 = await imageUrlToBase64(defaultArtwork);
}
const defaultArtworkBase64 = artworkBase64Cache.defaultArtworkBase64;
if (!media) {
pauseStartTime = null;
return {
details: "Nothing playing",
state: "",
paused: true,
artworkBase64: defaultArtworkBase64
};
}
const paused = media.paused || media.readyState <= 2;
const metadata = navigator.mediaSession?.metadata || {};
const title = metadata.title || media.title || "Unknown Title";
const artist = metadata.artist || "Unknown Artist";
const album = metadata.album || "Unknown Album";
const artworkSrc = Array.isArray(metadata.artwork) && metadata.artwork[0]?.src
? metadata.artwork[0].src
: null;
let artworkBase64 = null;
const largeImageKey = artworkSrc?.replace(/\d+x\d+[a-z]*/i, '150x150bb'); // More robust for Apple Music artwork URLs
if (largeImageKey) {
if (artworkBase64Cache.lastArtworkUrl !== largeImageKey) {
artworkBase64 = await imageUrlToBase64(largeImageKey);
artworkBase64Cache.lastArtworkUrl = largeImageKey;
artworkBase64Cache.lastArtworkBase64 = artworkBase64;
} else {
artworkBase64 = artworkBase64Cache.lastArtworkBase64;
}
}
if (paused) {
pauseStartTime ??= Date.now();
if (Date.now() - pauseStartTime >= pausedTimeout) {
return {
details: "Nothing playing",
state: "",
paused: true,
artworkBase64: defaultArtworkBase64
};
}
} else {
pauseStartTime = null;
}
const currentTime = timestampInput ? Number(timestampInput.getAttribute('aria-valuenow')) : media.currentTime || 0;
const duration = timestampInput ? Number(timestampInput.getAttribute('aria-valuemax')) : media.duration || 0;
const nowUnix = Math.floor(Date.now() / metadataFetchInterval);
return {
details: title,
state: artist,
album,
paused,
currentTime,
startTimestamp: nowUnix - Math.floor(currentTime),
endTimestamp: nowUnix + Math.floor(Math.max(0, duration - currentTime)),
duration,
artworkBase64
};
}
async function sendNowPlaying() {
if (!endpoint || !token) {
console.warn("Endpoint or token not loaded.");
return;
}
const data = await getNowPlayingData();
if (!data) return;
const { artworkBase64, details, state, ...noArtData } = data;
try {
await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ ...noArtData, details, state })
});
console.log('Now playing data sent:', details, '-', state);
} catch (e) {
console.error('Failed to send now playing:', e);
}
// Only send artwork when song changes
const currentTrackKey = `${details} - ${state}`;
if (artworkEndpoint && artworkBase64 && currentTrackKey !== lastSentArtworkKey) {
try {
await fetch(artworkEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
image: artworkBase64
})
});
lastSentArtworkKey = currentTrackKey;
console.log('Artwork sent for:', currentTrackKey);
} catch (e) {
console.error('Failed to send artwork:', e);
}
}
}
function safeCall(fn) {
try {
fn().catch(e => {
if (e.message?.includes("Extension context invalidated")) {
console.warn("Extension context invalidated, clearing interval.");
clearInterval(intervalId);
} else {
console.error("Unexpected async error:", e);
}
});
} catch (e) {
console.error("Unexpected sync error:", e);
}
}
function startNowPlayingLoop() {
if (!window._nowPlayingIntervalSet) {
window._nowPlayingIntervalSet = true;
intervalId = setInterval(() => safeCall(sendNowPlaying), metadataFetchInterval);
}
}
if (!window._nowPlayingExtensionLogged) {
console.log('Apple Music Now Playing Extension made by Sophia Atkinson with help from PreMiD contributors');
window._nowPlayingExtensionLogged = true;
}
if (chrome?.storage?.sync) {
chrome.storage.sync.get(['endpoint', 'artworkEndpoint', 'token'], result => {
if (chrome.runtime.lastError) {
console.error("Failed to load settings:", chrome.runtime.lastError.message);
return;
}
({ endpoint, artworkEndpoint, token } = result);
if (!endpoint || !token) {
console.error("No endpoint/token configured.");
return;
}
startNowPlayingLoop();
});
} else {
console.warn("Chrome extension APIs are not available. Please run this script as a Chrome extension.");
}
let lastUrl = location.href;
new MutationObserver(() => {
if (location.href !== lastUrl) {
console.log('[NowPlaying] Route change detected.');
lastUrl = location.href;
clearInterval(intervalId);
window._nowPlayingIntervalSet = false;
setTimeout(() => {
if (!window._nowPlayingIntervalSet) startNowPlayingLoop();
}, metadataFetchInterval);
}
}).observe(document, { subtree: true, childList: true });