spotify-embed.js (4269B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* globals exportFunction */ 6 7 "use strict"; 8 9 /** 10 * Spotify embeds default to "track preview mode". They require first-party 11 * storage access in order to detect the login status and allow the user to play 12 * the whole song or add it to their library. 13 * Upon clicking the "play" button in the preview view this shim attempts to get 14 * storage access and on success, reloads the frame and plays the full track. 15 * This only works if the user is already logged in to Spotify in the 16 * first-party context. 17 */ 18 19 const AUTOPLAY_FLAG = "shimPlayAfterStorageAccess"; 20 const SELECTOR_PREVIEW_PLAY = 'div[data-testid="preview-play-pause"] > button'; 21 const SELECTOR_FULL_PLAY = 'button[data-testid="play-pause-button"]'; 22 23 /** 24 * Promise-wrapper around DOMContentLoaded event. 25 */ 26 function waitForDOMContentLoaded() { 27 return new Promise(resolve => { 28 window.addEventListener("DOMContentLoaded", resolve, { once: true }); 29 }); 30 } 31 32 /** 33 * Listener for the preview playback button which requests storage access and 34 * reloads the page. 35 */ 36 function previewPlayButtonListener(event) { 37 const { target, isTrusted } = event; 38 if (!isTrusted) { 39 return; 40 } 41 42 const button = target.closest("button"); 43 if (!button) { 44 return; 45 } 46 47 // Filter for the preview playback button. This won't match the full 48 // playback button that is shown when the user is logged in. 49 if (!button.matches(SELECTOR_PREVIEW_PLAY)) { 50 return; 51 } 52 53 // The storage access request below runs async so playback won't start 54 // immediately. Mitigate this UX issue by updating the clicked element's 55 // style so the user gets some immediate feedback. 56 button.style.opacity = 0.5; 57 event.stopPropagation(); 58 event.preventDefault(); 59 60 console.debug("Requesting storage access.", location.origin); 61 document 62 .requestStorageAccess() 63 // When storage access is granted, reload the frame for the embedded 64 // player to detect the login state and give us full playback 65 // capabilities. 66 .then(() => { 67 // Use a flag to indicate that we want to click play after reload. 68 // This is so the user does not have to click play twice. 69 sessionStorage.setItem(AUTOPLAY_FLAG, "true"); 70 console.debug("Reloading after storage access grant."); 71 location.reload(); 72 }) 73 // If the user denies the storage access prompt we can't use the login 74 // state. Attempt start preview playback instead. 75 .catch(() => { 76 button.click(); 77 }) 78 // Reset button style for both success and error case. 79 .finally(() => { 80 button.style.opacity = 1.0; 81 }); 82 } 83 84 /** 85 * Attempt to start (full) playback. Waits for the play button to appear and 86 * become ready. 87 */ 88 async function startFullPlayback() { 89 await document.requestStorageAccess(); 90 91 // Wait for DOMContentLoaded before looking for the playback button. 92 await waitForDOMContentLoaded(); 93 94 let numTries = 0; 95 let intervalId = setInterval(() => { 96 try { 97 document.querySelector(SELECTOR_FULL_PLAY).click(); 98 clearInterval(intervalId); 99 console.debug("Clicked play after storage access grant."); 100 } catch (e) {} 101 numTries++; 102 103 if (numTries >= 50) { 104 console.debug("Can not start playback. Giving up."); 105 clearInterval(intervalId); 106 } 107 }, 200); 108 } 109 110 (async () => { 111 // Only run the shim for embedded iframes. 112 if (window.top == window) { 113 return; 114 } 115 116 console.warn( 117 `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.` 118 ); 119 120 // Already requested storage access before the reload, trigger playback. 121 if (sessionStorage.getItem(AUTOPLAY_FLAG) == "true") { 122 sessionStorage.removeItem(AUTOPLAY_FLAG); 123 124 await startFullPlayback(); 125 return; 126 } 127 128 // Wait for the user to click the preview play button. If the player has 129 // already loaded the full version, this method will do nothing. 130 document.documentElement.addEventListener( 131 "click", 132 previewPlayButtonListener, 133 { capture: true } 134 ); 135 })();