Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* globals exportFunction */
"use strict";
/**
* Spotify embeds default to "track preview mode". They require first-party
* storage access in order to detect the login status and allow the user to play
* the whole song or add it to their library.
* Upon clicking the "play" button in the preview view this shim attempts to get
* storage access and on success, reloads the frame and plays the full track.
* This only works if the user is already logged in to Spotify in the
* first-party context.
*/
const AUTOPLAY_FLAG = "shimPlayAfterStorageAccess";
const SELECTOR_PREVIEW_PLAY = 'div[data-testid="preview-play-pause"] > button';
const SELECTOR_FULL_PLAY = 'button[data-testid="play-pause-button"]';
/**
* Promise-wrapper around DOMContentLoaded event.
*/
function waitForDOMContentLoaded() {
return new Promise(resolve => {
window.addEventListener("DOMContentLoaded", resolve, { once: true });
});
}
/**
* Listener for the preview playback button which requests storage access and
* reloads the page.
*/
function previewPlayButtonListener(event) {
const { target, isTrusted } = event;
if (!isTrusted) {
return;
}
const button = target.closest("button");
if (!button) {
return;
}
// Filter for the preview playback button. This won't match the full
// playback button that is shown when the user is logged in.
if (!button.matches(SELECTOR_PREVIEW_PLAY)) {
return;
}
// The storage access request below runs async so playback won't start
// immediately. Mitigate this UX issue by updating the clicked element's
// style so the user gets some immediate feedback.
button.style.opacity = 0.5;
event.stopPropagation();
event.preventDefault();
console.debug("Requesting storage access.", location.origin);
document
.requestStorageAccess()
// When storage access is granted, reload the frame for the embedded
// player to detect the login state and give us full playback
// capabilities.
.then(() => {
// Use a flag to indicate that we want to click play after reload.
// This is so the user does not have to click play twice.
sessionStorage.setItem(AUTOPLAY_FLAG, "true");
console.debug("Reloading after storage access grant.");
location.reload();
})
// If the user denies the storage access prompt we can't use the login
// state. Attempt start preview playback instead.
.catch(() => {
button.click();
})
// Reset button style for both success and error case.
.finally(() => {
button.style.opacity = 1.0;
});
}
/**
* Attempt to start (full) playback. Waits for the play button to appear and
* become ready.
*/
async function startFullPlayback() {
// Wait for DOMContentLoaded before looking for the playback button.
await waitForDOMContentLoaded();
let numTries = 0;
let intervalId = setInterval(() => {
try {
document.querySelector(SELECTOR_FULL_PLAY).click();
clearInterval(intervalId);
console.debug("Clicked play after storage access grant.");
} catch (e) {}
numTries++;
if (numTries >= 50) {
console.debug("Can not start playback. Giving up.");
clearInterval(intervalId);
}
}, 200);
}
(async () => {
// Only run the shim for embedded iframes.
if (window.top == window) {
return;
}
console.warn(
`When using the Spotify embedded player, Firefox calls the Storage Access API on behalf of the site. See https://bugzilla.mozilla.org/show_bug.cgi?id=1792395 for details.`
);
// Already requested storage access before the reload, trigger playback.
if (sessionStorage.getItem(AUTOPLAY_FLAG) == "true") {
sessionStorage.removeItem(AUTOPLAY_FLAG);
await startFullPlayback();
return;
}
// Wait for the user to click the preview play button. If the player has
// already loaded the full version, this method will do nothing.
document.documentElement.addEventListener(
"click",
previewPlayButtonListener,
{ capture: true }
);
})();