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 });