mu-player
Version:
Play music from vk.com and soundcloud.com via Music Player Daemon
325 lines (271 loc) • 9.44 kB
JavaScript
import * as vkActions from './../actions/vk-actions';
import * as scActions from './../actions/sc-actions';
import Playlist from './playlist';
import * as player from './../player/player-control';
import loadingSpinner from '../tui/loading-spinner';
import Promise from 'bluebird';
import {
getRemoteBitrate
}
from '../actions/music-actions';
import storage from '../storage/storage';
import * as lfmActions from './../actions/lastfm-actions';
let screen = null;
let layout = null;
let plistPane = null;
let playlist = null;
let trackInfo = null;
let songid = null;
let getBitrateAsync = Promise.promisify(getRemoteBitrate);
export let init = (_screen, _layout) => {
screen = _screen;
layout = _layout;
plistPane = _layout.playlist;
trackInfo = _layout.trackInfo;
playlist = new Playlist(plistPane, layout.plistCount);
plistPane.on('select', () => playCurrent());
};
let errorHandler = (err) => {
Logger.error(err);
if (typeof err === 'object') {
if (err.code == 14) {
Logger.screen.error('VKontakte API limits reached');
// } else if (err.code === 'ETIMEDOUT') {
// Logger.screen.err('ETIMEDOUT');
} else if (err.cause) {
Logger.screen.error('ERROR:', err.cause);
} else {
if (err.error_msg) Logger.screen.error('ERR_MSG', err.error_msg);
if (err.code) Logger.screen.error('ERR_CODE', err.code);
Logger.screen.error('ERROR:', err);
}
}
};
let playCurrent = () => {
let urlFinded = false;
if (playlist.list.items.length > 0) {
while (!urlFinded) {
let url = playlist.getCurrent().url;
let id = playlist.getCurrent().mpdId;
if (url) {
urlFinded = true;
(typeof url === 'function' ? url() : Promise.resolve(url)).then((url) => {
player.play(url, id);
trackInfo.status = 'play';
}).catch(errorHandler);
} else {
playlist.moveNext();
}
}
}
};
export let favToggle = () => {
var track = playlist.getSelected();
lfmActions.favToggle({
title: track.title,
artist: track.artist
});
};
export let stop = () => {
screen.title = ':mu';
playlist.stop();
trackInfo.stop();
};
let appendTracks = (tracks) => {
plistPane.focus();
playlist.appendPlaylist(tracks);
return tracks;
};
let sortAndResumePlay = (sortQuery) => {
let status = trackInfo.status;
let cur = playlist.getCurrent();
playlist.sort(sortQuery);
let resumePlay = (system) => {
Logger.info(system);
if (system === 'playlist') {
let id = null;
for (var i = 0; i < playlist.data.length; i++) {
if (playlist.data[i].url === cur.url) {
id = playlist.data[i].mpdId;
break;
}
}
if (id !== null) player.play(cur.url, id);
playlist.mpd.removeListener('changed', resumePlay);
}
};
if (status === 'play') {
playlist.mpd.on('changed', resumePlay);
}
};
let loadBitrates = (tracks, spinner) => {
let msg = 'Loading bitrates...';
Logger.screen.log(msg);
spinner.setContent(msg);
let bitrates = [];
tracks.forEach((track) => {
bitrates.push(getBitrateAsync(track.url, track.duration).then((bitrate) => {
track.bitrate = bitrate;
}).catch((err) => {
if (err.code === 'ETIMEDOUT') errorHandler(new Error('BITRATE ETIMEDOUT: ' + track.url));
else errorHandler(err);
}));
});
return Promise.all(bitrates).then(() => {
let msg = 'Bitrates are loaded!';
Logger.screen.log(msg);
spinner.setContent(msg);
return tracks;
});
};
export let setPlaylist = (tracks) => {
let spinner = loadingSpinner(screen, 'Loading tracks...', false);
playlist.clearOnAppend = true;
plistPane.focus();
appendTracks(tracks);
return loadBitrates(tracks, spinner).then(() => spinner.stop());
};
export let search = (payload) => {
let sc;
let vk;
let spinner = loadingSpinner(screen, 'Searching for tracks...', false, payload.query);
let tryTimeout = storage.data.search.timeout;
let tryAttempts = storage.data.search.retries;
stop();
if (payload.type === 'search' || payload.type === 'tagsearch') {
playlist.clearOnAppend = true;
sc = scActions.getSearch(payload.query, {tryTimeout: tryTimeout, tryAttempts: tryAttempts}).then(appendTracks);
vk = vkActions.getSearch(payload.query, {tryTimeout: tryTimeout, tryAttempts: tryAttempts}).then((tracks) => {
appendTracks(tracks);
return loadBitrates(tracks, spinner);
});
} else if (payload.type === 'searchWithArtist') {
playlist.clearOnAppend = true;
sc = scActions.getSearchWithArtist(payload.track, payload.artist, {tryTimeout: tryTimeout, tryAttempts: tryAttempts}).then(appendTracks);
vk = vkActions.getSearchWithArtist(payload.track, payload.artist, {tryTimeout: tryTimeout, tryAttempts: tryAttempts}).then((tracks) => {
appendTracks(tracks);
return loadBitrates(tracks, spinner);
});
}
// smart sorting
Promise.all([vk, sc]).then(() => {
let count = 0;
if (sc && sc.isFulfilled() && Array.isArray(sc.value()))
count += sc.value().length;
if (vk && vk.isFulfilled() && Array.isArray(vk.value()))
count += vk.value().length;
Logger.screen.log(`Found: ${count} result(s)`);
if (count > 1) sortAndResumePlay(payload);
spinner.stop();
}).catch((err) => {
errorHandler(err);
spinner.stop();
});
};
let getBatchSearch = (tracklist, spinner) => {
let apiDelay = storage.data.batchSearch.apiDelay;
let maxApiDelay = storage.data.batchSearch.maxApiDelay;
let limit = storage.data.batchSearch.bitrateSearchLimit;
let tryAttempts = storage.data.batchSearch.retries;
let tryTimeout = storage.data.batchSearch.timeout;
let isUserStop = false;
spinner.once('destroy', () => isUserStop = true);
let localError = (err) => {
if (err && err.message && err.message.indexOf(':NotFound') === -1) {
errorHandler(err);
apiDelay = apiDelay * 2;
if (apiDelay > maxApiDelay) apiDelay = maxApiDelay;
Logger.screen.info('search', 'increasing delay - ', apiDelay + 'ms');
} else {
Logger.screen.info(err.message, 'nothing found');
}
};
return Promise.reduce(tracklist, (total, current, index) => {
if (isUserStop) throw new Error('Stopped by User');
let delay = Promise.delay(apiDelay); // new unresolved delay promise
spinner.setLabel(`${index + 1} / ${tracklist.length}: ${current.track}`);
spinner.setContent('Searching for "' + current.track + '"...');
return delay.then(() => {
let sc = scActions.getSearchWithArtist(current.track, current.artist,
{ limit: 10, tryTimeout: tryTimeout, tryAttempts: tryAttempts })
.catch(localError);
let vk = vkActions.getSearchWithArtist(current.track, current.artist,
{ limit: limit, tryTimeout: tryTimeout, tryAttempts: tryAttempts })
.catch(localError);
return Promise.all([sc, vk]).then((data) => {
let vkTracks = [];
let scTracks = [];
if (sc && sc.isFulfilled() && Array.isArray(sc.value()) && sc.value().length > 0)
scTracks = sc.value();
if (vk && vk.isFulfilled() && Array.isArray(vk.value()) && vk.value().length > 0)
vkTracks = vk.value();
return loadBitrates(vkTracks, spinner).then(() => {
let sorted = playlist.sorter([].concat(vkTracks, scTracks), {
track: current.track,
artist: current.artist,
type: 'batch'
});
if (typeof sorted[0] === 'object') {
sorted[0].index = (index + 1);
playlist.appendPlaylist([sorted[0]]);
}
});
});
});
}, 0); // initial value for the reduce!
};
export let batchSearch = (payload) => {
let spinner = loadingSpinner(screen, 'Loading top tracks...', false);
playlist.clearOnAppend = true;
stop();
getBatchSearch(payload.tracklist, spinner).then(() => {
Logger.screen.log('Batch search complete!');
spinner.stop();
}).catch((err) => {
errorHandler(err);
spinner.stop();
});
};
export let updatePlaying = (status) => {
Logger.info(status);
if (status.error) Logger.screen.error('MPD:', status.error);
if (status.state === 'play') {
if (playlist.setCurrentById(status.songid) === null) {
Logger.screen.error('Playlist:', 'Can\'t find track with id', status.songid);
stop();
return;
}
if (status.songid === songid) {
// resume from pause or from stop
trackInfo.updateStatus('play');
return;
}
// new song
songid = status.songid;
let cur = playlist.getCurrent();
Logger.info('Playing:', cur.url);
player.metadata(cur.url, (err, info) => {
if (err) return errorHandler(err);
for (var k in info) {
Logger.screen.info(`info`, `${k}: ${info[k]}`);
}
});
Logger.screen.status('Play:', cur.artist, '-', cur.title, '[' + status.bitrate + ' kbps]');
screen.title = cur.artist + ' - ' + cur.title;
trackInfo.init({
duration: cur.duration,
title: cur.title,
artist: cur.artist,
bitrate: cur.bitrate,
status: 'play'
});
} else if (status.state === 'stop') {
stop();
} else if (status.state === 'pause') {
trackInfo.updateStatus('pause');
}
};
export let updatePbar = (elapsed, seek) => {
if (trackInfo === null || trackInfo.hidden) return;
trackInfo.setProgress(elapsed, seek);
};