diff --git a/README.md b/README.md index 912012ff..fbd06c1b 100755 --- a/README.md +++ b/README.md @@ -58,7 +58,10 @@ import Winamp from 'winamp2-js'; const winamp = new Winamp({ initialTrack: { - name: "1. DJ Mike Llama - Llama Whippin' Intro", + metaData: { + artist: "DJ Mike Llama", + title: "Llama Whippin' Intro", + }, url: "https://d38dnrh1liu4f5.cloudfront.net/projects/winamp2-js/mp3/llama-2.91.mp3" }, initialSkin: { diff --git a/js/actionCreators.js b/js/actionCreators.js index 7c5efc4c..9c125f96 100644 --- a/js/actionCreators.js +++ b/js/actionCreators.js @@ -3,10 +3,11 @@ import { parser, creator } from "winamp-eqf"; import { genArrayBufferFromFileReference, genArrayBufferFromUrl, - promptForFileReferences + promptForFileReferences, + filenameFromUrl } from "./fileUtils"; import skinParser from "./skinParser"; -import { BANDS, TRACK_HEIGHT } from "./constants"; +import { BANDS, TRACK_HEIGHT, LOAD_STYLE } from "./constants"; import { getEqfData, nextTrack, @@ -77,7 +78,7 @@ export function play() { state.playlist.curentTrack == null && state.playlist.trackOrder.length === 0 ) { - dispatch(openFileDialog()); + dispatch(openMediaFileDialog()); } else { dispatch({ type: PLAY }); } @@ -196,20 +197,11 @@ function setEqFromFileReference(fileReference) { } export function addTracksFromReferences(fileReferences, autoPlay, atIndex) { - return dispatch => { - if (autoPlay) { - // I'm the worst. It just so happens that in every case that we autoPlay, - // we should also clear all tracks. - dispatch(removeAllTracks()); - } - Array.from(fileReferences).forEach((file, i) => { - const priority = i === 0 && autoPlay ? "PLAY" : "NONE"; - const id = uniqueId(); - const url = URL.createObjectURL(file); - dispatch(_addTrackFromUrl(url, file.name, id, priority, atIndex + i)); - dispatch(fetchMediaTags(file, id)); - }); - }; + const tracks = Array.from(fileReferences).map(file => ({ + blob: file, + defaultName: file.name + })); + return loadMediaFiles(tracks, autoPlay, atIndex); } const SKIN_FILENAME_MATCHER = new RegExp("(wsz|zip)$", "i"); @@ -245,8 +237,18 @@ export function fetchMediaDuration(url, id) { const { duration } = audio; dispatch({ type: SET_MEDIA_DURATION, duration, id }); audio.removeEventListener("durationchange", durationChange); + audio.url = null; + // TODO: Not sure if this really gets cleaned up. }; audio.addEventListener("durationchange", durationChange); + audio.addEventListener("error", () => { + // TODO: Should we update the state to indicate that we don't know the length? + /* Disabled, because people might drag in bogus local files. + Raven.captureMessage( + `Error getting duration of ${url}: ${audio.error.message}` + ); + */ + }); audio.src = url; }; } @@ -256,29 +258,74 @@ function uniqueId() { return counter++; } -function _addTrackFromUrl(url, name, id, priority, atIndex) { +/* +type Track = { + defaultName: ?string, + duration: ?number, + url?: string, + blob?: fileReference +} +*/ + +export function loadMediaFiles(tracks, autoPlay = true, atIndex = 0) { return dispatch => { - dispatch({ type: ADD_TRACK_FROM_URL, url, name, id, atIndex }); - switch (priority) { - case "BUFFER": - dispatch({ type: BUFFER_TRACK, name, id }); - break; - case "PLAY": - dispatch({ type: PLAY_TRACK, name, id }); - break; - default: - // If we're not going to load this right away, - // we should fetch duration on our own - dispatch(fetchMediaDuration(url, id)); + if (autoPlay) { + // I'm the worst. It just so happens that in every case that we autoPlay, + // we should also clear all tracks. + dispatch(removeAllTracks()); } + tracks.forEach((track, i) => { + const priority = i === 0 && autoPlay ? LOAD_STYLE.PLAY : null; + dispatch(loadMediaFile(track, priority, atIndex)); + }); }; } -export function loadMediaFromUrl(url, name, priority) { +export function loadMediaFile(track, priority = null, atIndex = 0) { return dispatch => { const id = uniqueId(); - dispatch(_addTrackFromUrl(url, name, id, priority)); - dispatch(fetchMediaTags(url, id)); + const { url, blob, defaultName, metaData, duration } = track; + let canonicalUrl = url; + let canonicalDefaultName = defaultName; + if (canonicalUrl == null) { + if (blob == null) { + throw new Error("Expected track to have either a blob or a url"); + } + canonicalUrl = URL.createObjectURL(track.blob); + } else if (!defaultName) { + canonicalDefaultName = filenameFromUrl(url); + // TODO: Derive the defaultName from the URL if none is provided + } + dispatch({ + type: ADD_TRACK_FROM_URL, + url: canonicalUrl, + name: canonicalDefaultName, + id, + atIndex + }); + switch (priority) { + case LOAD_STYLE.BUFFER: + dispatch({ type: BUFFER_TRACK, id }); + break; + case LOAD_STYLE.PLAY: + dispatch({ type: PLAY_TRACK, id }); + break; + default: + // If we're not going to load this right away, + // we should set duration on our own + if (duration != null) { + dispatch({ type: SET_MEDIA_DURATION, duration, id }); + } else { + dispatch(fetchMediaDuration(canonicalUrl, id)); + } + } + + if (metaData != null) { + const { artist, title } = metaData; + dispatch({ type: SET_MEDIA_TAGS, artist, title, id }); + } else { + dispatch(fetchMediaTags(url || blob, id)); + } }; } @@ -339,13 +386,36 @@ export function setSkinFromUrl(url) { }; } -export function openFileDialog(accept) { +// This function is private, since Winamp consumers may wish to support +// opening files via other means (Dropbox?). Only use the file type specific +// versions below, since they can defer to the user-defined behavior. +function _openFileDialog(accept) { return async dispatch => { const fileReferences = await promptForFileReferences(accept); dispatch(loadFilesFromReferences(fileReferences)); }; } +export function openEqfFileDialog() { + return _openFileDialog(".eqf"); +} + +export function openMediaFileDialog() { + return async (dispatch, getState, { promptForMediaFiles }) => { + if (promptForMediaFiles == null) { + return dispatch(_openFileDialog()); + } + const mediaFiles = await promptForMediaFiles(); + if (mediaFiles != null) { + return dispatch(loadMediaFiles(mediaFiles, true, null)); + } + }; +} + +export function openSkinFileDialog() { + return _openFileDialog(".zip, .wsz"); +} + export function setEqBand(band, value) { return { type: SET_BAND_VALUE, band, value }; } diff --git a/js/components/EqualizerWindow/PresetsContextMenu.js b/js/components/EqualizerWindow/PresetsContextMenu.js index b05d1911..177d26dd 100644 --- a/js/components/EqualizerWindow/PresetsContextMenu.js +++ b/js/components/EqualizerWindow/PresetsContextMenu.js @@ -1,18 +1,15 @@ import React from "react"; import { connect } from "react-redux"; -import { openFileDialog, downloadPreset } from "../../actionCreators"; +import { openEqfFileDialog, downloadPreset } from "../../actionCreators"; import { ContextMenu, Node } from "../ContextMenu"; const MainContextMenu = props => ( }> - + ); -const mapDispatchToProps = { - openFileDialog: () => openFileDialog(".eqf"), - downloadPreset -}; +const mapDispatchToProps = { openEqfFileDialog, downloadPreset }; export default connect(null, mapDispatchToProps)(MainContextMenu); diff --git a/js/components/MainWindow/Eject.js b/js/components/MainWindow/Eject.js index 84271ec8..ee8de7a3 100644 --- a/js/components/MainWindow/Eject.js +++ b/js/components/MainWindow/Eject.js @@ -1,12 +1,12 @@ import React from "react"; import { connect } from "react-redux"; -import { openFileDialog } from "../../actionCreators"; +import { openMediaFileDialog } from "../../actionCreators"; const Eject = props => ( -
+
); -const mapDispatchToProps = { openFileDialog }; +const mapDispatchToProps = { openMediaFileDialog }; export default connect(null, mapDispatchToProps)(Eject); diff --git a/js/components/MainWindow/MainContextMenu.js b/js/components/MainWindow/MainContextMenu.js index 1975d8b0..f9ff8757 100644 --- a/js/components/MainWindow/MainContextMenu.js +++ b/js/components/MainWindow/MainContextMenu.js @@ -1,7 +1,12 @@ import React from "react"; import { connect } from "react-redux"; import ClickedDiv from "../ClickedDiv"; -import { close, setSkinFromUrl, openFileDialog } from "../../actionCreators"; +import { + close, + setSkinFromUrl, + openMediaFileDialog, + openSkinFileDialog +} from "../../actionCreators"; import { ContextMenu, Hr, Node, Parent, LinkNode } from "../ContextMenu"; const MainContextMenu = props => ( @@ -16,9 +21,9 @@ const MainContextMenu = props => ( label="Winamp2-js" />
- + - + {!!props.avaliableSkins.length &&
} {props.avaliableSkins.map(skin => ( ({ const mapDispatchToProps = { close, - openFileDialogForSkin: () => openFileDialog(".zip, .wsz"), - openFileDialogForMedia: openFileDialog, + openSkinFileDialog, + openMediaFileDialog, setSkin: setSkinFromUrl }; diff --git a/js/components/PlaylistWindow/PlaylistActionArea.js b/js/components/PlaylistWindow/PlaylistActionArea.js index 29079ac5..6df655ee 100644 --- a/js/components/PlaylistWindow/PlaylistActionArea.js +++ b/js/components/PlaylistWindow/PlaylistActionArea.js @@ -7,7 +7,7 @@ import { stop, next, previous, - openFileDialog + openMediaFileDialog } from "../../actionCreators"; import MiniTime from "../MiniTime"; @@ -22,7 +22,10 @@ const PlaylistWindow = props => (
-
+
@@ -32,7 +35,7 @@ const mapDispatchToProps = { play, pause, stop, - openFileDialog, + openMediaFileDialog, next, previous }; diff --git a/js/components/PlaylistWindow/PlaylistShade.test.js b/js/components/PlaylistWindow/PlaylistShade.test.js index fed94aa4..f4aa24e0 100644 --- a/js/components/PlaylistWindow/PlaylistShade.test.js +++ b/js/components/PlaylistWindow/PlaylistShade.test.js @@ -2,7 +2,7 @@ import React from "react"; import { Provider } from "react-redux"; import renderer from "react-test-renderer"; import getStore from "../../store"; -import { loadMediaFromUrl } from "../../actionCreators"; +import { loadMediaFiles } from "../../actionCreators"; import PlaylistShade from "./PlaylistShade"; @@ -21,7 +21,9 @@ describe("PlaylistShade", () => { }); it("renders to snapshot", () => { - store.dispatch(loadMediaFromUrl("http://example.com", "Some Name", "NONE")); + store.dispatch( + loadMediaFiles([{ url: "http://example.com", defaultName: "Some Name" }]) + ); const tree = renderer .create( diff --git a/js/constants.js b/js/constants.js index 0211a04d..309fe8e1 100644 --- a/js/constants.js +++ b/js/constants.js @@ -6,6 +6,11 @@ export const WINDOWS = { EQUALIZER: "EQUALIZER" }; +export const LOAD_STYLE = { + BUFFER: "BUFFER", + PLAY: "PLAY" +}; + export const UTF8_ELLIPSIS = "\u2026"; export const CHARACTER_WIDTH = 5; export const PLAYLIST_RESIZE_SEGMENT_WIDTH = 25; diff --git a/js/fileUtils.js b/js/fileUtils.js index 09b8b3d7..4df5db70 100644 --- a/js/fileUtils.js +++ b/js/fileUtils.js @@ -47,3 +47,12 @@ export async function promptForFileReferences(accept) { fileInput.click(); }); } + +// This is not perfect, but... meh: https://stackoverflow.com/a/36756650/1263117 +export function filenameFromUrl(url) { + return url + .split("/") + .pop() + .split("#")[0] + .split("?")[0]; +} diff --git a/js/hotkeys.js b/js/hotkeys.js index c552bf94..9629b4a9 100644 --- a/js/hotkeys.js +++ b/js/hotkeys.js @@ -5,7 +5,7 @@ import { adjustVolume, toggleRepeat, toggleShuffle, - openFileDialog, + openMediaFileDialog, seekForward, seekBackward, reverseList, @@ -72,7 +72,7 @@ export default function(dispatch) { dispatch(pause()); break; case 76: // L - dispatch(openFileDialog()); + dispatch(openMediaFileDialog()); break; case 82: // R dispatch(toggleRepeat()); @@ -90,7 +90,7 @@ export default function(dispatch) { dispatch(previous()); break; case 96: // numpad 0 - dispatch(openFileDialog()); + dispatch(openMediaFileDialog()); break; case 97: // numpad 1 dispatch(nextN(-10)); diff --git a/js/index.js b/js/index.js index c09fffe1..ccc08a63 100644 --- a/js/index.js +++ b/js/index.js @@ -34,7 +34,11 @@ Raven.context(() => { url: skinUrl }, initialTrack: { - name: "DJ Mike Llama - Llama Whippin' Intro", + defaultName: "DJ Mike Llama - Llama Whippin' Intro", + metaData: { + artist: "DJ Mike Llama", + title: "Llama Whippin' Intro" + }, url: audioUrl }, avaliableSkins: [ diff --git a/js/winamp.js b/js/winamp.js index 6aee2868..36bf9547 100644 --- a/js/winamp.js +++ b/js/winamp.js @@ -6,7 +6,8 @@ import getStore from "./store"; import App from "./components/App"; import Hotkeys from "./hotkeys"; import Media from "./media"; -import { setSkinFromUrl, loadMediaFromUrl } from "./actionCreators"; +import { setSkinFromUrl, loadMediaFile } from "./actionCreators"; +import { LOAD_STYLE } from "./constants"; import { SET_AVALIABLE_SKINS } from "./actionTypes"; @@ -39,17 +40,14 @@ class Winamp { this.options = options; this.media = new Media(); - this.store = getStore(this.media, this.options.__initialState); + this.store = getStore(this.media, this.options); this.store.dispatch(setSkinFromUrl(this.options.initialSkin.url)); + // TODO: Make this initial track_s_ if (this.options.initialTrack && this.options.initialTrack.url) { this.store.dispatch( - loadMediaFromUrl( - this.options.initialTrack.url, - this.options.initialTrack.name, - "BUFFER" - ) + loadMediaFile(this.options.initialTrack, LOAD_STYLE.BUFFER) ); } if (this.options.avaliableSkins) {