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 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; } } // Cache for artwork base64 by URL const artworkBase64Cache = { lastArtworkUrl: null, lastArtworkBase64: 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')?.querySelector('video#apple-music-video-player'); const media = video || audio; if (!media) { pauseStartTime = null; return { details: "Nothing playing", state: "", image: defaultArtwork, paused: true }; } // From PreMiD Apple Music Presence const timestampInput = document.querySelector('amp-lcd.lcd.lcd__music')?.shadowRoot ?.querySelector('input#playback-progress[aria-valuenow][aria-valuemax]'); const paused = media.paused || media.readyState <= 2; const metadata = navigator.mediaSession?.metadata || {}; const title = metadata.title || media.title || "Unknown Title"; const album = metadata.album || "Unknown Album"; const artist = metadata.artist || "Unknown Artist"; const artworkSrc = Array.isArray(metadata.artwork) && metadata.artwork[0]?.src ? metadata.artwork[0].src : null; const largeImageKey = artworkSrc?.replace(/\d+x\d+[a-z]*/i, '150x150bb'); // More robust for Apple Music artwork URLs let artworkBase64 = null; if (artworkSrc) { if (artworkBase64Cache.lastArtworkUrl === largeImageKey && artworkBase64Cache.lastArtworkBase64) { artworkBase64 = artworkBase64Cache.lastArtworkBase64; } else { artworkBase64 = await imageUrlToBase64(largeImageKey); artworkBase64Cache.lastArtworkUrl = largeImageKey; artworkBase64Cache.lastArtworkBase64 = artworkBase64; } } 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); const commonData = { details: title, state: artist, album, paused, currentTime, startTimestamp: nowUnix - Math.floor(currentTime), endTimestamp: nowUnix + Math.floor(Math.max(0, duration - currentTime)), duration }; if (paused) { if (!pauseStartTime) pauseStartTime = Date.now(); if (Date.now() - pauseStartTime >= pausedTimeout) { return { details: "Not playing", image: defaultArtwork, state: "", paused: true }; } } return { ...commonData, 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) return; 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 (typeof chrome !== "undefined" && chrome.storage && 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 = result.endpoint; artworkEndpoint = result.artworkEndpoint; token = result.token; 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 });