diff --git a/.eslintignore b/.eslintignore
index b514dfa..5980a24 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1 +1,2 @@
src/ui/**/*.js
+__data__
diff --git a/README.md b/README.md
index f3a3b36..453f933 100644
--- a/README.md
+++ b/README.md
@@ -22,12 +22,7 @@ So now that I found a solid match between a low level audio player, and an appli
## Install
-Playa delivers sound to your loudspeakers (and eventually to your ears) via [libgroove](https://github.com/andrewrk/libgroove).
-As I haven't found a way to pack it with the app itself yet, you have to install it via `brew`:
-
- $ brew install libgroove
-
-Then either [download the latest build from here](https://github.com/moonwave99/playa/releases), or build manually:
+Either [download the latest build from here](https://github.com/moonwave99/playa/releases), or build manually:
$ npm install
$ gulp release
diff --git a/package.json b/package.json
index f72f47d..d455334 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,13 @@
"url": "http://github.com/moonwave99/playa/raw/master/LICENSE.md"
}
],
+ "build": {
+ "asarUnpack": [
+ "**/node_modules/ffmpeg/*"
+ ]
+ },
"dependencies": {
+ "app-root-dir": "^1.0.2",
"async": "1.4.0",
"bluebird": "2.9.27",
"bootstrap-styl": "^4.0.4",
@@ -32,6 +38,7 @@
"emissary": "^1.3.1",
"enquire.js": "^2.1.1",
"express": "^4.13.4",
+ "ffbinaries": "^1.0.2",
"fluent-ffmpeg": "^2.0.1",
"font-awesome": "^4.5.0",
"fs-extra": "^0.20.1",
@@ -40,7 +47,6 @@
"fs-promise": "^1.0.0",
"fstream": "0.1.24",
"glob": "^5.0.10",
- "groove": "^2.4.0",
"i18next": "^6.0.2",
"js-yaml": "^3.3.1",
"keymaster": "^1.6.2",
@@ -57,8 +63,8 @@
"path-extra": "^3.0.0",
"property-accessors": "^1",
"react": "^0.14",
- "react-dnd": "^1.1.3",
- "react-dnd-html5-backend": "^2.1.2",
+ "react-dnd": "^2.4.0",
+ "react-dnd-html5-backend": "^2.4.1",
"react-dom": "^0.14.6",
"react-list": "^0.7.0",
"react-simpletabs": "^0.7.0",
@@ -72,7 +78,7 @@
"underscore-plus": "^1.6.1",
"vm-compatibility-layer": "0.1.0",
"walkdir": "0.0.10",
- "waveform": "^2.0.0",
+ "wavesurfer.js": "git@github.com:katspaugh/wavesurfer.js.git#2.0.0-beta01",
"yargs": "^1.3.3"
},
"devDependencies": {
@@ -80,9 +86,9 @@
"babel-preset-es2015-rollup": "^3.0.0",
"babel-preset-react": "^6.22.0",
"babel-register": "^6.22.0",
- "electron": "1.4.13",
- "electron-packager": "8.5.1",
- "electron-rebuild": "^1.5.7",
+ "electron": "1.6.11",
+ "electron-packager": "9.0.0",
+ "electron-rebuild": "^1.6.0",
"eslint": "^3.14.1",
"eslint-config-airbnb": "^13.0.0",
"eslint-plugin-import": "^2.2.0",
@@ -114,7 +120,7 @@
"test": "tape -r babel-register src/**/*.spec.js | faucet",
"eslint": "eslint --ext .js,.jsx src",
"postinstall": "make postinstall",
- "rebuild": "./node_modules/.bin/electron-rebuild -v 1.4.13",
+ "rebuild": "./node_modules/.bin/electron-rebuild -v 1.6.11",
"localInstall": "cp -r ./release/Playa-darwin-x64/Playa.app/ /Applications/Playa.app/"
},
"private": true
diff --git a/src/config/default.js b/src/config/default.js
index 3dc5867..6017d59 100644
--- a/src/config/default.js
+++ b/src/config/default.js
@@ -1,18 +1,16 @@
+const path = require('path');
+const appRootDir = require('app-root-dir').get();
+
export default {
- ffmpegPath: '/usr/local/bin/ffmpeg',
- ffprobePath: '/usr/local/bin/ffprobe',
+ ffmpegPath: path.join(appRootDir, 'node_modules/ffmpeg/ffmpeg'),
+ ffprobePath: path.join(appRootDir, 'node_modules/ffmpeg/ffprobe'),
coverFolderName: 'Covers',
- waveformFolderName: 'Waveforms',
playlistFolderName: 'Playlists',
coverLoaderLog: false,
- waveformLoader: {
- log: false,
- wait: 300,
- 'png-width': 1600,
- 'png-height': 160,
- 'png-color-bg': '00000000',
- 'png-color-center': '505050FF',
- 'png-color-outer': '505050FF',
+ wavesurfer: {
+ waveColor: '#7f7f7f',
+ progressColor: '#bfbfbf',
+ height: 140,
},
fileExtensions: ['mp3', 'm4a', 'flac', 'ogg'],
playlistExtension: '.yml',
diff --git a/src/playa.js b/src/playa.js
index 1476645..93fa38d 100644
--- a/src/playa.js
+++ b/src/playa.js
@@ -1,4 +1,4 @@
-import { omit, map } from 'lodash';
+import { map } from 'lodash';
import fs from 'fs-extra';
import fsPlus from 'fs-plus';
import md5 from 'md5';
@@ -15,7 +15,6 @@ import AlbumPlaylist from './renderer/util/AlbumPlaylist';
import PlaylistLoader from './renderer/util/PlaylistLoader';
import MediaFileLoader from './renderer/util/MediaFileLoader';
import CoverLoader from './renderer/util/CoverLoader';
-import WaveformLoader from './renderer/util/WaveformLoader';
import LastFMClient from './renderer/util/LastFMClient';
import { formatTimeShort as formatTime } from './renderer/util/helpers/formatters';
import AppDispatcher from './renderer/dispatcher/AppDispatcher';
@@ -99,7 +98,6 @@ export default class Playa {
scrobbleThreshold: this.getSetting('config', 'lastFM').scrobbleThreshold,
storeFolders: {
covers: this.getSetting('config', 'coverFolderName'),
- waveforms: this.getSetting('config', 'waveformFolderName'),
playlists: this.getSetting('config', 'playlistFolderName'),
},
},
@@ -173,13 +171,6 @@ export default class Playa {
}),
});
- const waveformSettings = this.getSetting('config', 'waveformLoader');
- this.waveformLoader = new WaveformLoader({
- root: path.join(options.userDataFolder, this.getSetting('common', 'storeFolders').waveforms),
- enableLog: waveformSettings.log,
- config: omit(waveformSettings, 'log'),
- });
-
this.openPlaylistManager = new OpenPlaylistManager({
loader: this.playlistLoader,
mediaFileLoader: this.mediaFileLoader,
@@ -198,12 +189,15 @@ export default class Playa {
mediaFileLoader: this.mediaFileLoader,
resolution: 1000,
scrobbleThreshold: this.getSetting('common', 'scrobbleThreshold'),
+ audioElement: options.audioElement,
});
+ ffmpeg.setFfmpegPath(this.getSetting('config', 'ffmpegPath'));
ffmpeg.setFfprobePath(this.getSetting('config', 'ffprobePath'));
this._onOpenPlaylistChange = this._onOpenPlaylistChange.bind(this);
this.saveSetting = this.saveSetting.bind(this);
+ this.toggleSidebar = this.toggleSidebar.bind(this);
}
init() {
this.firstPlaylistLoad = false;
@@ -459,7 +453,11 @@ export default class Playa {
}
render() {
ReactDOM.render(
- React.createElement(Main, this.settings.ui.all()),
+ React.createElement(Main, Object.assign({
+ lastFMClient: this.lastFMClient,
+ toggleSidebar: this.toggleSidebar,
+ wavesurferSettings: this.getSetting('config', 'wavesurfer'),
+ }, this.settings.ui.all())),
document.getElementById('main'),
);
this.postRender();
diff --git a/src/renderer/components/Main.jsx b/src/renderer/components/Main.jsx
index 0521c82..4c8fcf4 100644
--- a/src/renderer/components/Main.jsx
+++ b/src/renderer/components/Main.jsx
@@ -161,7 +161,7 @@ class Main extends Component {
enquire.register(minWidth(this.props.breakpoints.widescreen), {
match: () => {
if (this.state.settings.user.openSidebar) {
- playa.toggleSidebar(true);
+ this.props.toggleSidebar(true);
}
},
unmatch: () => {},
@@ -202,9 +202,11 @@ class Main extends Component {
return (
-
+
@@ -236,6 +238,15 @@ Main.propTypes = {
widescreen: PropTypes.string,
widefont: PropTypes.string,
}),
+ lastFMClient: PropTypes.shape({
+
+ }),
+ toggleSidebar: PropTypes.func,
+ wavesurferSettings: PropTypes.shape({
+ waveColor: PropTypes.String,
+ progressColor: PropTypes.String,
+ height: PropTypes.Number,
+ }),
};
export default dragDropContext(HTML5Backend)(Main);
diff --git a/src/renderer/components/player/PlaybackBar.jsx b/src/renderer/components/player/PlaybackBar.jsx
index f4e47c9..1d1d99e 100644
--- a/src/renderer/components/player/PlaybackBar.jsx
+++ b/src/renderer/components/player/PlaybackBar.jsx
@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import React, { Component, PropTypes } from 'react';
import cx from 'classnames';
import { formatTimeShort as formatTime } from '../../util/helpers/formatters';
import PlayerStore from '../../stores/PlayerStore';
@@ -115,7 +115,10 @@ class PlaybackBar extends Component {
-
+
{ this.renderCover() }
{
+ this.waveform.classList.add('loaded');
+ });
+ }
componentDidUpdate(prevProps) {
- if (!this.props.currentTrack) {
- this.updateWaveform(null);
- } else if (
- this.props.currentTrack
- && (!prevProps.currentTrack || prevProps.currentTrack.id !== this.props.currentTrack.id)
- ) {
- this.updateWaveform(null);
- playa.waveformLoader.load(this.props.currentTrack)
- .then(this.updateWaveform)
- .catch( err => console.error(err, err.stack)); // eslint-disable-line
- }
+ this.updateWaveform(this.props.currentTrack, prevProps.currentTrack);
+ }
+ componentWillUnmount() {
+ this.wavesurfer.unAll();
}
handleMouseEnter() {
this.cursor.style.opacity = '1';
@@ -46,12 +51,15 @@ class ProgressBar extends Component {
const percent = ((event.clientX - waveformBounds.left) / waveformBounds.width) * 100;
this.cursor.style.left = `${percent}%`;
}
- updateWaveform(waveform) {
- if (waveform) {
- this.waveform.style.backgroundImage = `url('file://${encodeURI(waveform)}')`;
- this.waveform.classList.add('loaded');
- } else {
+ updateWaveform(currentTrack, prevTrack) {
+ if (!currentTrack) {
+ this.wavesurfer.empty();
this.waveform.classList.remove('loaded');
+ } else if (
+ currentTrack
+ && (!prevTrack || prevTrack.id !== currentTrack.id)
+ ) {
+ this.wavesurfer.load(currentTrack.filename);
}
}
render() {
@@ -93,6 +101,11 @@ ProgressBar.propTypes = {
id: PropTypes.string,
}),
playing: PropTypes.bool,
+ wavesurferSettings: PropTypes.shape({
+ waveColor: PropTypes.String,
+ progressColor: PropTypes.String,
+ height: PropTypes.Number,
+ }),
};
export default ProgressBar;
diff --git a/src/renderer/components/playlist/AlbumPlaylistItem.jsx b/src/renderer/components/playlist/AlbumPlaylistItem.jsx
index 4259b3f..fe493db 100644
--- a/src/renderer/components/playlist/AlbumPlaylistItem.jsx
+++ b/src/renderer/components/playlist/AlbumPlaylistItem.jsx
@@ -89,7 +89,6 @@ class AlbumPlaylistItem extends Component {
.catch(() => {});
}
getDisabledContextMenuActions() {
- const folder = this.props.album.getFolder();
const label = i18n.t('playlist.album.contextMenu.locateFolderShort');
return [
{
diff --git a/src/renderer/main.js b/src/renderer/main.js
index 8df3937..f2752e5 100644
--- a/src/renderer/main.js
+++ b/src/renderer/main.js
@@ -6,6 +6,7 @@ window.playa = new Playa({
userDataFolder: ipc.sendSync('request:app:path', { key: 'userData' }),
sessionInfo: ipc.sendSync('request:session:settings'),
config: config(process.NODE_ENV),
+ audioElement: document.getElementById('audio'),
});
window.playa.init();
diff --git a/src/renderer/util/Album.js b/src/renderer/util/Album/Album.js
similarity index 56%
rename from src/renderer/util/Album.js
rename to src/renderer/util/Album/Album.js
index 845c970..cb26b32 100644
--- a/src/renderer/util/Album.js
+++ b/src/renderer/util/Album/Album.js
@@ -1,34 +1,45 @@
-import _, { find, uniq, findWhere } from 'lodash';
+import _, { find, findIndex, uniq, findWhere } from 'lodash';
import path from 'path';
-import AlbumConstants from '../constants/AlbumConstants';
+import AlbumConstants from '../../constants/AlbumConstants';
-const isCompilation = function isCompilation(album) {
+export const isCompilation = function isCompilation(album) {
return (
album.tracks[0].metadata.albumartist
&& album.tracks[0].metadata.albumartist.match(/various/i)
) || album._artists.length > AlbumConstants.VARIOUS_ARTISTS_THRESHOLD;
};
+export const isMultiple = function isMultiple(album) {
+ return uniq(album.tracks.map(t => t.getDiscNumber())).length > 1;
+};
+
+export const findValue = function findValue(tracks, key, defaultValue) {
+ const foundTrack = find(tracks, t => t.metadata[key]);
+ if (foundTrack) {
+ return foundTrack.metadata[key];
+ }
+ return defaultValue;
+};
+
export default class Album {
constructor({ id, tracks = [], disabled = false }) {
this.id = id;
this.tracks = tracks;
this._folder = this.tracks.length && path.dirname(this.tracks[0].filename);
this.disabled = disabled;
+ this._artists = [];
if (this.disabled) {
- this._artists = [];
return;
}
- this._title = find(this.tracks, t => t.metadata.album).metadata.album
- || AlbumConstants.NO_ALBUM;
- this._year = +find(this.tracks, t => t.metadata.year).metadata.year;
+ this._title = findValue(this.tracks, 'album', AlbumConstants.NO_ALBUM);
+ this._year = findValue(this.tracks, 'year', 0);
this._artists = _(this.tracks.map(t => t.metadata.artist))
.uniq(a => (a || '').toLowerCase())
.compact()
.value();
this._isCompilation = isCompilation(this);
- this._isSplit = this._artists.length > 1 && !this._isCompilation;
- this._isMultiple = uniq(this.tracks.map(t => t.getDiscNumber())).length > 1;
+ this._isMultiple = isMultiple(this);
+ this._isSplit = this.getArtistCount() > 1 && !this.isCompilation();
}
contains(id) {
return this.tracks.map(i => i.id).indexOf(id) > -1;
@@ -36,11 +47,25 @@ export default class Album {
findById(id) {
return findWhere(this.tracks, { id });
}
+ findNextById(id) {
+ const currentIndex = findIndex(this.tracks, { id });
+ if (currentIndex === this.tracks.length - 1) {
+ return null;
+ }
+ return this.tracks[currentIndex + 1];
+ }
+ findPrevById(id) {
+ const currentIndex = findIndex(this.tracks, { id });
+ if (currentIndex === 0) {
+ return null;
+ }
+ return this.tracks[currentIndex - 1];
+ }
isCompilation() {
- return !!this._isCompilation;
+ return this._isCompilation;
}
isMultiple() {
- return !!this._isMultiple;
+ return this._isMultiple;
}
getTitle() {
return this._title;
@@ -49,7 +74,9 @@ export default class Album {
return this._artists.length;
}
getArtist() {
- return this._isCompilation ? AlbumConstants.VARIOUS_ARTISTS_LABEL : this._artists.join(', ');
+ return this._isCompilation
+ ? AlbumConstants.VARIOUS_ARTISTS_LABEL
+ : this._artists.join(', ');
}
getYear() {
return this._year;
diff --git a/src/renderer/util/Album/Album.spec.js b/src/renderer/util/Album/Album.spec.js
new file mode 100644
index 0000000..5f76993
--- /dev/null
+++ b/src/renderer/util/Album/Album.spec.js
@@ -0,0 +1,38 @@
+import test from 'tape';
+import Album from './';
+import PlaylistItem from '../PlaylistItem';
+import data from './__data__/album';
+
+test('Album # contains()', (assert) => {
+ const album = new Album({
+ id: data.id,
+ tracks: data.tracks.map(track => new PlaylistItem(track)),
+ });
+ assert.equal(
+ album.contains(data.tracks[0].id),
+ true,
+ 'should return true if album contains given track id',
+ );
+ assert.equal(
+ album.contains('unexisting_id'),
+ false,
+ 'should return false if album does not contain given track id',
+ );
+ assert.end();
+});
+
+test('Album # missingTracksCount()', (assert) => {
+ const tracks = [1, 2, 3]
+ .map(n => ({ filename: `path/to/filename_${n}.mp3`, disabled: n % 2 }))
+ .map(track => new PlaylistItem(track));
+ const album = new Album({
+ id: data.id,
+ tracks,
+ });
+ assert.equal(
+ album.missingTracksCount(),
+ tracks.filter(track => track.disabled).length,
+ 'should return the number of disabled tracks',
+ );
+ assert.end();
+});
diff --git a/src/renderer/util/Album/__data__/album.js b/src/renderer/util/Album/__data__/album.js
new file mode 100644
index 0000000..9470db6
--- /dev/null
+++ b/src/renderer/util/Album/__data__/album.js
@@ -0,0 +1,193 @@
+export default {
+ "id": "a_39b14144926b97bc29145217ed0bb74a",
+ "tracks": [{
+ "metadata": {
+ "duration": 320,
+ "picture": [],
+ "disk": {
+ "no": 0,
+ "of": 0
+ },
+ "genre": ["Rock"],
+ "track": 1,
+ "year": "1994",
+ "album": "Submarine",
+ "albumartist": "",
+ "artist": "Submar㏌e",
+ "title": "I Can't be Satisfied"
+ },
+ "duration": 320,
+ "filename": "/Users/mwlabs/Downloads/_muzak/_slsk/complete/S/Submarine/[Album]/1994 - Submarine/01 - I Can't be Satisfied.mp3",
+ "disabled": false,
+ "id": "t_94f493bf401f5061e0186723ddf3d8f2",
+ "audioMetadata": null
+ }, {
+ "metadata": {
+ "duration": 237,
+ "picture": [],
+ "disk": {
+ "no": 0,
+ "of": 0
+ },
+ "genre": ["Rock"],
+ "track": 2,
+ "year": "1994",
+ "album": "Submarine",
+ "albumartist": "",
+ "artist": "Submar㏌e",
+ "title": "Electric Bathing"
+ },
+ "duration": 237,
+ "filename": "/Users/mwlabs/Downloads/_muzak/_slsk/complete/S/Submarine/[Album]/1994 - Submarine/02 - Electric Bathing.mp3",
+ "disabled": false,
+ "id": "t_094bf74280e247d6f691d221a9c2f528",
+ "audioMetadata": null
+ }, {
+ "metadata": {
+ "duration": 240,
+ "picture": [],
+ "disk": {
+ "no": 0,
+ "of": 0
+ },
+ "genre": ["Rock"],
+ "track": 3,
+ "year": "1994",
+ "album": "Submarine",
+ "albumartist": "",
+ "artist": "Submar㏌e",
+ "title": "Jnr. Elvis"
+ },
+ "duration": 240,
+ "filename": "/Users/mwlabs/Downloads/_muzak/_slsk/complete/S/Submarine/[Album]/1994 - Submarine/03 - Jnr. Elvis.mp3",
+ "disabled": false,
+ "id": "t_249cec33cad8c9d79d5705e434d8f5c4",
+ "audioMetadata": null
+ }, {
+ "metadata": {
+ "duration": 227,
+ "picture": [],
+ "disk": {
+ "no": 0,
+ "of": 0
+ },
+ "genre": ["Rock"],
+ "track": 4,
+ "year": "1994",
+ "album": "Submarine",
+ "albumartist": "",
+ "artist": "Submar㏌e",
+ "title": "Empty"
+ },
+ "duration": 227,
+ "filename": "/Users/mwlabs/Downloads/_muzak/_slsk/complete/S/Submarine/[Album]/1994 - Submarine/04 - Empty.mp3",
+ "disabled": false,
+ "id": "t_9824bbcc5a05a95ee4f6df40f49b7fc3",
+ "audioMetadata": null
+ }, {
+ "metadata": {
+ "duration": 378,
+ "picture": [],
+ "disk": {
+ "no": 0,
+ "of": 0
+ },
+ "genre": ["Rock"],
+ "track": 5,
+ "year": "1994",
+ "album": "Submarine",
+ "albumartist": "",
+ "artist": "Submar㏌e",
+ "title": "Lips and Fingers"
+ },
+ "duration": 378,
+ "filename": "/Users/mwlabs/Downloads/_muzak/_slsk/complete/S/Submarine/[Album]/1994 - Submarine/05 - Lips and Fingers.mp3",
+ "disabled": false,
+ "id": "t_09870578d5d358b01b0ffbe628dd1bf1",
+ "audioMetadata": null
+ }, {
+ "metadata": {
+ "duration": 201,
+ "picture": [],
+ "disk": {
+ "no": 0,
+ "of": 0
+ },
+ "genre": ["Rock"],
+ "track": 6,
+ "year": "1994",
+ "album": "Submarine",
+ "albumartist": "",
+ "artist": "Submar㏌e",
+ "title": "Never be Alright Again"
+ },
+ "duration": 201,
+ "filename": "/Users/mwlabs/Downloads/_muzak/_slsk/complete/S/Submarine/[Album]/1994 - Submarine/06 - Never be Alright Again.mp3",
+ "disabled": false,
+ "id": "t_ea115bb9fdf20df97a0144a2aa9e1bf0",
+ "audioMetadata": null
+ }, {
+ "metadata": {
+ "duration": 284,
+ "picture": [],
+ "disk": {
+ "no": 0,
+ "of": 0
+ },
+ "genre": ["Rock"],
+ "track": 7,
+ "year": "1994",
+ "album": "Submarine",
+ "albumartist": "",
+ "artist": "Submar㏌e",
+ "title": "Fading"
+ },
+ "duration": 284,
+ "filename": "/Users/mwlabs/Downloads/_muzak/_slsk/complete/S/Submarine/[Album]/1994 - Submarine/07 - Fading.mp3",
+ "disabled": false,
+ "id": "t_9abca68968751e60e76df53ad34174fa",
+ "audioMetadata": null
+ }, {
+ "metadata": {
+ "duration": 453,
+ "picture": [],
+ "disk": {
+ "no": 0,
+ "of": 0
+ },
+ "genre": ["Rock"],
+ "track": 8,
+ "year": "1994",
+ "album": "Submarine",
+ "albumartist": "",
+ "artist": "Submar㏌e",
+ "title": "Jodie Foster"
+ },
+ "duration": 453,
+ "filename": "/Users/mwlabs/Downloads/_muzak/_slsk/complete/S/Submarine/[Album]/1994 - Submarine/08 - Jodie Foster.mp3",
+ "disabled": false,
+ "id": "t_c98132f453ee30211bb67c96ed1910e5",
+ "audioMetadata": null
+ }, {
+ "metadata": {
+ "duration": 335,
+ "picture": [],
+ "disk": {
+ "no": 0,
+ "of": 0
+ },
+ "genre": ["Rock"],
+ "track": 9,
+ "year": "1994",
+ "album": "Submarine",
+ "albumartist": "",
+ "artist": "Submar㏌e",
+ "title": "Alright Sunshine Song"
+ },
+ "duration": 335,
+ "filename": "/Users/mwlabs/Downloads/_muzak/_slsk/complete/S/Submarine/[Album]/1994 - Submarine/09 - Alright Sunshine Song.mp3",
+ "disabled": false,
+ "id": "t_74b967b462760e1976413ea31ccd0616",
+ "audioMetadata": null
+ }]
+};
diff --git a/src/renderer/util/Album/__data__/generator.js b/src/renderer/util/Album/__data__/generator.js
new file mode 100644
index 0000000..1927768
--- /dev/null
+++ b/src/renderer/util/Album/__data__/generator.js
@@ -0,0 +1,35 @@
+import md5 from 'md5';
+
+const generateTrack = function generateTrack({
+ album,
+ artist,
+ title,
+ ext = '.mp3',
+ track = 1,
+ year = 1999,
+ diskNo = 0,
+ diskOf = 0,
+ genre = 'Rock',
+ duration = 194,
+}) {
+ const filename = `/path/to/${artist}/${album}/${track} - ${title}${ext}`
+ const id = `t_${md5(filename)}`;
+ return {
+ id,
+ filename,
+ metadata: {
+ album,
+ artist,
+ albumartist,
+ duration,
+ title,
+ track,
+ year,
+ genre,
+ disk: {
+ no: diskNo,
+ of: diskOf,
+ },
+ },
+ };
+};
diff --git a/src/renderer/util/Album/index.js b/src/renderer/util/Album/index.js
new file mode 100644
index 0000000..7f90b07
--- /dev/null
+++ b/src/renderer/util/Album/index.js
@@ -0,0 +1,3 @@
+import Album from './Album';
+
+export default Album;
diff --git a/src/renderer/util/Player.js b/src/renderer/util/Player.js
index cdcb297..dc4dd84 100644
--- a/src/renderer/util/Player.js
+++ b/src/renderer/util/Player.js
@@ -1,56 +1,18 @@
import { EventEmitter } from 'events';
import Promise from 'bluebird';
-import groove from 'groove';
-import md5 from 'md5';
-import { find, findIndex } from 'lodash';
-
-groove.setLogging(groove.LOG_ERROR);
-
-const formatTrackId = function formatTrackId(filename) {
- return `t_${md5(filename)}`;
-};
-
-const closeFile = function closeFile(file) {
- return new Promise((resolve, reject) => {
- const filename = file.filename;
- file.close((err) => {
- if (err) {
- reject(err);
- } else {
- resolve(filename);
- }
- });
- });
-};
-
-const openTrack = function openTrack(filename) {
- return new Promise((resolve, reject) =>
- groove.open(filename, (err, file) => {
- if (err) {
- reject(err);
- } else {
- resolve(file);
- }
- }),
- );
-};
export default class Player extends EventEmitter {
constructor({
mediaFileLoader,
resolution = 1000,
scrobbleThreshold,
+ audioElement,
}) {
super();
this.mediaFileLoader = mediaFileLoader;
this.resolution = resolution;
this.scrobbleThreshold = scrobbleThreshold;
- this.player = groove.createPlayer();
- this.player.useExactAudioFormat = true;
- this.player.on('nowplaying', this.onNowplaying.bind(this));
- this.groovePlaylist = groove.createPlaylist();
this.timer = null;
- this.attached = false;
this.loading = false;
this.playing = false;
this.alreadyScrobbled = false;
@@ -61,6 +23,10 @@ export default class Player extends EventEmitter {
this.lastAction = null;
this.playbackDirection = 0;
this.currentTrackPlayedAmount = 0;
+ this.player = audioElement;
+ this.player.onplaying = this.onPlaying.bind(this);
+ this.player.onpause = this.onPause.bind(this);
+ this.player.onended = this.onEnded.bind(this);
}
startTimer() {
if (this.timer) {
@@ -91,151 +57,69 @@ export default class Player extends EventEmitter {
this.alreadyScrobbled = true;
}
}
- attach() {
- return new Promise((resolve, reject) => {
- if (this.attached) {
- resolve(true);
- } else if (!this.groovePlaylist) {
- reject(new Error('No playlist set!'));
- } else {
- this.player.attach(this.groovePlaylist, (err) => {
- if (err) {
- reject(err);
- } else {
- this.attached = true;
- resolve(true);
- }
- });
- }
- });
- }
- detach() {
- return new Promise((resolve, reject) => {
- if (!this.attached) {
- resolve(true);
- } else if (!this.groovePlaylist) {
- reject(new Error('No playlist to detach!'));
- } else {
- this.player.detach((err) => {
- if (err) {
- reject(err);
- } else {
- this.attached = false;
- resolve(true);
- }
- });
- }
- });
+ onPause() {
+ this.playing = false;
+ this.clearTimer();
+ this.emit('nowplaying');
}
- onNowplaying() {
- if (this.loading) {
- return;
- }
- const current = this.groovePlaylist.position();
- if (current.item) {
- this.alreadyScrobbled = false;
- if (!this.lastPlayedTrack) {
- this.lastPlayedTrack = current.item.file.metadata();
- } else {
- const _lastPlayedTrack = current.item.file.metadata();
- this.playbackDirection = this.lastPlayedTrack.track <= _lastPlayedTrack.track ? 1 : -1;
- this.lastPlayedTrack = _lastPlayedTrack;
- }
- if (!this.timer) {
- this.startTimer();
- }
- this.currentTrack = this.currentAlbum.findById(formatTrackId(current.item.file.filename));
- this.emit('nowplaying');
- } else if (this.playbackDirection === 0) {
- if (this.lastAction === 'prev') {
- this.prevAlbum();
- } else {
- this.nextAlbum();
- }
- } else if (this.playbackDirection > 0) {
- this.prevAlbum();
- } else {
- this.nextAlbum();
+ onPlaying() {
+ if (!this.timer) {
+ this.startTimer();
}
+ this.playing = true;
+ this.emit('nowplaying');
+ }
+ onEnded() {
+ this.nextTrack();
}
playbackInfo() {
- if (!this.groovePlaylist) {
- return null;
- }
- const info = this.groovePlaylist.position();
return {
- position: info.pos,
+ position: this.player.currentTime,
playing: this.playing,
currentTrack: this.currentTrack,
currentAlbum: this.currentAlbum,
};
}
play() {
- this.attach().then(() => {
- if (this.groovePlaylist.count() === 0) {
- return false;
- }
- this.startTimer();
- this.playing = true;
- this.groovePlaylist.play();
- return true;
- });
+ this.player.play();
}
pause() {
- this.groovePlaylist.pause();
- this.playing = false;
- this.clearTimer();
+ this.player.pause();
}
nextTrack() {
this.lastAction = 'next';
- const items = this.groovePlaylist.items();
- const current = this.groovePlaylist.position();
- const currentIndex = findIndex(items, item => item.id === current.item.id);
- if (currentIndex < items.length - 1) {
- this.clearTimer();
- this.groovePlaylist.seek(items[currentIndex + 1], 0);
- return true;
+ const nextTrack = this.currentAlbum.findNextById(this.currentTrack.id);
+ if (!nextTrack) {
+ return this.nextAlbum().then(() => {
+ this.gotoTrack(this.currentAlbum.tracks[0].id);
+ });
}
- return this.nextAlbum();
+ this.gotoTrack(nextTrack.id);
+ return true;
}
prevTrack() {
this.lastAction = 'prev';
- const items = this.groovePlaylist.items();
- const current = this.groovePlaylist.position();
- const currentIndex = findIndex(items, item => item.id === current.item.id);
- if (currentIndex > 0) {
- this.clearTimer();
- this.groovePlaylist.seek(items[currentIndex - 1], 0);
- return true;
+ const prevTrack = this.currentAlbum.findPrevById(this.currentTrack.id);
+ if (!prevTrack) {
+ return this.prevAlbum().then(() => {
+ this.gotoTrack(this.currentAlbum.tracks[0].id);
+ });
}
- return this.prevAlbum();
+ this.gotoTrack(prevTrack.id);
+ return true;
}
gotoTrack(id = this.currentAlbum.tracks[0].id) {
- const item = find(
- this.groovePlaylist.items(),
- _item => id === formatTrackId(_item.file.filename),
- );
- if (!item) {
+ const currentTrack = this.currentAlbum.findById(id);
+ if (!currentTrack) {
return;
}
- this.clearTimer();
- this.currentTrack = this.currentAlbum.findById(id);
+ this.currentTrack = currentTrack;
+ this.player.src = this.currentTrack.filename;
+ this.play();
this.emit('trackChange');
- this.groovePlaylist.seek(item, 0);
- if (!this.groovePlaylist.playing()) {
- this.play();
- }
}
seek(to) {
- if (!this.groovePlaylist) {
- return;
- }
- this.currentTrackPlayedAmount = 0;
- const current = this.groovePlaylist.position();
- const seekToSecond = current.item.file.duration() * to;
- if (current.item) {
- this.groovePlaylist.seek(current.item, seekToSecond);
- }
+ this.player.currentTime = this.player.duration * to;
}
nextAlbum() {
const nextAlbum = this.currentPlaylist.getNext(this.currentAlbum);
@@ -251,49 +135,15 @@ export default class Player extends EventEmitter {
}
return this.loadAlbum(prevAlbum);
}
- insert(file) {
- this.groovePlaylist.insert(file);
- if (this.playing && !this.attached) {
- this.groovePlaylist.pause();
- }
- }
- remove(file) {
- this.groovePlaylist.remove(file);
- }
- clearPlaylist() {
- const filesToClose = this.groovePlaylist.items().map(i => i.file);
- this.groovePlaylist.clear();
- return Promise.all(filesToClose.map(closeFile));
- }
- append(files) {
- files.forEach((file) => {
- if (!file) {
- return;
- }
- this.groovePlaylist.insert(file);
- });
- }
loadAlbum(album) {
const isNewAlbum = !this.currentAlbum || (this.currentAlbum.id !== album.id);
if (!isNewAlbum) {
this.emit('trackChange');
return Promise.resolve(this.currentAlbum);
}
- this.loading = true;
- return this.clearPlaylist()
- .then(() => Promise.settle(
- album.tracks.map(({ filename }) => openTrack(filename)),
- ))
- .then((files) => {
- this.currentAlbum = album;
- const resolvedFiles = files.filter(f => f.isFulfilled()).map(f => f.value());
- return this.append(resolvedFiles);
- })
- .then(() => {
- this.loading = false;
- this.clearTimer();
- this.emit('trackChange');
- return album;
- });
+ this.currentAlbum = album;
+ this.clearTimer();
+ this.emit('trackChange');
+ return Promise.resolve(album);
}
}
diff --git a/src/renderer/util/PlaylistItem/PlaylistItem.js b/src/renderer/util/PlaylistItem/PlaylistItem.js
index f48b6ec..2dc9ac2 100644
--- a/src/renderer/util/PlaylistItem/PlaylistItem.js
+++ b/src/renderer/util/PlaylistItem/PlaylistItem.js
@@ -2,7 +2,7 @@ import md5 from 'md5';
import AudioMetadata from '../AudioMetadata';
export default class PlaylistItem {
- constructor({ metadata = {}, duration = 0, filename, disabled = false }) {
+ constructor({ metadata = { disk: {} }, duration = 0, filename, disabled = false }) {
this.metadata = metadata;
this.duration = duration;
this.filename = filename;
diff --git a/src/renderer/util/WaveformLoader.js b/src/renderer/util/WaveformLoader.js
deleted file mode 100644
index 8f71c4e..0000000
--- a/src/renderer/util/WaveformLoader.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import path from 'path';
-import fs from 'fs-plus';
-import waveform from 'waveform';
-import { omit } from 'lodash';
-
-const OUTPUT_EXT = '.png';
-
-export default class WaveformLoader {
- constructor({ root, config = {}, enableLog = false }) {
- this.root = root;
- this.config = config;
- this.enableLog = enableLog;
- }
- load(track) {
- return new Promise((resolve, reject) => {
- let waveformPath = this.getCached(track);
- if (waveformPath) {
- setTimeout(
- () => resolve(waveformPath),
- this.config.wait,
- );
- } else {
- waveformPath = this.getWaveformPath(track);
- const waveformOptions = Object.assign({
- scan: false,
- png: waveformPath,
- }, omit(this.config, 'wait'));
- waveform(track.filename, waveformOptions, (err) => {
- if (err) {
- reject(err);
- } else {
- resolve(waveformPath);
- }
- });
- }
- });
- }
- getCached(track) {
- const waveformPath = this.getWaveformPath(track);
- return fs.existsSync(waveformPath) ? waveformPath : false;
- }
- getWaveformPath(track) {
- return path.join(
- this.root,
- [
- track.id,
- this.config['png-width'],
- this.config['png-height'],
- this.config['png-color-bg'],
- this.config['png-color-center'],
- this.config['png-color-outer'],
- ].join('_') + OUTPUT_EXT,
- );
- }
- log(message, response) {
- if (!this.enableLog) {
- return;
- }
- response ? console.info(message, response) : console.info(message); // eslint-disable-line
- }
-}
diff --git a/src/styles/_playback_bar.styl b/src/styles/_playback_bar.styl
index bc56f04..5bda2b7 100644
--- a/src/styles/_playback_bar.styl
+++ b/src/styles/_playback_bar.styl
@@ -68,6 +68,7 @@
bottom 0
width 100%
height size($playbackBarProgressIndicatorHeight)
+ z-index 10
.waveform
width 100%
height 100%
@@ -77,6 +78,7 @@
opacity 0
transition opacity .4s ease-in-out
z-index -1
+ overflow hidden
&.loaded
opacity 1
.waveform-progress
diff --git a/src/ui/index.html b/src/ui/index.html
index 8763931..a6c7de4 100644
--- a/src/ui/index.html
+++ b/src/ui/index.html
@@ -14,5 +14,6 @@
+