summaryrefslogtreecommitdiffstats
path: root/browser/extensions/webcompat/shims/spotify-embed.js
blob: 62ad05b725bd410f089240c41588b7a360f5d1b4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/* 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 }
  );
})();