NOTE: this project is not really in a usable state right now. I'm working on a giant refactor which will hopefully be finished around early summer 2016. This refactor is taking place in the playlists
branch, but it is unfinished as of yet. Pull requests are welcome there, but please keep this refactor in mind for now!
Simple, modular music player written in node.js
Disclaimer: for personal use only - make sure you configure nodeplayer appropriately so that others can't access your music. I take no responsibility for example if your streaming services find you are violating their ToS. You're running this software entirely at your own risk!
Screenshot of the weblistener
![Screenshot of the weblistener] (https://raw.githubusercontent.com/FruitieX/nodeplayer-plugin-weblistener/master/screenshot.png)
Make sure you have Node.js installed, then run:
npm install -g nodeplayer
(here's how you can do this without sudo, highly recommended)nodeplayer
nodeplayer will now ask you to edit its configuration file. For a basic setup the defaults should be good. You may want to add a few more backends and/or plugins later, see below for some examples!
When you're done configuring, run nodeplayer
again. nodeplayer now
automatically installs missing plugins and backends. Note that if you installed
nodeplayer as root (you probably shouldn't), this step also requires root
since modules are installed to the same path as nodeplayer.
Note that backends and plugins you load may ask you to perform additional configuration steps. Read through the setup instructions for each of the plugins/backends you enable, and read through the output they print to console on first run. If you're using the default nodeplayer configuration, you should at least consider changing the configuration of the:
- YouTube backend, which uses a throwaway API key by default (might or might not work!)
- Passport plugin
for password protecting nodeplayer. By default passport uses username
changeMe
and passwordkeyboard cat
.
All modules can be updated by running nodeplayer -u
- nodeplayer The core music player component
- nodeplayer-client CLI client for controlling nodeplayer
- nodeplayer-player CLI audio playback client
- nodeplayer-plugin-express expressjs server
- nodeplayer-plugin-passport Password protection for nodeplayer
- nodeplayer-plugin-ipfilter IP filtering
- nodeplayer-plugin-partyplay Party playlist
- nodeplayer-plugin-rest REST API
- nodeplayer-plugin-socketio socket.io API
- nodeplayer-plugin-storequeue Save the queue
- nodeplayer-plugin-verifymac Verify queue add operations
- nodeplayer-plugin-weblistener Web-based audio player
- nodeplayer-backend-gmusic
- nodeplayer-backend-youtube
- nodeplayer-backend-spotify
- nodeplayer-backend-file
This repository contains the core nodeplayer module. As a standalone component it is rather useless, as it is meant to be extended by other modules. The core module manages a playback queue and initializes any external modules that you have configured it to load. External modules are given various ways to manipulate the queue, and without them you can't really interact with nodeplayer in any way!
External modules are categorized as follows:
- Backend modules: Sources of music
- Plugin modules: Extend the functionality of the core in various ways
By keeping nodeplayer modular it is possible to use it in a wide variety of scenarios, ranging from being a basic personal music player to a party playlist manager where partygoers can vote on songs. Or perhaps configure it as a streaming music player to your mobile devices and when you come home, you can simply switch music sources over to your PC since the music plays back in sync. More cool functionality can easily be implemented by writing new modules!
NOTE: The API is NOT stable yet! Things may break at any time without warning, I'm trying to stabilize things for 0.2.0. But some issues still exist that will definitely change the plugin and backend API a bit (likewise for the client API).
Code style adheres mostly to the Google JavaScript Style Guide, with the following exceptions:
- One indent equals 4 spaces, not 2
- Maximum line length is 100, not 80
- UNIX endlines (LF) are enforced
Apart from unit tests, code is ran through jshint
and jscs
with above options.
Before submitting a pull request, make sure that your code passes the test suite.
This can be checked with:
npm test
This repository follows the principles of git-flow
Modules are best developed by using npm link
. This allows nodeplayer to load the module directly from your module repository. This is only possible when running nodeplayer directly from the nodeplayer repository,
so getting a clone is highly recommended. npm link
can be used like so:
- First make sure your module has a
package.json
file - In your module repository, run
npm link
(here's how you can do this without sudo, highly recommended) - In the nodeplayer repository, link the module with
npm link nodeplayer-plugin-myplugin
- Now you can develop your module inside its own repository, any changes will take effect immediately when you run nodeplayer.
The core provides several functions for managing the queue to plugins, and through the use of hooks the core will call a plugin's hook functions (if defined) at well defined times.
TODO: template plugin
A plugin module must export at least an init function:
exports.init = function(player, logger, callback) {...};
The init functions:
- Are called once for each configured plugin when nodeplayer is started.
- Are called in sequence (unlike backends), and can thus depend on another plugin being loaded, possibly expanding the functionalities of that plugin.
- Are passed the following arguments:
- player: reference to the player object in nodeplayer core, store this if you need it later.
- logger: winston logger with per-plugin tag, use this for logging! (log levels are: logger.silly, logger.debug, logger.info, logger.warn, logger.error)
- callback: callback must be called with no arguments when you are done initializing the plugin. If there was an error initializing, call it with a string stating the reason for the error.
And there you have it, the simplest possible plugin. For more details, take a look at example plugins linked at the top! Now let's make it actually do something by taking a look at hook functions!
Plugin hook functions are called by the core (usually) before or after completing
some specific task. For instance onSongEnd
whenever a song ends, with the song as the first
and only argument. Anything with a reference to the player object can call hook functions like so:
player.callHooks('hookName', [arg1, arg2, ...]);
This will call the hook function hookName
in every plugin that has defined a
function with that name, in the order the plugins were loaded, with arg1, arg2, ...
as arguments. Simply define a hook function, eg. hookName
in the plugin as such:
exports.hookName = function(arg1, arg2, ...) {...};
If any hook returns a truthy value it is an error that will also be returned by
callHooks()
, and callHooks()
will stop iterating through other hooks with the same name.
List of hook functions with explanations (FIXME: might be out of date, grep the code for callHooks
to be sure)
onSongChange(np)
- song has changed tonp
onSongEnd(np)
- songnp
endedonSongPause(np)
- songnp
was pausedonSongPrepareError(song, err)
- preparingsong
failed witherr
onSongPrepared(song)
- preparingsong
succeededonPrepareProgress(song, s, done)
- data (s
bytes) related tosong
written to disk. Ifdone
true then we're done preparing the song.onEndOfQueue()
- queue endedonQueueModify(queue)
- queue was potentially modifiedpreAddSearchResult(song)
- about to add search resultsong
, returning a truthy value rejects search resultpreSongsRemoved(pos, cnt)
- about to removecnt
amount of songs starting atpos
. TODO: these should probably be possible to rejectpostSongsRemoved(pos, cnt)
- removedcnt
amount of songs starting atpos
preSongsQueued(songs, pos)
- about to queuesongs
topos
postSongsQueued(songs, pos)
- queuedsongs
topos
preSongQueued(song)
- about to queuesong
topos
postSongQueued(song)
- queuedsong
topos
sortQueue()
- queue sort hookonPluginInitialized(plugin)
-plugin
was initializedonPluginInitError(plugin, err)
-err
while initializingplugin
onPluginsInitialized()
- all plugins were initializedonBackendInitialized(backend)
-backend
was initializedonBackendInitError(backend, err)
-err
while initializingbackend
onBackendsInitialized()
- all backends were initialized
Backend modules are sources of music and need to export the following functions:
exports.init = function(player, logger, callback) {...};
- Very similar to the plugin init function
- Perform necessary initialization here
- Run callback with descriptive string argument on error, and no argument on success.
exports.search = function(query, callback, errCallback) {...};
- Used for searching songs in your backend.
query
contains aterms
property which represents the search terms - Callback should be called with results on success
- errCallback should be called with descriptive error string on error
- Results are a JavaScript object like so:
{
songs: {
dummySongID1: { // dummySongID1 should equal to the value of songID inside the song object
...
},
dummySongID1: {
...
},
}
}
- You can also choose to include some custom metadata as keys in the object, these will be passed along with the results. (eg. pagination)
- Song objects look like this:
{
artist: 'dummyArtist',
title: 'dummyTitle',
album: 'dummyAlbum',
albumArt: { // URLs to album art, must contain 'lq' (low quality) and 'hq' (high quality) links
lq: 'http://dummy.com/albumArt.png',
hq: 'http://dummy.com/albumArt_HighQuality.png'
},
duration: 123456, // in milliseconds
songID: 'dummySongID1', // a string uniquely identifying the song in your backend
score: i, // how relevant is this result, ideally from 0 (least relevant) to 100 (most relevant)
backendName: 'dummy', // name of this backend
format: 'opus' // file format/extension of encoded song
};
And finally, get ready for the insane one doing all the heavy lifting:
exports.prepareSong = function(song, progCallback, errCallback) {...};
- Called by the core when it wants backend to prepare audio data to disk.
- Audio data should be encoded and stored in for example:
/home/user/.nodeplayer/song-cache/backendName/songID.opus
- Use the following functions/variables to build up the path:
- config.getConfigDir()
- path.sep
- When more audio data has been written to disk, call progCallback with arguments:
- song object (song)
- Number of bytes written to disk
- true/false: is the whole song now written to disk?
- Call errCallback with descriptive error string if something goes wrong, nodeplayer will then remove all instances of that song from the queue and skip to the next song.
prepareSong
should return a function which if called, will cancel the preparation process and clean up any song data written so far. Nodeplayer may call this function if for example the song is skipped.
exports.isPrepared = function(song) {...};
- Called by nodeplayer to check if preparation is needed or not
- Returns
true
if the song is prepared,false
otherwise - Is allowed to return
true
while the song is being prepared - Often just a
return fs.existsSync(filePath)
TODO: template backend
For more details, take a look at example backends linked at the top!