Skip to content

Commit 147e405

Browse files
authored
feat: change api (#290)
1 parent 30110f9 commit 147e405

File tree

9 files changed

+3230
-2598
lines changed

9 files changed

+3230
-2598
lines changed

cypress/integration/play-button.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
/* global cy expect*/
2-
31
describe('Stop and play the music', () => {
42
beforeEach(() => {
53
cy.visit('http://localhost:3000');
@@ -8,11 +6,9 @@ describe('Stop and play the music', () => {
86
it('Click play button', () => {
97
cy.get('audio')
108
.invoke('attr', 'src')
11-
.should('contain', 'freecodecamp.org/radio')
9+
.should('contain', '.mp3')
1210
.then(() => {
13-
cy.get('#toggle-play-pause')
14-
.should('be.visible')
15-
.click();
11+
cy.get('#toggle-play-pause').should('be.visible').click();
1612
cy.get('audio').should(audioElements => {
1713
const audioIsPaused = audioElements[0].paused;
1814
expect(audioIsPaused).to.eq(false);

package-lock.json

Lines changed: 3096 additions & 2521 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@
1212
"@fortawesome/react-fontawesome": "0.2.0",
1313
"@sentry/react": "5.30.0",
1414
"@sentry/tracing": "5.30.0",
15-
"nchan": "1.0.10",
16-
"react": "16.14.0",
15+
"react": "18.2.0",
1716
"react-device-detect": "2.2.3",
18-
"react-dom": "16.14.0",
17+
"react-dom": "18.2.0",
1918
"react-page-visibility": "6.4.0",
2019
"react-scripts": "5.0.1",
2120
"store": "2.0.12"

src/components/App.js

Lines changed: 80 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,29 @@
11
import React from 'react';
2-
import NchanSubscriber from 'nchan';
32
import * as Sentry from '@sentry/react';
43
import store from 'store';
54
import { isIOS, isDesktop } from 'react-device-detect';
65

76
import Nav from './Nav';
87
import Main from './Main';
98
import Footer from './Footer';
9+
import { buildEventSource } from '../utils/buildEventSource';
1010

1111
import '../css/App.css';
1212

13-
const SUB = new NchanSubscriber(
14-
'wss://coderadio-admin.freecodecamp.org/api/live/nowplaying/coderadio'
15-
);
13+
const sseUri =
14+
'https://coderadio-admin-v2.freecodecamp.org/api/live/nowplaying/sse?cf_connect=%7B%22subs%22%3A%7B%22station%3Acoderadio%22%3A%7B%7D%2C%22global%3Atime%22%3A%7B%7D%7D%7D';
15+
const jsonUri = `https://coderadio-admin-v2.freecodecamp.org/api/nowplaying_static/coderadio.json`;
16+
17+
let sse = buildEventSource(sseUri);
18+
1619
const CODERADIO_VOLUME = 'coderadio-volume';
1720

18-
SUB.on('error', function (err, errDesc) {
21+
sse.onerror = ({ message, error }) => {
1922
Sentry.addBreadcrumb({
20-
message: 'NchanSubscriber error: ' + errDesc
23+
message: 'WebSocket error: ' + message
2124
});
22-
/**
23-
* I'm assuming captureException is appropriate here, I'm not sure what type
24-
* the first argument has.
25-
*/
26-
Sentry.captureException(err);
27-
});
25+
Sentry.captureException(error);
26+
};
2827

2928
export default class App extends React.Component {
3029
constructor(props) {
@@ -223,35 +222,28 @@ export default class App extends React.Component {
223222
}
224223

225224
play() {
226-
const { mounts, remotes, playing } = this.state;
227-
if (!playing) {
228-
if (!SUB.running) {
229-
SUB.start();
230-
}
225+
const { mounts, remotes } = this.state;
231226

232-
let streamUrls = Array.from(
233-
[...mounts, ...remotes],
234-
stream => stream.url
235-
);
227+
let streamUrls = Array.from([...mounts, ...remotes], stream => stream.url);
236228

237-
// Check if the url has been reset by pause
238-
if (!streamUrls.includes(this._player.src)) {
239-
this._player.src = this.state.url;
240-
this._player.load();
241-
}
242-
this._player.volume = 0;
243-
this._player.play().then(() => {
244-
this.setState(state => {
245-
return {
246-
audioConfig: { ...state.audioConfig, currentVolume: 0 },
247-
playing: true,
248-
pullMeta: true
249-
};
250-
});
229+
// Check if the url has been reset by pause
230+
if (!streamUrls.includes(this._player.src)) {
231+
this._player.src = this.state.url;
232+
this._player.load();
233+
}
251234

252-
this.fadeUp();
235+
this._player.volume = 0;
236+
this._player.play().then(() => {
237+
this.setState(state => {
238+
return {
239+
audioConfig: { ...state.audioConfig, currentVolume: 0 },
240+
playing: true,
241+
pullMeta: true
242+
};
253243
});
254-
}
244+
245+
this.fadeUp();
246+
});
255247
}
256248

257249
pause() {
@@ -268,7 +260,7 @@ export default class App extends React.Component {
268260
pausing: false
269261
},
270262
() => {
271-
SUB.stop();
263+
// socket.close();
272264
resolve();
273265
}
274266
);
@@ -453,41 +445,65 @@ export default class App extends React.Component {
453445
});
454446
}
455447

456-
getNowPlaying() {
457-
SUB.on('message', message => {
458-
let np = JSON.parse(message);
459-
460-
// We look through the available mounts to find the default mount
461-
if (this.state.url === '') {
448+
fetchJSON() {
449+
fetch(jsonUri)
450+
.then(response => {
451+
return response.json();
452+
})
453+
.then(np => {
462454
this.setState({
463455
mounts: np.station.mounts,
464-
remotes: np.station.remotes
465-
});
466-
this.setMountToConnection(np.station.mounts, np.station.remotes);
467-
}
468-
469-
if (this.state.listeners !== np.listeners.current) {
470-
this.setState({
471-
listeners: np.listeners.current
472-
});
473-
}
474-
475-
// We only need to update the metadata if the song has been changed
476-
if (
477-
np.now_playing.song.id !== this.state.currentSong.id ||
478-
this.state.pullMeta
479-
) {
480-
this.setState({
456+
remotes: np.station.remotes,
457+
listeners: np.listeners.current,
481458
currentSong: np.now_playing.song,
482459
songStartedAt: np.now_playing.played_at * 1000,
483460
songDuration: np.now_playing.duration,
484461
pullMeta: false,
485462
songHistory: np.song_history
486463
});
464+
this.setMountToConnection(np.station.mounts, np.station.remotes);
465+
})
466+
.catch(() => {});
467+
}
468+
469+
getNowPlaying() {
470+
// Since json recives data faster than sse, set the data initially
471+
this.fetchJSON();
472+
473+
// Reconnect Timeout needs to be added
474+
sse.onmessage = event => {
475+
const data = JSON.parse(event.data);
476+
const np = data?.pub?.data?.np || null;
477+
if (np) {
478+
// Process Now Playing data in `np` var.
479+
// We look through the available mounts to find the default mount
480+
if (this.state.url === '') {
481+
this.setState({
482+
mounts: np.station.mounts,
483+
remotes: np.station.remotes
484+
});
485+
this.setMountToConnection(np.station.mounts, np.station.remotes);
486+
}
487+
if (this.state.listeners !== np.listeners.current) {
488+
this.setState({
489+
listeners: np.listeners.current
490+
});
491+
}
492+
// We only need to update the metadata if the song has been changed
493+
if (
494+
np.now_playing.song.id !== this.state.currentSong.id ||
495+
this.state.pullMeta
496+
) {
497+
this.setState({
498+
currentSong: np.now_playing.song,
499+
songStartedAt: np.now_playing.played_at * 1000,
500+
songDuration: np.now_playing.duration,
501+
pullMeta: false,
502+
songHistory: np.song_history
503+
});
504+
}
487505
}
488-
});
489-
SUB.reconnectTimeout = this.state.config.metadataTimer;
490-
SUB.start();
506+
};
491507
}
492508

493509
increaseVolume = () =>

src/components/App.test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
import React from 'react';
22
import ReactDOM from 'react-dom';
33
import App from './App';
4+
import * as utils from '../utils/buildEventSource';
5+
6+
const buildEventSourceSpy = jest.spyOn(utils, 'buildEventSource');
7+
8+
buildEventSourceSpy.mockReturnValue({
9+
CLOSED: 0,
10+
CONNECTING: 0,
11+
OPEN: 0,
12+
dispatchEvent(event) {
13+
return false;
14+
},
15+
onerror: jest.fn(),
16+
onmessage: jest.fn(),
17+
onopen: jest.fn(),
18+
readyState: 0,
19+
url: '',
20+
withCredentials: false,
21+
addEventListener: jest.fn(),
22+
close: jest.fn(),
23+
removeEventListener: jest.fn()
24+
});
425

526
it('renders without crashing', () => {
627
const div = document.createElement('div');

src/components/Footer.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ export default class Footer extends React.PureComponent {
110110
currentVolume,
111111
setTargetVolume,
112112
listeners,
113-
fastConnection
113+
fastConnection,
114+
url
114115
} = this.props;
115116

116117
return (
@@ -131,7 +132,11 @@ export default class Footer extends React.PureComponent {
131132
playing={playing}
132133
songDuration={songDuration}
133134
/>
134-
<PlayPauseButton playing={playing} togglePlay={togglePlay} />
135+
<PlayPauseButton
136+
playing={playing}
137+
togglePlay={togglePlay}
138+
url={url}
139+
/>
135140
<Slider
136141
currentVolume={currentVolume}
137142
setTargetVolume={setTargetVolume}

src/components/PlayPauseButton.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
33
import { isBrowser } from 'react-device-detect';
4+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5+
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons';
46

57
import { ReactComponent as Pause } from '../assets/pause.svg';
68
import { ReactComponent as Play } from '../assets/play.svg';
@@ -23,7 +25,7 @@ class PlayPauseButton extends React.Component {
2325
}
2426

2527
render() {
26-
return (
28+
return this.props.url ? (
2729
<button
2830
aria-label={this.props.playing ? 'Pause' : 'Play'}
2931
className={
@@ -37,13 +39,21 @@ class PlayPauseButton extends React.Component {
3739
>
3840
{this.props.playing ? <Pause /> : <Play />}
3941
</button>
42+
) : (
43+
<FontAwesomeIcon
44+
aria-hidden='true'
45+
className='loader-circle-notch'
46+
icon={faCircleNotch}
47+
spin={true}
48+
/>
4049
);
4150
}
4251
}
4352

4453
PlayPauseButton.propTypes = {
4554
playing: PropTypes.bool,
46-
togglePlay: PropTypes.func
55+
togglePlay: PropTypes.func,
56+
url: PropTypes.string
4757
};
4858

4959
export default PlayPauseButton;

src/css/App.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ p {
124124
}
125125

126126
.slider {
127+
appearance: none;
127128
-webkit-appearance: none;
128129
background: #dfdfe2;
129130
height: 6px;
@@ -323,6 +324,12 @@ p {
323324
width: 60%;
324325
}
325326

327+
.loader-circle-notch{
328+
height: 60%;
329+
width: auto;
330+
align-self: center;
331+
}
332+
326333
.recent-song-list {
327334
background-color: #1b1b32;
328335
bottom: 70px;

src/utils/buildEventSource.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const buildEventSource = url => {
2+
return new EventSource(url);
3+
};

0 commit comments

Comments
 (0)