You can configure the following settings: - Placeholder artwork URL - Paused timeout (in seconds, minutes, or hours) - Metadata fetch interval (in seconds, minutes, or hours) I also added helping notes to the title of the inputs to make it clearer what each setting does. Also meow meow meow meow meow meow meow meow meow meow meow meow meow meow
229 lines
6.6 KiB
JavaScript
229 lines
6.6 KiB
JavaScript
let pauseStartTime = null;
|
|
let endpoint = null;
|
|
let artworkEndpoint = null;
|
|
let token = null;
|
|
let intervalId = null;
|
|
let lastSentArtworkKey = null;
|
|
let placeholderArtwork = null;
|
|
let pausedTimeout = 300000;
|
|
let metadataFetchInterval = 1000;
|
|
|
|
|
|
// log credits to console
|
|
if (!window._nowPlayingExtensionLogged) {
|
|
console.log('Apple Music Now Playing Extension made by Sophia Atkinson with help from PreMiD contributors');
|
|
window._nowPlayingExtensionLogged = true;
|
|
}
|
|
|
|
|
|
// Cache for artwork base64 by URL
|
|
const artworkBase64Cache = {
|
|
lastArtworkUrl: null,
|
|
lastArtworkBase64: null,
|
|
defaultArtworkBase64: 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 && placeholderArtwork) {
|
|
artworkBase64Cache.defaultArtworkBase64 = await imageUrlToBase64(placeholderArtwork);
|
|
}
|
|
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() / 1000);
|
|
|
|
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 (chrome?.storage?.sync) {
|
|
chrome.storage.sync.get(['endpoint', 'artworkEndpoint', 'token', 'placeholderArtwork', 'pausedTimeout', 'metadataFetchInterval'], 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;
|
|
placeholderArtwork = result.placeholderArtwork || 'https://placehold.co/150/1E1F22/FFF?text=Nothing%20playing';
|
|
pausedTimeout = Number(result.pausedTimeout) || pausedTimeout;
|
|
metadataFetchInterval = Number(result.metadataFetchInterval) || metadataFetchInterval;
|
|
|
|
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 });
|