Browse Source

Basic error mechanism in webplayer

Phyks (Lucas Verney) 5 years ago
parent
commit
23aa8b52ab

+ 41
- 11
app/actions/webplayer.js View File

@@ -4,6 +4,7 @@
4 4
 
5 5
 // Other actions
6 6
 import { decrementRefCount, incrementRefCount } from "./entities";
7
+import { i18nRecord } from "../models/i18n";
7 8
 
8 9
 
9 10
 export const PLAY_PAUSE = "PLAY_PAUSE";
@@ -251,8 +252,10 @@ export const TOGGLE_RANDOM = "TOGGLE_RANDOM";
251 252
  * @return  Dispatch a TOGGLE_RANDOM action.
252 253
  */
253 254
 export function toggleRandom() {
254
-    return {
255
-        type: TOGGLE_RANDOM,
255
+    return (dispatch) => {
256
+        dispatch({
257
+            type: TOGGLE_RANDOM,
258
+        });
256 259
     };
257 260
 }
258 261
 
@@ -264,8 +267,10 @@ export const TOGGLE_REPEAT = "TOGGLE_REPEAT";
264 267
  * @return  Dispatch a TOGGLE_REPEAT action.
265 268
  */
266 269
 export function toggleRepeat() {
267
-    return {
268
-        type: TOGGLE_REPEAT,
270
+    return (dispatch) => {
271
+        dispatch({
272
+            type: TOGGLE_REPEAT,
273
+        });
269 274
     };
270 275
 }
271 276
 
@@ -277,8 +282,10 @@ export const TOGGLE_MUTE = "TOGGLE_MUTE";
277 282
  * @return  Dispatch a TOGGLE_MUTE action.
278 283
  */
279 284
 export function toggleMute() {
280
-    return {
281
-        type: TOGGLE_MUTE,
285
+    return (dispatch) => {
286
+        dispatch({
287
+            type: TOGGLE_MUTE,
288
+        });
282 289
     };
283 290
 }
284 291
 
@@ -292,10 +299,33 @@ export const SET_VOLUME = "SET_VOLUME";
292 299
  * @return  Dispatch a SET_VOLUME action.
293 300
  */
294 301
 export function setVolume(volume) {
295
-    return {
296
-        type: SET_VOLUME,
297
-        payload: {
298
-            volume: volume,
299
-        },
302
+    return (dispatch) => {
303
+        dispatch({
304
+            type: SET_VOLUME,
305
+            payload: {
306
+                volume: volume,
307
+            },
308
+        });
309
+    };
310
+}
311
+
312
+
313
+export const SET_ERROR = "SET_ERROR";
314
+/**
315
+ * Set an error in case a song is not in a supported format.
316
+ *
317
+ * @return  Dispatch a SET_ERROR action.
318
+ */
319
+export function unsupportedMediaType() {
320
+    return (dispatch) => {
321
+        dispatch({
322
+            type: SET_ERROR,
323
+            payload: {
324
+                error: new i18nRecord({
325
+                    id: "app.webplayer.unsupported",
326
+                    values: {},
327
+                }),
328
+            },
329
+        });
300 330
     };
301 331
 }

+ 7
- 0
app/components/elements/WebPlayer.jsx View File

@@ -138,6 +138,12 @@ class WebPlayerCSSIntl extends Component {
138 138
                         </div>
139 139
                     </div>
140 140
 
141
+                    {
142
+                        this.props.error
143
+                            ? <div className="row text-center"><p>{this.props.error}</p></div>
144
+                            : null
145
+                    }
146
+
141 147
                     <div className="row text-center" styleName="controls">
142 148
                         <div className="col-xs-12">
143 149
                             <button styleName="prevBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.previous"])} title={formatMessage(webplayerMessages["app.webplayer.previous"])} onClick={onPrev} ref="prevBtn">
@@ -179,6 +185,7 @@ WebPlayerCSSIntl.propTypes = {
179 185
     volume: PropTypes.number.isRequired,
180 186
     currentIndex: PropTypes.number.isRequired,
181 187
     playlist: PropTypes.instanceOf(Immutable.List).isRequired,
188
+    error: PropTypes.string,
182 189
     currentSong: PropTypes.instanceOf(Immutable.Map),
183 190
     currentArtist: PropTypes.instanceOf(Immutable.Map),
184 191
     onPlayPause: PropTypes.func.isRequired,

+ 1
- 0
app/locales/en-US/index.js View File

@@ -51,5 +51,6 @@ module.exports = {
51 51
     "app.webplayer.previous": "Previous",  // Previous button description
52 52
     "app.webplayer.random": "Random",  // Random button description
53 53
     "app.webplayer.repeat": "Repeat",  // Repeat button description
54
+    "app.webplayer.unsupported": "Unsupported media type",  // "Unsupported media type",
54 55
     "app.webplayer.volume": "Volume",  // Volume button description
55 56
 };

+ 1
- 0
app/locales/fr-FR/index.js View File

@@ -51,5 +51,6 @@ module.exports = {
51 51
     "app.webplayer.previous": "Précédent",  // Previous button description
52 52
     "app.webplayer.random": "Aléatoire",  // Random button description
53 53
     "app.webplayer.repeat": "Répéter",  // Repeat button description
54
+    "app.webplayer.unsupported": "Format non supporté",  // "Unsupported media type",
54 55
     "app.webplayer.volume": "Volume",  // Volume button description
55 56
 };

+ 5
- 0
app/locales/messagesDescriptors/elements/WebPlayer.js View File

@@ -34,6 +34,11 @@ const messages = [
34 34
         defaultMessage: "Playlist",
35 35
         description: "Playlist button description",
36 36
     },
37
+    {
38
+        "id": "app.webplayer.unsupported",
39
+        "description": "Unsupported media type",
40
+        "defaultMessage": "Unsupported media type",
41
+    },
37 42
 ];
38 43
 
39 44
 export default messages;

+ 1
- 0
app/models/webplayer.js View File

@@ -15,4 +15,5 @@ export const stateRecord = new Immutable.Record({
15 15
     volume: 100,  /** Current volume, between 0 and 100 */
16 16
     currentIndex: 0,  /** Current index in the playlist */
17 17
     playlist: new Immutable.List(),  /** List of songs IDs, references songs in the entities store */
18
+    error: null,  /** An error string */
18 19
 });

+ 41
- 26
app/reducers/webplayer.js View File

@@ -25,6 +25,7 @@ import {
25 25
     TOGGLE_REPEAT,
26 26
     TOGGLE_MUTE,
27 27
     SET_VOLUME,
28
+    SET_ERROR,
28 29
     INVALIDATE_STORE } from "../actions";
29 30
 
30 31
 
@@ -35,25 +36,6 @@ import {
35 36
 var initialState = new stateRecord();
36 37
 
37 38
 
38
-/**
39
- * Helper functions
40
- */
41
-
42
-/**
43
- * Stop playback in reducer helper.
44
- *
45
- * @param   state   Current state to update.
46
- */
47
-function stopPlayback(state) {
48
-    return (
49
-        state
50
-        .set("isPlaying", false)
51
-        .set("currentIndex", 0)
52
-        .set("playlist", new Immutable.List())
53
-    );
54
-}
55
-
56
-
57 39
 /**
58 40
  * Reducers
59 41
  */
@@ -61,11 +43,21 @@ function stopPlayback(state) {
61 43
 export default createReducer(initialState, {
62 44
     [PLAY_PAUSE]: (state, payload) => {
63 45
         // Force play or pause
64
-        return state.set("isPlaying", payload.isPlaying);
46
+        return (
47
+            state
48
+            .set("isPlaying", payload.isPlaying)
49
+            .set("error", null)
50
+        );
65 51
     },
66 52
     [STOP_PLAYBACK]: (state) => {
67 53
         // Clear the playlist
68
-        return stopPlayback(state);
54
+        return (
55
+            state
56
+            .set("isPlaying", false)
57
+            .set("currentIndex", 0)
58
+            .set("playlist", new Immutable.List())
59
+            .set("error", null)
60
+        );
69 61
     },
70 62
     [SET_PLAYLIST]: (state, payload) => {
71 63
         // Set current playlist, reset playlist index
@@ -73,6 +65,7 @@ export default createReducer(initialState, {
73 65
             state
74 66
             .set("playlist", new Immutable.List(payload.playlist))
75 67
             .set("currentIndex", 0)
68
+            .set("error", null)
76 69
         );
77 70
     },
78 71
     [PUSH_SONG]: (state, payload) => {
@@ -113,6 +106,9 @@ export default createReducer(initialState, {
113 106
                 "currentIndex",
114 107
                 Math.max(newState.get("currentIndex") - 1, 0)
115 108
             );
109
+        } else if (payload.index == state.get("currentIndex")) {
110
+            // If we remove current song, clear the error as well
111
+            newState = newState.set("error", null);
116 112
         }
117 113
         return newState;
118 114
     },
@@ -127,9 +123,13 @@ export default createReducer(initialState, {
127 123
             // If there is an overlow on the left of the playlist, just play
128 124
             // first music again
129 125
             // TODO: Should seek to beginning of music
130
-            return state;
126
+            return state.set("error", null);
131 127
         } else {
132
-            return state.set("currentIndex", newIndex);
128
+            return (
129
+                state
130
+                .set("currentIndex", newIndex)
131
+                .set("error", null)
132
+            );
133 133
         }
134 134
     },
135 135
     [PLAY_NEXT_SONG]: (state) => {
@@ -138,14 +138,22 @@ export default createReducer(initialState, {
138 138
             // If there is an overflow
139 139
             if (state.get("isRepeat")) {
140 140
                 // TODO: Handle repeat
141
-                return state;
141
+                return state.set("error", null);
142 142
             } else {
143 143
                 // Just stop playback
144
-                return state.set("isPlaying", false);
144
+                return (
145
+                    state
146
+                    .set("isPlaying", false)
147
+                    .set("error", null)
148
+                );
145 149
             }
146 150
         } else {
147 151
             // Else, play next item
148
-            return state.set("currentIndex", newIndex);
152
+            return (
153
+                state
154
+                .set("currentIndex", newIndex)
155
+                .set("error", null)
156
+            );
149 157
         }
150 158
     },
151 159
     [TOGGLE_RANDOM]: (state) => {
@@ -160,6 +168,13 @@ export default createReducer(initialState, {
160 168
     [SET_VOLUME]: (state, payload) => {
161 169
         return state.set("volume", payload.volume);
162 170
     },
171
+    [SET_ERROR]: (state, payload) => {
172
+        return (
173
+            state
174
+            .set("isPlaying", false)
175
+            .set("error", payload.error)
176
+        );
177
+    },
163 178
     [INVALIDATE_STORE]: () => {
164 179
         return new stateRecord();
165 180
     },

+ 34
- 15
app/views/WebPlayer.jsx View File

@@ -2,7 +2,11 @@
2 2
 import React, { Component, PropTypes } from "react";
3 3
 import { bindActionCreators } from "redux";
4 4
 import { connect } from "react-redux";
5
-import { Howl } from "howler";
5
+import { defineMessages, injectIntl, intlShape } from "react-intl";
6
+import { Howler, Howl } from "howler";
7
+
8
+// Local imports
9
+import { messagesMap, handleErrorI18nObject } from "../utils";
6 10
 
7 11
 // Actions
8 12
 import * as actionCreators from "../actions";
@@ -10,11 +14,17 @@ import * as actionCreators from "../actions";
10 14
 // Components
11 15
 import WebPlayerComponent from "../components/elements/WebPlayer";
12 16
 
17
+// Translations
18
+import messages from "../locales/messagesDescriptors/elements/WebPlayer";
19
+
20
+// Define translations
21
+const webplayerMessages = defineMessages(messagesMap(Array.concat([], messages)));
22
+
13 23
 
14 24
 /**
15 25
  * Webplayer container.
16 26
  */
17
-class WebPlayer extends Component {
27
+class WebPlayerIntl extends Component {
18 28
     constructor(props) {
19 29
         super(props);
20 30
 
@@ -75,18 +85,22 @@ class WebPlayer extends Component {
75 85
     startPlaying(props) {
76 86
         if (props.isPlaying && props.currentSong) {
77 87
             // If it should be playing any song
78
-            // Build a new Howler object with current song to play
79 88
             const url = props.currentSong.get("url");
80
-            this.howl = new Howl({
81
-                src: [url],
82
-                html5: true,  // Use HTML5 by default to allow streaming
83
-                mute: props.isMute,
84
-                volume: props.volume / 100,  // Set current volume
85
-                autoplay: false,  // No autoplay, we handle it manually
86
-                onend: () => props.actions.playNextSong(),  // Play next song at the end
87
-            });
88
-            // Start playing
89
-            this.howl.play();
89
+            if (Howler.codecs(url.split(".").pop())) {
90
+                // Build a new Howler object with current song to play
91
+                this.howl = new Howl({
92
+                    src: [url],
93
+                    html5: true,  // Use HTML5 by default to allow streaming
94
+                    mute: props.isMute,
95
+                    volume: props.volume / 100,  // Set current volume
96
+                    autoplay: false,  // No autoplay, we handle it manually
97
+                    onend: () => props.actions.playNextSong(),  // Play next song at the end
98
+                });
99
+                // Start playing
100
+                this.howl.play();
101
+            } else {
102
+                this.props.actions.unsupportedMediaType();
103
+            }
90 104
         }
91 105
         else {
92 106
             // If it should not be playing
@@ -120,6 +134,8 @@ class WebPlayer extends Component {
120 134
     }
121 135
 
122 136
     render() {
137
+        const { formatMessage } = this.props.intl;
138
+
123 139
         const webplayerProps = {
124 140
             isPlaying: this.props.isPlaying,
125 141
             isRandom: this.props.isRandom,
@@ -128,6 +144,7 @@ class WebPlayer extends Component {
128 144
             volume: this.props.volume,
129 145
             currentIndex: this.props.currentIndex,
130 146
             playlist: this.props.playlist,
147
+            error: handleErrorI18nObject(this.props.error, formatMessage, webplayerMessages),
131 148
             currentSong: this.props.currentSong,
132 149
             currentArtist: this.props.currentArtist,
133 150
             // Use a lambda to ensure no first argument is passed to
@@ -151,8 +168,9 @@ class WebPlayer extends Component {
151 168
         );
152 169
     }
153 170
 }
154
-WebPlayer.propTypes = {
171
+WebPlayerIntl.propTypes = {
155 172
     location: PropTypes.object,
173
+    intl: intlShape.isRequired,
156 174
 };
157 175
 const mapStateToProps = (state) => {
158 176
     const currentIndex = state.webplayer.currentIndex;
@@ -172,6 +190,7 @@ const mapStateToProps = (state) => {
172 190
         volume: state.webplayer.volume,
173 191
         currentIndex: currentIndex,
174 192
         playlist: playlist,
193
+        error: state.webplayer.error,
175 194
         currentSong: currentSong,
176 195
         currentArtist: currentArtist,
177 196
     };
@@ -179,4 +198,4 @@ const mapStateToProps = (state) => {
179 198
 const mapDispatchToProps = (dispatch) => ({
180 199
     actions: bindActionCreators(actionCreators, dispatch),
181 200
 });
182
-export default connect(mapStateToProps, mapDispatchToProps)(WebPlayer);
201
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(WebPlayerIntl));

+ 2
- 0
scripts/extractTranslations.js View File

@@ -3,6 +3,8 @@
3 3
  * in the app, and generates a complete locale file for English.
4 4
  *
5 5
  * This script is meant to be run through `npm run extractTranslations`.
6
+ *
7
+ * TODO: Check that every identifier is actually used in the code.
6 8
  */
7 9
 import * as fs from 'fs';
8 10
 import {sync as globSync} from 'glob';