222 lines
6.3 KiB
JavaScript
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 });
|