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 @@
+