Skip to content

Commit

Permalink
Merge pull request #2939 from metabrainz/update-music-service-token
Browse files Browse the repository at this point in the history
Update Token in context after being refreshed
  • Loading branch information
MonkeyDo committed Jul 16, 2024
2 parents d624827 + 37b4c08 commit 00d9e43
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 52 deletions.
10 changes: 1 addition & 9 deletions frontend/js/src/common/brainzplayer/BrainzPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -902,13 +902,7 @@ export default class BrainzPlayer extends React.Component<
refreshSoundcloudToken,
listenBrainzAPIBaseURI,
} = this.props;
const {
youtubeAuth,
spotifyAuth,
soundcloudAuth,
appleAuth,
userPreferences,
} = this.context;
const { youtubeAuth, userPreferences } = this.context;
const brainzPlayerDisabled =
userPreferences?.brainzplayer?.spotifyEnabled === false &&
userPreferences?.brainzplayer?.youtubeEnabled === false &&
Expand Down Expand Up @@ -949,7 +943,6 @@ export default class BrainzPlayer extends React.Component<
refreshSpotifyToken={refreshSpotifyToken}
onInvalidateDataSource={this.invalidateDataSource}
ref={this.spotifyPlayer}
spotifyUser={spotifyAuth}
playerPaused={playerPaused}
onPlayerPausedChange={this.playerPauseChange}
onProgressChange={this.progressChange}
Expand Down Expand Up @@ -994,7 +987,6 @@ export default class BrainzPlayer extends React.Component<
}
onInvalidateDataSource={this.invalidateDataSource}
ref={this.soundcloudPlayer}
soundcloudUser={soundcloudAuth}
refreshSoundcloudToken={refreshSoundcloudToken}
playerPaused={playerPaused}
onPlayerPausedChange={this.playerPauseChange}
Expand Down
20 changes: 14 additions & 6 deletions frontend/js/src/common/brainzplayer/SoundcloudPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
getTrackName,
searchForSoundcloudTrack,
} from "../../utils/utils";
import GlobalAppContext from "../../utils/GlobalAppContext";

require("../../../lib/soundcloud-player-api");

Expand Down Expand Up @@ -61,13 +62,13 @@ export type SoundcloudPlayerState = {
};

export type SoundCloudPlayerProps = DataSourceProps & {
soundcloudUser?: SoundCloudUser;
refreshSoundcloudToken: () => Promise<string>;
};

export default class SoundcloudPlayer
extends React.Component<SoundCloudPlayerProps, SoundcloudPlayerState>
implements DataSourceType {
static contextType = GlobalAppContext;
static hasPermissions = (soundcloudUser?: SoundCloudUser) => {
return Boolean(soundcloudUser?.access_token);
};
Expand Down Expand Up @@ -112,19 +113,22 @@ export default class SoundcloudPlayer
// and it simplifies some of the closure issues we've had with old tokens.
private accessToken = "";
private authenticationRetries = 0;
declare context: React.ContextType<typeof GlobalAppContext>;

constructor(props: SoundCloudPlayerProps) {
super(props);
this.accessToken = props.soundcloudUser?.access_token || "";
this.state = { currentSound: undefined };
this.iFrameRef = React.createRef();
}

componentDidMount() {
const { soundcloudAuth: soundcloudUser = undefined } = this.context;
this.accessToken = soundcloudUser?.access_token || "";
// initial permissions check
if (!SoundcloudPlayer.hasPermissions(props.soundcloudUser)) {
if (!SoundcloudPlayer.hasPermissions(soundcloudUser)) {
this.handleAccountError();
}
}

componentDidMount() {
const { onInvalidateDataSource } = this.props;
if (!(window as any).SC) {
onInvalidateDataSource(this, "Soundcloud JS API did not load properly.");
Expand Down Expand Up @@ -204,7 +208,7 @@ export default class SoundcloudPlayer
};

canSearchAndPlayTracks = (): boolean => {
const { soundcloudUser } = this.props;
const { soundcloudAuth: soundcloudUser = undefined } = this.context;
// check if the user is authed to search with the SoundCloud API
return Boolean(soundcloudUser) && Boolean(soundcloudUser?.access_token);
};
Expand Down Expand Up @@ -264,6 +268,7 @@ export default class SoundcloudPlayer
callbackFunction: () => void
): Promise<void> => {
const { refreshSoundcloudToken, onTrackNotFound, handleError } = this.props;
const { soundcloudAuth: soundcloudUser = undefined } = this.context;
if (this.authenticationRetries > 5) {
handleError(
isString(error) ? error : error?.message,
Expand All @@ -276,6 +281,9 @@ export default class SoundcloudPlayer
try {
this.accessToken = await refreshSoundcloudToken();
this.authenticationRetries = 0;
if (soundcloudUser) {
soundcloudUser.access_token = this.accessToken;
}
callbackFunction();
} catch (refreshError) {
handleError(refreshError, "Error connecting to SoundCloud");
Expand Down
21 changes: 16 additions & 5 deletions frontend/js/src/common/brainzplayer/SpotifyPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
getArtistName,
} from "../../utils/utils";
import { DataSourceType, DataSourceProps } from "./BrainzPlayer";
import GlobalAppContext from "../../utils/GlobalAppContext";

// Fix for LB-447 (Player does not play any sound)
// https://github.com/spotify/web-playback-sdk/issues/75#issuecomment-487325589
Expand All @@ -34,7 +35,6 @@ const fixSpotifyPlayerStyleIssue = () => {
};

export type SpotifyPlayerProps = DataSourceProps & {
spotifyUser?: SpotifyUser;
refreshSpotifyToken: () => Promise<string>;
};

Expand All @@ -48,6 +48,7 @@ export type SpotifyPlayerState = {
export default class SpotifyPlayer
extends React.Component<SpotifyPlayerProps, SpotifyPlayerState>
implements DataSourceType {
static contextType = GlobalAppContext;
static hasPermissions = (spotifyUser?: SpotifyUser) => {
if (!spotifyUser) {
return false;
Expand Down Expand Up @@ -109,11 +110,11 @@ export default class SpotifyPlayer
private authenticationRetries = 0;
spotifyPlayer?: SpotifyPlayerType;
debouncedOnTrackEnd: () => void;
declare context: React.ContextType<typeof GlobalAppContext>;

constructor(props: SpotifyPlayerProps) {
super(props);

this.accessToken = props.spotifyUser?.access_token || "";
this.state = {
durationMs: 0,
};
Expand All @@ -122,9 +123,15 @@ export default class SpotifyPlayer
leading: true,
trailing: false,
});
}

async componentDidMount(): Promise<void> {
const { spotifyAuth: spotifyUser = undefined } = this.context;

this.accessToken = spotifyUser?.access_token || "";

// Do an initial check of the spotify token permissions (scopes) before loading the SDK library
if (SpotifyPlayer.hasPermissions(props.spotifyUser)) {
if (SpotifyPlayer.hasPermissions(spotifyUser)) {
window.onSpotifyWebPlaybackSDKReady = this.connectSpotifyPlayer;
loadScriptAsync(document, "https://sdk.scdn.co/spotify-player.js");
} else {
Expand Down Expand Up @@ -287,12 +294,12 @@ export default class SpotifyPlayer
};

canSearchAndPlayTracks = (): boolean => {
const { spotifyUser } = this.props;
const { spotifyAuth: spotifyUser = undefined } = this.context;
return SpotifyPlayer.hasPermissions(spotifyUser);
};

datasourceRecordsListens = (): boolean => {
const { spotifyUser } = this.props;
const { spotifyAuth: spotifyUser = undefined } = this.context;
if (!spotifyUser?.permission) {
return false;
}
Expand Down Expand Up @@ -428,6 +435,7 @@ export default class SpotifyPlayer
return;
}
const { refreshSpotifyToken } = this.props;
const { spotifyAuth: spotifyUser = undefined } = this.context;

this.spotifyPlayer = new window.Spotify.Player({
name: "ListenBrainz Player",
Expand All @@ -436,6 +444,9 @@ export default class SpotifyPlayer
const userToken = await refreshSpotifyToken();
this.accessToken = userToken;
this.authenticationRetries = 0;
if (spotifyUser) {
spotifyUser.access_token = userToken;
}
authCallback(userToken);
} catch (error) {
handleError(error, "Error connecting to Spotify");
Expand Down
43 changes: 29 additions & 14 deletions frontend/js/tests/common/brainzplayer/SoundcloudPlayer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
import * as React from "react";
import { mount, ReactWrapper, shallow } from "enzyme";
import { mount } from "enzyme";

import { act } from "react-dom/test-utils";
import SoundcloudPlayer, {
SoundcloudPlayerState,
} from "../../../src/common/brainzplayer/SoundcloudPlayer";
import {
DataSourceProps,
DataSourceTypes,
} from "../../../src/common/brainzplayer/BrainzPlayer";
import SoundcloudPlayer from "../../../src/common/brainzplayer/SoundcloudPlayer";
import { DataSourceTypes } from "../../../src/common/brainzplayer/BrainzPlayer";
import APIService from "../../../src/utils/APIService";
import RecordingFeedbackManager from "../../../src/utils/RecordingFeedbackManager";
import GlobalAppContext from "../../../src/utils/GlobalAppContext";

// Create a new instance of GlobalAppContext
const defaultContext = {
APIService: new APIService("foo"),
websocketsUrl: "",
youtubeAuth: {} as YoutubeUser,
spotifyAuth: {} as SpotifyUser,
currentUser: {} as ListenBrainzUser,
soundcloudAuth: {
access_token: "heyo-soundcloud",
},
recordingFeedbackManager: new RecordingFeedbackManager(
new APIService("foo"),
{ name: "Fnord" }
),
};

const props = {
show: true,
playerPaused: false,
soundcloudUser: {
access_token: "heyo-soundcloud",
},
refreshSoundcloudToken: new APIService("base-uri").refreshSoundcloudToken,
onPlayerPausedChange: (paused: boolean) => {},
onProgressChange: (progressMs: number) => {},
Expand Down Expand Up @@ -65,11 +75,16 @@ describe("SoundcloudPlayer", () => {
onInvalidateDataSource,
onTrackNotFound,
};
const wrapper = mount<SoundcloudPlayer>(
<SoundcloudPlayer {...mockProps} />

const wrapper = mount(
<GlobalAppContext.Provider value={defaultContext}>
<SoundcloudPlayer {...mockProps} />
</GlobalAppContext.Provider>
);
const instance = wrapper
.find(SoundcloudPlayer)
.instance() as SoundcloudPlayer;

const instance = wrapper.instance();
if (!instance.soundcloudPlayer) {
throw new Error("no SoundcloudPlayer");
}
Expand Down
60 changes: 42 additions & 18 deletions frontend/js/tests/common/brainzplayer/SpotifyPlayer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import * as React from "react";
import { mount, ReactWrapper, shallow, ShallowWrapper } from "enzyme";
import { mount, shallow } from "enzyme";

import { act } from "react-dom/test-utils";
import { Link } from "react-router-dom";
import SpotifyPlayer, {
SpotifyPlayerProps,
SpotifyPlayerState,
} from "../../../src/common/brainzplayer/SpotifyPlayer";
import SpotifyPlayer from "../../../src/common/brainzplayer/SpotifyPlayer";
import APIService from "../../../src/utils/APIService";
import {
DataSourceProps,
DataSourceTypes,
} from "../../../src/common/brainzplayer/BrainzPlayer";
import { DataSourceTypes } from "../../../src/common/brainzplayer/BrainzPlayer";
import GlobalAppContext from "../../../src/utils/GlobalAppContext";
import RecordingFeedbackManager from "../../../src/utils/RecordingFeedbackManager";

const props = {
spotifyUser: {
// Create a new instance of GlobalAppContext
const defaultContext = {
APIService: new APIService("foo"),
websocketsUrl: "",
youtubeAuth: {} as YoutubeUser,
spotifyAuth: {
access_token: "heyo",
permission: ["user-read-currently-playing"] as SpotifyPermission[],
},
currentUser: {} as ListenBrainzUser,
recordingFeedbackManager: new RecordingFeedbackManager(
new APIService("foo"),
{ name: "Fnord" }
),
};

const props = {
refreshSpotifyToken: new APIService("base-uri").refreshSpotifyToken,
show: true,
playerPaused: false,
Expand Down Expand Up @@ -113,7 +121,7 @@ describe("SpotifyPlayer", () => {
...props,
onInvalidateDataSource,
};
expect(SpotifyPlayer.hasPermissions(mockProps.spotifyUser)).toEqual(
expect(SpotifyPlayer.hasPermissions(defaultContext.spotifyAuth)).toEqual(
false
);
const wrapper = shallow<SpotifyPlayer>(<SpotifyPlayer {...mockProps} />);
Expand All @@ -138,10 +146,18 @@ describe("SpotifyPlayer", () => {
const mockProps = {
...props,
onInvalidateDataSource,
spotifyUser,
};
const wrapper = shallow<SpotifyPlayer>(<SpotifyPlayer {...mockProps} />);
const instance = wrapper.instance();
const wrapper = mount(
<GlobalAppContext.Provider
value={{
...defaultContext,
spotifyAuth: spotifyUser,
}}
>
<SpotifyPlayer {...mockProps} />
</GlobalAppContext.Provider>
);
const instance = wrapper.find(SpotifyPlayer).instance() as SpotifyPlayer;

expect.assertions(2);
expect(instance.props.onInvalidateDataSource).not.toHaveBeenCalled();
Expand All @@ -163,11 +179,19 @@ describe("SpotifyPlayer", () => {
const mockProps = {
...props,
onInvalidateDataSource,
spotifyUser,
checkSpotifyToken,
};
const wrapper = shallow<SpotifyPlayer>(<SpotifyPlayer {...mockProps} />);
const instance = wrapper.instance();
const wrapper = mount(
<GlobalAppContext.Provider
value={{
...defaultContext,
spotifyAuth: spotifyUser,
}}
>
<SpotifyPlayer {...mockProps} />
</GlobalAppContext.Provider>
);
const instance = wrapper.find(SpotifyPlayer).instance() as SpotifyPlayer;

instance.handleAccountError();
expect(instance.props.onInvalidateDataSource).toHaveBeenCalledTimes(1);
Expand Down

0 comments on commit 00d9e43

Please sign in to comment.