Compare commits

..

7 commits

Author SHA1 Message Date
Andrew Kelley
091a24ee9d derping around. ugh 2014-04-01 23:28:07 -07:00
Andrew Kelley
1631fdd10f refactor encoding to be always ahead of playhead 2014-04-01 10:40:21 -07:00
Andrew Kelley
4e1c926cd5 fix time keeping when no streamers or encoders attached 2014-03-31 22:38:20 -07:00
Andrew Kelley
a9a4796922 debug stuff 2014-03-31 18:34:13 -07:00
Andrew Kelley
54fd53000b more reliable headless playback 2014-03-31 18:34:13 -07:00
Andrew Kelley
259526c82b add debug thing 2014-03-31 18:34:13 -07:00
Andrew Kelley
c5f31269d2 ability to attach and detach encoder and player.
* admin option to disable and enable hardware playback. Closes #129
 * only attach encoder when streamers are connected. Closes #143
2014-03-31 18:34:13 -07:00
19 changed files with 670 additions and 1646 deletions

View file

@ -2,7 +2,7 @@
Music player server with a web-based user interface inspired by Amarok 1.4.
Run it on a server (such as a 512MB
Run it on a server (such as a
[Raspberry Pi](http://www.raspberrypi.org/)) connected to some speakers
in your home or office. Guests can control the music player by connecting
with a laptop, tablet, or smart phone. Further, you can stream your music
@ -11,7 +11,7 @@ library remotely.
Groove Basin works with your personal music library; not an external music
service. Groove Basin will never support DRM content.
Try out the [live demo](http://demo.groovebasin.com/).
Live discussion in #libgroove on Freenode.
## Features
@ -46,10 +46,7 @@ Try out the [live demo](http://demo.groovebasin.com/).
## Install
1. Install [Node.js](http://nodejs.org) v0.10.x. Note that on Debian and
Ubuntu, sadly the official node package is not sufficient. You will either
have to use [Chris Lea's PPA](https://launchpad.net/~chris-lea/+archive/node.js/)
or compile from source.
1. Install [Node.js](http://nodejs.org) v0.10.x.
2. Install [libgroove](https://github.com/andrewrk/libgroove).
3. Clone the source.
4. `npm run build`
@ -76,11 +73,6 @@ $ npm run dev
This will install dependencies, build generated files, and then start the
sever. It is up to you to restart it when you modify assets or server files.
### Community
Pull requests, feature requests, and bug reports are welcome! Live discussion
in #libgroove on Freenode.
### Roadmap
1. Tag Editing

3
docs/.gitignore vendored
View file

@ -1,3 +0,0 @@
_build
_static
_templates

View file

@ -1,287 +0,0 @@
Changelog
=========
1.0.1 (March 18, 2014)
----------------------
.. What does this mean?
* Default import path includes artist directory.
* Groove Basin now recognizes the `TCMP`_ ID3 tag as a compilation album flag.
.. _TCMP: http://id3.org/iTunes%20Compilation%20Flag
Fixes:
* Fixed Last.fm authentication.
* Fixed a race condition when removing tracks from playlist.
1.0.0 (March 15, 2014)
----------------------
In the 1.0.0 release, Groove Basin has removed its dependency on MPD, using
`libgroove`_ for audio playback and streaming support. Groove Basin is also not
written in `coco`_ anymore. Hopefully this will encourage more contributors to
join the project!
Major features include `ReplayGain`_ style automatic loudness detection using the
`EBU R128`_ recommendation. Scanning takes place on the fly, taking advantage of
multi-core systems. Groove Basin automatically switches between album and track
mode depending on the next item in the play queue.
Chat and playlist functionality have been removed as they are not quite ready
yet. These features will be reimplemented better in a future release.
.. _libgroove: https://github.com/andrewrk/libgroove
.. _coco: https://github.com/satyr/coco
.. _ReplayGain: https://en.wikipedia.org/wiki/ReplayGain
.. _EBU R128: https://tech.ebu.ch/loudness
Other features:
* Groove Basin now functions as an MPD server. MPD clients can connect to port
6600 by default.
* The config file is simpler and should survive new version releases.
* Client and server communications now use a simpler and more efficient protocol.
* Rebuilding the album index is faster.
* The HTTP audio stream buffers much more quickly and flushes the buffer on seek.
* Streaming shows when it is buffering.
* The web UI now specifies a `UTF-8`_ character set.
* Groove Basin's music library now updates automatically by watching the music
folder for changes.
* HTTP streaming now uses native HTML5 audio, instead of `SoundManager 2`_
* `jQuery`_ and `jQuery UI`_ have been updated to the latest stable version, fixing
some UI glitches.
* Static assets are gzipped and held permanently in memory, making the web
interface load faster.
* Now routing Dynamic mode through the permissions framework.
* Better default password generation.
.. _UTF-8: https://en.wikipedia.org/wiki/UTF-8
.. _SoundManager 2: http://www.schillmania.com/projects/soundmanager2/
.. _jQuery: https://jquery.com/
.. _jQuery UI: https://jqueryui.com/
Fixes:
* Fixed a regression for handling unknown artists or albums.
* Fixed play queue to display the artist name of tracks.
* Plugged an upload security hole.
* Pressing the previous track button on the first track in the play queue when
"repeat all" is turned on now plays the last track in the play queue.
* The volume widget no longer goes higher than 100%.
* Changing the volume now shows up on other clients.
* The volume keyboard shortcuts now work in Firefox.
* Ensured that no-cache headers are set for the stream.
* Fixed an issue in the Web UI where the current track was sometimes not
displayed.
Thanks to Josh Wolfe, who worked to fix some issues around deleting library
items, ensuring that deleting library items removes them from the play queue,
and that the play queue correctly reacts to deleted library entries.
In addition, he worked to:
* Convert Groove Basin to not use MPD.
* fix multiselect shiftIds.
* fix shift click going up in the queue.
.. What does this mean?
0.2.0 (October 16, 2012)
-------------------------
* Andrew Kelley:
* ability to import songs by pasting a URL
* improve build and development setup
* update style to not resize on selection. closes #23
* better connection error messages. closes #21
* separate [mpd.js](https://github.com/andrewrk/mpd.js) into an open source module. closes #25
* fix dynamicmode; use higher level sticker api. closes #22
* search uses ascii folding so that 'jonsi' matches 'Jónsi'. closes #29
* server restarts if it crashes
* server runs as daemon
* server logs to rotating log files
* remove setuid feature. use authbind if you want to run as port 80
* ability to download albums and artists as zip. see #9
* ability to download arbitrary selection as zip. closes #9
* fix track 08 and 09 displaying as 0. closes #65
* fix right click for IE
* better error reporting when state json file is corrupted
* log chats
* fix edge case with unicode characters. closes #67
* fix next and previous while stopped behavior. closes #19
* handle uploading errors. fixes #59
* put link to stream URL in settings. closes #69
* loads faster and renders faster
* send a 404 when downloading can't find artist or album. closes #70
* read-only stored playlist support
* fix playlist display when empty
* add uploaded songs to "Incoming" playlist. closes #80
* fix resize weirdness when you click library tab. closes #75
* don't bold menu option text
* add color to the first part of the track slider. closes #15
* Josh Wolfe:
* fix dynamic mode glitch
* fix dynamic mode with no library or no tags file
* uploading with mpd <0.17 falls back to upload name
0.1.2 (July 12, 2012)
---------------------
* Andrew Kelley:
* lock in the major versions of dependencies
* more warnings about mpd conf settings
* remove "alert" text on no connection
* better build system
* move dynamic mode configuration to server
* server handles permissions in mpd.conf correctly
* clients can set a password
* ability to delete from library
* use soundmanager2 instead of jplayer for streaming
* buffering status on stream button
* stream button has a paused state
* use .npmignore to only deploy generated files
* update to work with node 0.8.2
* Josh Wolfe:
* pointing at mpd's own repository in readme. #12
* fixing null pointer error for when streaming is disabled
* fixing blank search on library update
* fixing username on reconnect
* backend support for configurable dynamic history and future sizes
* ui for configuring dynamic mode history and future sizes
* coloring yourself different in chat
* scrubbing stale user ids in my_user_ids
* better chat name setting ui
* scrolling chat window properly
* moar chat history
* formatting the state file
* fixing chat window resize on join/left
* validation on dynamic mode settings
* clearer wording in Get Started section and louder mpd version dependency
documentation
0.0.6 (April 27, 2012)
----------------------
* Josh Wolfe:
* fixing not queuing before random when pressing enter in the search box
* fixing streaming hotkey not updating button ui
* stopping and starting streaming in sync with mpd.status.state.
* fixing weird bug with Stream button checked state
* warning when bind_to_address is not also configured for localhost
* fixing derpy log reference
* fixing negative trackNumber scrobbling
* directory urls download .zip files. #9
* document dependency on mpd version 0.17
* Andrew Kelley:
* fix regression: not queuing before random songs client side
* uploaded songs are queued in the correct place
* support restarting mpd without restarting daemon
* ability to reconnect without refreshing
* log.info instead of console.info for track uploaded msg
* avoid the use of 'static' keyword
* David Banham:
* Make jPlayer aware of which stream format is set
* Removed extra constructor. Changed tabs to 2spaces
0.0.5 (March 11, 2012)
----------------------
* Note: Requires you to pull from latest mpd git code and recompile.
* Andrew Kelley:
* disable volume slider when mpd reports volume as -1. fixes #8
* on last.fm callback, do minimal work then refresh. fixes #7
* warnings output the actual mpd.conf path instead of "mpd conf". see #5
* resize things *after* rendering things. fixes #6
* put uploaded files in an intelligent place, and fix #2
* ability to retain server state file even when structure changes
* downgrade user permissions ASAP
* label playlist items upon status update
* use blank user_id to avoid error message
* use jplayer for streaming
* Josh Wolfe:
* do not show ugly "user_n" text after usernames in chat.
0.0.4 (March 6, 2012)
---------------------
* Andrew Kelley:
* update keyboard shortcuts dialog
* fix enter not queuing library songs in firefox
* ability to authenticate with last.fm, last.fm scrobbling
* last.fm scrobbling works
* fix issues with empty playlist. fixes #4
* fix bug with dynamic mode when playlist is clear
* Josh Wolfe:
* easter eggs
* daemon uses a state file
0.0.3 (March 4, 2012)
---------------------
* Andrew Kelley:
* ability to select artists, albums, tracks in library
* prevents sticker race conditions from crashing the server (#3)
* escape clears the selection cursor too
* ability to shift+click select in library
* right-click queuing in library works
* do not show download menu option since it is not supported yet
* show selection on expanded elements
* download button works for single tracks in right click library menu
* library up/down to change selection
* nextLibPos/prevLibPos respects whether tree items are expanded or collapse
* library window scrolls down when you press up/down to move selection
* double click artists and albums in library to queue
* left/right expands/collapses library tree when lib has selection
* handle enter in playlist and library
* ability to drag artists, albums, tracks to playlist
* Josh Wolfe:
* implement chat room
* users can set their name in the chat room
* users can change their name multiple times
* storing username persistently. disambiguating conflicting usernames.
* loading recent chat history on connect
* normalizing usernames and sanitizing username display
* canot send blank chats
* supporting /nick renames in chat box
* hotkey to focus chat box
0.0.2 (March 1, 2012)
-------------------------
* Andrew Kelley:
* learn mpd host and port in mpd conf
* render unknown albums and unknown artists the same in the playlist (blank)
* auto-scroll playlist window and library window appropriately
* fix server crash when no top-level files exist
* fix some songs error message when uploading
* edit file uploader spinny gif to fit the theme
* move chat stuff to another tab
* Josh Wolfe:
* tracking who is online

View file

@ -1,242 +0,0 @@
# -*- coding: utf-8 -*-
#
# Groove Basin documentation build configuration file, created by
# sphinx-quickstart on Thu Apr 24 14:07:20 2014.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Groove Basin'
copyright = u'2014, Andrew Kelley'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.0.1'
# The full version, including alpha/beta/rc tags.
release = '1.0.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'GrooveBasindoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'GrooveBasin.tex', u'Groove Basin Documentation',
u'Andrew Kelley', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'groovebasin', u'Groove Basin Documentation',
[u'Andrew Kelley'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'GrooveBasin', u'Groove Basin Documentation',
u'Andrew Kelley', 'GrooveBasin', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'

View file

@ -1,69 +0,0 @@
Getting Started
===============
Welcome to Groove Basin! This guide will help you begin using it to listen to music.
Installing
----------
Installing on Ubuntu
^^^^^^^^^^^^^^^^^^^^
Groove Basin is still in development and has not yet been packaged by Ubuntu, so you will have to build it from source.
Install `Node.js`_ v0.10.x or greater. We recommend using `Chris Lea's PPA`_ for Node. If you want to use the PPA, run:
``add-apt-repository ppa:chris-lea/node.js``
``apt-get update && apt-get install nodejs``
.. _Node.js: http://nodejs.org
.. _Chris Lea's PPA: https://launchpad.net/~chris-lea/+archive/node.js/
Install `libgroove`_ from the `libgroove PPA`_:
.. _libgroove: https://github.com/andrewrk/libgroove
.. _libgroove PPA: https://launchpad.net/~andrewrk/+archive/libgroove
``apt-add-repository ppa:andrewrk/libgroove``
``apt-get update && apt-get install libgroove-dev libgrooveplayer-dev libgrooveloudness-dev libgroovefingerprinter-dev``
Install `Git`_ if it is not already installed:
``apt-get install git``
.. _Git: http://git-scm.com/
Clone the Groove Basin git repository somewhere:
``git clone https://github.com/andrewrk/groovebasin.git``
Build Groove Basin:
``cd groovebasin``
``npm run build``
Running Groove Basin
--------------------
To start Groove Basin:
``npm start``
Importing Your Library
----------------------
Groove Basin currently supports a single music library folder. Open the ``config.js`` file that Groove Basin creates on first run and edit the ``musicDirectory`` key to point to your music directory.
Playing Your Music
------------------
Now that you have Groove Basin set up and indexing your music, you can start playing your music!
Open your favorite web browser and point it to:
http://localhost:16242
You should now see Groove Basin and can add songs to the play queue for playback. Double click on a song to play it.

View file

@ -1,29 +0,0 @@
.. Groove Basin documentation master file, created by
sphinx-quickstart on Thu Apr 24 14:07:20 2014.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Groove Basin: the ultimate music player
========================================
Welcome to the documentation for Groove Basin, a music player with a web-based user interface inspired by Amarok 1.4..
If you're new to Groove Basin, begin with the `:docs:guides/main`_ guide. That guide walks you through installing Groove Basin, setting it up how you like it, and starting to build your music library.
If you still need help, your can drop by the #libgroove IRC channel on Freenode or file a bug in the issue tracker. Please let us know where you think this documentation can be improved.
.. _:docs:guides/main: guides/main.rst
Contents:
.. toctree::
:maxdepth: 2
Index
==================
* :ref:`genindex`
* :ref:`search`

View file

@ -43,8 +43,6 @@ var defaultConfig = {
lastFmApiSecret: "8713e8e893c5264608e584a232dd10a0",
mpdHost: '0.0.0.0',
mpdPort: 6600,
acoustidAppKey: 'bgFvC4vW',
instantBufferBytes: 220 * 1024,
};
defaultConfig.permissions[genPassword()] = {
@ -140,7 +138,7 @@ GrooveBasin.prototype.start = function() {
self.initializeDownload();
self.initializeUpload();
self.player = new Player(self.db, self.config.musicDirectory, self.config.instantBufferBytes);
self.player = new Player(self.db, self.config.musicDirectory);
self.player.initialize(function(err) {
if (err) {
console.error("unable to initialize player:", err.stack);

View file

@ -1320,7 +1320,7 @@ function writePlaylistInfo(self, start, end) {
}
function forEachMatchingTrack(self, filters, caseSensitive, fn) {
// TODO: support 'in' as tag type
// TODO: support 'any' and 'in' as tag types
var trackTable = self.player.libraryIndex.trackTable;
if (!caseSensitive) {
filters.forEach(function(filter) {
@ -1329,25 +1329,14 @@ function forEachMatchingTrack(self, filters, caseSensitive, fn) {
}
for (var key in trackTable) {
var track = trackTable[key];
var matches = false;
var matches = true;
for (var filterIndex = 0; filterIndex < filters.length; filterIndex += 1) {
var filter = filters[filterIndex];
var filterField = String(track[filter.field]);
if (!filterField) continue;
var filterField = track[filter.field];
if (!caseSensitive && filterField) filterField = filterField.toLowerCase();
/* assumes:
* caseSensitive implies "find"
* !caseSensitive implies "search"
*/
if (caseSensitive) {
if (filterField === filter.value) {
matches = true;
break;
}
} else if (filterField.indexOf(filter.value) > -1) {
matches = true;
break;
if (filterField !== filter.value) {
matches = false;
break;
}
}
if (matches) fn(track);
@ -1394,23 +1383,12 @@ function parseFindArgs(self, args, caseSensitive, onTrack, cb, onFinish) {
}
var filters = [];
for (var i = 0; i < args.length; i += 2) {
var tagsToSearch = [];
if (args[i].toLowerCase() === "any") {
// Special case the any key. Just search everything.
for (var tagType in tagTypes) {
tagsToSearch.push(tagTypes[tagType]);
}
} else {
var tagType = tagTypes[args[i].toLowerCase()];
if (!tagType) return cb(ERR_CODE_ARG, "\"" + args[i] + "\" is not known");
tagsToSearch.push(tagType);
}
for (var j = 0; j < tagsToSearch.length; j++) {
filters.push({
field: tagsToSearch[j].grooveTag,
value: args[i+1],
});
}
var tagType = tagTypes[args[i].toLowerCase()];
if (!tagType) return cb(ERR_CODE_ARG, "\"" + args[i] + "\" is not known");
filters.push({
field: tagType.grooveTag,
value: args[i+1],
});
forEachMatchingTrack(self, filters, caseSensitive, onTrack);
}
onFinish();

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,14 @@ var Player = require('./player');
module.exports = PlayerServer;
// these are the ones we send to the web, not the ones we store in the DB
var DB_FILE_PROPS = [
'key', 'name', 'artistName', 'albumArtistName',
'albumName', 'compilation', 'track', 'trackCount',
'disc', 'discCount', 'duration', 'year', 'genre',
'file'
];
PlayerServer.plugins = [];
PlayerServer.actions = {
@ -58,6 +66,13 @@ PlayerServer.actions = {
self.player.setDynamicModeFutureSize(size);
},
},
'hardwarePlayback': {
permission: 'admin',
args: 'boolean',
fn: function(self, client, isOn) {
self.player.setHardwarePlayback(isOn);
},
},
'importUrl': {
permission: 'control',
args: 'object',
@ -73,7 +88,10 @@ PlayerServer.actions = {
} else {
key = dbFile.key;
}
client.sendMessage('importUrl', {id: id, key: key});
// client might have disconnected by now
try {
client.sendMessage('importUrl', {id: id, key: key});
} catch (err) {}
});
},
},
@ -102,13 +120,6 @@ PlayerServer.actions = {
}
},
},
'updateTags': {
permission: 'admin',
args: 'object',
fn: function(self, client, obj) {
self.player.updateTags(obj);
},
},
'unsubscribe': {
permission: 'read',
args: 'string',
@ -201,6 +212,7 @@ PlayerServer.prototype.initialize = function() {
self.player.on('repeatUpdate', addSubscription('repeat', getRepeat));
self.player.on('volumeUpdate', addSubscription('volume', getVolume));
self.player.on('playlistUpdate', addSubscription('playlist', serializePlaylist));
self.player.on('hardwarePlayback', addSubscription('hardwarePlayback', getHardwarePlayback));
var onLibraryUpdate = addSubscription('library', serializeLibrary);
self.player.on('addDbTrack', onLibraryUpdate);
@ -253,6 +265,10 @@ PlayerServer.prototype.initialize = function() {
return new Date();
}
function getHardwarePlayback(client) {
return self.player.hardwarePlayback;
}
function getRepeat(client) {
return self.player.repeat;
}
@ -296,7 +312,7 @@ PlayerServer.prototype.initialize = function() {
var table = {};
for (var key in self.player.libraryIndex.trackTable) {
var track = self.player.libraryIndex.trackTable[key];
table[key] = Player.trackWithoutIndex('read', track);
table[key] = Player.trackWithoutIndex(DB_FILE_PROPS, track);
}
return table;
}

View file

@ -1,62 +0,0 @@
var ytdl = require('ytdl');
var url = require('url');
module.exports = YtDlPlugin;
// sorted from worst to best
var YTDL_AUDIO_ENCODINGS = [
'mp3',
'aac',
'wma',
'vorbis',
'wav',
'flac',
];
function YtDlPlugin(gb) {
gb.player.importUrlFilters.push(this);
}
YtDlPlugin.prototype.importUrl = function(urlString, cb) {
var parsedUrl = url.parse(urlString);
var isYouTube = (parsedUrl.pathname === '/watch' &&
(parsedUrl.hostname === 'youtube.com' ||
parsedUrl.hostname === 'www.youtube.com' ||
parsedUrl.hostname === 'm.youtube.com')) ||
parsedUrl.hostname === 'youtu.be' ||
parsedUrl.hostname === 'www.youtu.be';
if (!isYouTube) {
cb();
return;
}
var bestFormat = null;
ytdl.getInfo(urlString, gotYouTubeInfo);
function gotYouTubeInfo(err, info) {
if (err) return cb(err);
for (var i = 0; i < info.formats.length; i += 1) {
var format = info.formats[i];
if (bestFormat == null || format.audioBitrate > bestFormat.audioBitrate ||
(format.audioBitrate === bestFormat.audioBitrate &&
YTDL_AUDIO_ENCODINGS.indexOf(format.audioEncoding) >
YTDL_AUDIO_ENCODINGS.indexOf(bestFormat.audioEncoding)))
{
bestFormat = format;
}
}
if (YTDL_AUDIO_ENCODINGS.indexOf(bestFormat.audioEncoding) === -1) {
console.warn("YouTube Import: unrecognized audio format:", bestFormat.audioEncoding);
}
var req = ytdl(urlString, {filter: filter});
var filename = info.title + '.' + bestFormat.container;
cb(null, req, filename);
function filter(format) {
return format.audioBitrate === bestFormat.audioBitrate &&
format.audioEncoding === bestFormat.audioEncoding;
}
}
};

View file

@ -10,10 +10,9 @@ function ProtocolParser(options) {
this.player = options.player;
this.buffer = "";
this.alreadyClosed = false;
}
ProtocolParser.prototype._read = function(size) {};
ProtocolParser.prototype._read = function(size) {}
ProtocolParser.prototype._write = function(chunk, encoding, callback) {
var self = this;
@ -45,18 +44,15 @@ ProtocolParser.prototype._write = function(chunk, encoding, callback) {
}
self.emit('message', jsonObject.name, jsonObject.args);
}
};
}
ProtocolParser.prototype.sendMessage = function(name, args) {
if (this.alreadyClosed) return;
var jsonObject = {name: name, args: args};
this.push(JSON.stringify(jsonObject));
};
ProtocolParser.prototype.close = function() {
if (this.alreadyClosed) return;
this.push(null);
this.alreadyClosed = true;
};
function extend(o, src) {

View file

@ -11,15 +11,10 @@ function WebSocketApiClient(ws) {
}
WebSocketApiClient.prototype.sendMessage = function(name, args) {
try {
this.ws.send(JSON.stringify({
name: name,
args: args,
}));
} catch (err) {
// nothing to do
// client might have disconnected by now
}
this.ws.send(JSON.stringify({
name: name,
args: args,
}));
};
WebSocketApiClient.prototype.close = function() {

View file

@ -18,7 +18,7 @@
},
"dependencies": {
"lastfm": "~0.9.0",
"express": "^4.0.0",
"express": "^4.0.0-rc4",
"superagent": "^0.17.0",
"mkdirp": "~0.3.5",
"mv": "~2.0.0",
@ -26,25 +26,25 @@
"zfill": "0.0.1",
"requireindex": "^1.1.0",
"mess": "~0.1.1",
"groove": "~1.4.1",
"groove": "^1.3.2",
"osenv": "0.0.3",
"level": "^0.18.0",
"level": "~0.18.0",
"findit": "~1.1.1",
"archiver": "^0.6.1",
"uuid": "~1.4.1",
"music-library-index": "^1.1.1",
"music-library-index": "^1.1.0",
"keese": "~1.0.0",
"ws": "^0.4.31",
"ws": "~0.4.31",
"jsondiffpatch": "~0.1.4",
"connect-static": "^1.1.0",
"multiparty": "^3.2.4",
"ytdl": "^0.2.4",
"multiparty": "^3.2.3",
"ytdl": "^0.2.2",
"serve-static": "^1.0.3",
"body-parser": "^1.0.1"
},
"devDependencies": {
"stylus": "^0.42.3",
"browserify": "^3.41.0"
"browserify": "~3.32.0"
},
"scripts": {
"start": "node lib/server.js",

View file

@ -9,6 +9,7 @@ var Socket = require('./socket');
var uuid = require('uuid');
var dynamicModeOn = false;
var hardwarePlaybackOn = false;
var selection = {
ids: {
@ -47,13 +48,13 @@ var selection = {
this.rangeSelectAnchor = null;
this.rangeSelectAnchorType = null;
},
selectOnly: function(selName, key){
selectOnly: function(sel_name, key){
this.clear();
this.type = selName;
this.ids[selName][key] = true;
this.type = sel_name;
this.ids[sel_name][key] = true;
this.cursor = key;
this.rangeSelectAnchor = key;
this.rangeSelectAnchorType = selName;
this.rangeSelectAnchorType = sel_name;
},
isMulti: function(){
var result, k;
@ -198,13 +199,8 @@ var selection = {
if (isAlbumExpanded(pos.album)) {
pos.track = pos.album.trackList[0];
} else {
var nextAlbum = pos.artist.albumList[pos.album.index + 1];
if (nextAlbum) {
pos.album = nextAlbum;
} else {
pos.artist = player.searchResults.artistList[pos.artist.index + 1];
pos.album = null;
}
pos.artist = player.searchResults.artistList[pos.artist.index + 1];
pos.album = null;
}
} else if (pos.artist != null) {
if (isArtistExpanded(pos.artist)) {
@ -357,8 +353,8 @@ var ICON_EXPANDED = 'ui-icon-triangle-1-se';
var permissions = {};
var socket = null;
var player = null;
var userIsSeeking = false;
var userIsVolumeSliding = false;
var user_is_seeking = false;
var user_is_volume_sliding = false;
var started_drag = false;
var abortDrag = function(){};
var clickTab = null;
@ -399,11 +395,11 @@ var $tabs = $('#tabs');
var $upload_tab = $tabs.find('.upload-tab');
var $library = $('#library');
var $lib_filter = $('#lib-filter');
var $trackSlider = $('#track-slider');
var $track_slider = $('#track-slider');
var $nowplaying = $('#nowplaying');
var $nowplaying_elapsed = $nowplaying.find('.elapsed');
var $nowplaying_left = $nowplaying.find('.left');
var $volSlider = $('#vol-slider');
var $vol_slider = $('#vol-slider');
var $settings = $('#settings');
var $uploadByUrl = $('#upload-by-url');
var $main_err_msg = $('#main-err-msg');
@ -436,9 +432,9 @@ var $settingsLastFmOut = $('#settings-lastfm-out');
var settingsLastFmUserDom = document.getElementById('settings-lastfm-user');
var $toggleScrobble = $('#toggle-scrobble');
var $shortcuts = $('#shortcuts');
var $editTagsDialog = $('#edit-tags');
var $playlistMenu = $('#menu-playlist');
var $libraryMenu = $('#menu-library');
var $toggleHardwarePlayback = $('#toggle-hardware-playback');
function saveLocalState(){
localStorage.setItem('state', JSON.stringify(localState));
@ -718,7 +714,6 @@ function refreshSelection() {
}
}
}
function getValidIds(selection_type) {
switch (selection_type) {
case 'playlist': return player.playlist.itemTable;
@ -824,7 +819,7 @@ function getCurrentTrackPosition(){
}
function updateSliderPos() {
if (userIsSeeking) return;
if (user_is_seeking) return;
var duration, disabled, elapsed, sliderPos;
if (player.currentItem && player.isPlaying != null && player.currentItem.track) {
@ -836,19 +831,19 @@ function updateSliderPos() {
disabled = true;
elapsed = duration = sliderPos = 0;
}
$trackSlider.slider("option", "disabled", disabled).slider("option", "value", sliderPos);
$track_slider.slider("option", "disabled", disabled).slider("option", "value", sliderPos);
$nowplaying_elapsed.html(formatTime(elapsed));
$nowplaying_left.html(formatTime(duration));
}
function renderVolumeSlider() {
if (userIsVolumeSliding) return;
if (user_is_volume_sliding) return;
var enabled = player.volume != null;
if (enabled) {
$volSlider.slider('option', 'value', player.volume);
$vol_slider.slider('option', 'value', player.volume);
}
$volSlider.slider('option', 'disabled', !enabled);
$vol_slider.slider('option', 'disabled', !enabled);
}
function renderNowPlaying(){
@ -888,7 +883,7 @@ function renderNowPlaying(){
new_class = 'ui-icon-play';
}
$nowplaying.find(".toggle span").removeClass(old_class).addClass(new_class);
$trackSlider.slider("option", "disabled", player.isPlaying == null);
$track_slider.slider("option", "disabled", player.isPlaying == null);
updateSliderPos();
renderVolumeSlider();
}
@ -1053,34 +1048,29 @@ function nextRepeatState(){
player.setRepeatMode((player.repeat + 1) % repeatModeNames.length);
}
var keyboardHandlers = (function(){
var keyboard_handlers = (function(){
function upDownHandler(event){
var defaultIndex, dir, nextPos;
var default_index, dir, next_pos;
if (event.which === 38) {
// up
defaultIndex = player.currentItem ? player.currentItem.index - 1 : player.playlist.itemList.length - 1;
default_index = player.playlist.itemList.length - 1;
dir = -1;
} else {
// down
defaultIndex = player.currentItem ? player.currentItem.index + 1 : 0;
default_index = 0;
dir = 1;
}
if (defaultIndex >= player.playlist.itemList.length) {
defaultIndex = player.playlist.itemList.length - 1;
} else if (defaultIndex < 0) {
defaultIndex = 0;
}
if (event.altKey) {
if (selection.isPlaylist()) {
player.shiftIds(selection.ids.playlist, dir);
}
} else {
if (selection.isPlaylist()) {
nextPos = player.playlist.itemTable[selection.cursor].index + dir;
if (nextPos < 0 || nextPos >= player.playlist.itemList.length) {
next_pos = player.playlist.itemTable[selection.cursor].index + dir;
if (next_pos < 0 || next_pos >= player.playlist.itemList.length) {
return;
}
selection.cursor = player.playlist.itemList[nextPos].id;
selection.cursor = player.playlist.itemList[next_pos].id;
if (!event.ctrlKey && !event.shiftKey) {
// single select
selection.clear();
@ -1096,22 +1086,24 @@ var keyboardHandlers = (function(){
selection.rangeSelectAnchorType = selection.type;
}
} else if (selection.isLibrary()) {
nextPos = selection.getPos();
next_pos = selection.getPos();
if (dir > 0) {
selection.incrementPos(nextPos);
selection.incrementPos(next_pos);
} else {
prevLibPos(nextPos);
prevLibPos(next_pos);
}
if (nextPos.artist == null) return;
if (nextPos.track != null) {
if (next_pos.artist == null) {
return;
}
if (next_pos.track != null) {
selection.type = 'track';
selection.cursor = nextPos.track.key;
} else if (nextPos.album != null) {
selection.cursor = next_pos.track.key;
} else if (next_pos.album != null) {
selection.type = 'album';
selection.cursor = nextPos.album.key;
selection.cursor = next_pos.album.key;
} else {
selection.type = 'artist';
selection.cursor = nextPos.artist.key;
selection.cursor = next_pos.artist.key;
}
if (!event.ctrlKey && !event.shiftKey) {
// single select
@ -1126,12 +1118,16 @@ var keyboardHandlers = (function(){
}
} else {
if (player.playlist.itemList.length === 0) return;
selection.selectOnly('playlist', player.playlist.itemList[defaultIndex].id);
selection.selectOnly('playlist', player.playlist.itemList[default_index].id);
}
refreshSelection();
}
if (selection.isPlaylist()) scrollPlaylistToSelection();
if (selection.isLibrary()) scrollLibraryToSelection();
if (selection.isPlaylist()) {
scrollPlaylistToSelection();
}
if (selection.isLibrary()) {
scrollLibraryToSelection();
}
}
function leftRightHandler(event){
var dir = event.which === 37 ? -1 : 1;
@ -1366,8 +1362,10 @@ var keyboardHandlers = (function(){
title: "Keyboard Shortcuts",
minWidth: 600,
height: $document.height() - 40,
close: function(){
$('#shortcuts').remove();
}
});
$shortcuts.focus();
} else {
clickTab('library');
$lib_filter.focus().select();
@ -1398,28 +1396,29 @@ function isArtistExpanded(artist){
return $li.find("> ul").is(":visible");
}
function isAlbumExpanded(album){
var $li = $("#lib-album-" + toHtmlId(album.key)).closest("li");
var $li;
$li = $("#lib-album-" + toHtmlId(album.key)).closest("li");
return $li.find("> ul").is(":visible");
}
function isStoredPlaylistExpanded(stored_playlist){
var $li = $("#stored-pl-pl-" + toHtmlId(stored_playlist.name)).closest("li");
var $li;
$li = $("#stored-pl-pl-" + toHtmlId(stored_playlist.name)).closest("li");
return $li.find("> ul").is(":visible");
}
function prevLibPos(libPos){
if (libPos.track != null) {
libPos.track = libPos.track.album.trackList[libPos.track.index - 1];
} else if (libPos.album != null) {
libPos.album = libPos.artist.albumList[libPos.album.index - 1];
if (libPos.album != null && isAlbumExpanded(libPos.album)) {
libPos.track = libPos.album.trackList[libPos.album.trackList.length - 1];
function prevLibPos(lib_pos){
if (lib_pos.track != null) {
lib_pos.track = lib_pos.track.album.trackList[lib_pos.track.index - 1];
} else if (lib_pos.album != null) {
lib_pos.album = lib_pos.artist.albumList[lib_pos.album.index - 1];
if (lib_pos.album != null && isAlbumExpanded(lib_pos.album)) {
lib_pos.track = lib_pos.album.trackList[lib_pos.album.trackList.length - 1];
}
} else if (libPos.artist != null) {
libPos.artist = player.searchResults.artistList[libPos.artist.index - 1];
if (libPos.artist != null && isArtistExpanded(libPos.artist)) {
libPos.album = libPos.artist.albumList[libPos.artist.albumList.length - 1];
if (libPos.album != null && isAlbumExpanded(libPos.album)) {
libPos.track = libPos.album.trackList[libPos.album.trackList.length - 1];
} else if (lib_pos.artist != null) {
lib_pos.artist = player.searchResults.artistList[lib_pos.artist.index - 1];
if (lib_pos.artist != null && isArtistExpanded(lib_pos.artist)) {
lib_pos.album = lib_pos.artist.albumList[lib_pos.artist.albumList.length - 1];
if (lib_pos.album != null && isAlbumExpanded(lib_pos.album)) {
lib_pos.track = lib_pos.album.trackList[lib_pos.album.trackList.length - 1];
}
}
}
@ -1549,14 +1548,14 @@ function setUpGenericUi(){
$document.on('mouseout', '.hoverable', function(event){
$(this).removeClass("ui-state-hover");
});
$(".jquery-button").button().on('click', blur);
$(".jquery-button").button();
$document.on('mousedown', function(){
removeContextMenu();
selection.fullClear();
selection.type = null;
refreshSelection();
});
$document.on('keydown', function(event){
var handler = keyboardHandlers[event.which];
var handler = keyboard_handlers[event.which];
if (handler == null) return true;
if (handler.ctrl != null && handler.ctrl !== event.ctrlKey) return true;
if (handler.alt != null && handler.alt !== event.altKey) return true;
@ -1564,41 +1563,23 @@ function setUpGenericUi(){
handler.handler(event);
return false;
});
$shortcuts.on('keydown', function(event) {
event.stopPropagation();
if (event.which === 27) {
$shortcuts.dialog('close');
}
});
}
function blur() {
$(this).blur();
}
var dynamicModeLabel = document.getElementById('dynamic-mode-label');
var plBtnRepeatLabel = document.getElementById('pl-btn-repeat-label');
function setUpPlaylistUi(){
$pl_window.on('click', 'button.clear', function(event){
$pl_window.on('click', 'button.clear', function(){
player.clear();
});
$pl_window.on('mousedown', 'button.clear', stopPropagation);
$pl_window.on('click', 'button.shuffle', function(){
player.shuffle();
});
$pl_window.on('mousedown', 'button.shuffle', stopPropagation);
$pl_btn_repeat.on('click', nextRepeatState);
plBtnRepeatLabel.addEventListener('mousedown', stopPropagation, false);
$pl_btn_repeat.on('click', function(){
nextRepeatState();
});
$dynamicMode.on('click', function(){
var value = $(this).prop("checked");
setDynamicMode(value);
return false;
});
dynamicModeLabel.addEventListener('mousedown', stopPropagation, false);
$playlistItems.on('dblclick', '.pl-item', function(event){
var trackId = $(this).attr('data-id');
player.seek(trackId, 0);
@ -1607,18 +1588,20 @@ function setUpPlaylistUi(){
return event.altKey;
});
$playlistItems.on('mousedown', '.pl-item', function(event){
var trackId, skipDrag;
if (started_drag) return true;
var trackId, skip_drag;
if (started_drag) {
return true;
}
$(document.activeElement).blur();
if (event.which === 1) {
event.preventDefault();
removeContextMenu();
trackId = $(this).attr('data-id');
skipDrag = false;
skip_drag = false;
if (!selection.isPlaylist()) {
selection.selectOnly('playlist', trackId);
} else if (event.ctrlKey || event.shiftKey) {
skipDrag = true;
skip_drag = true;
if (event.shiftKey && !event.ctrlKey) {
// range select click
selection.cursor = trackId;
@ -1634,7 +1617,7 @@ function setUpPlaylistUi(){
selection.selectOnly('playlist', trackId);
}
refreshSelection();
if (!skipDrag) {
if (!skip_drag) {
return performDrag(event, {
complete: function(result, event){
var delta, id;
@ -1657,7 +1640,9 @@ function setUpPlaylistUi(){
});
}
} else if (event.which === 3) {
if (event.altKey) return;
if (event.altKey) {
return;
}
event.preventDefault();
removeContextMenu();
trackId = $(this).attr('data-id');
@ -1675,7 +1660,15 @@ function setUpPlaylistUi(){
left: event.pageX + 1,
top: event.pageY + 1
});
updateAdminActions($playlistMenu);
if (!permissions.admin) {
$playlistMenu.find('.delete')
.addClass('ui-state-disabled')
.attr('title', "Insufficient privileges. See Settings.");
} else {
$playlistMenu.find('.delete')
.removeClass('ui-state-disabled')
.attr('title', '');
}
}
});
$playlistItems.on('mousedown', function(){
@ -1690,266 +1683,33 @@ function setUpPlaylistUi(){
removeContextMenu();
return false;
});
$playlistMenu.on('click', '.download', onDownloadContextMenu);
$playlistMenu.on('click', '.delete', onDeleteContextMenu);
$playlistMenu.on('click', '.edit-tags', onEditTagsContextMenu);
}
$playlistMenu.on('click', '.download', function(){
removeContextMenu();
function stopPropagation(event) {
event.stopPropagation();
}
if (selection.isMulti()) {
downloadKeys(selection.toTrackKeys());
return false;
}
function onDownloadContextMenu() {
removeContextMenu();
if (selection.isMulti()) {
downloadKeys(selection.toTrackKeys());
return true;
});
$playlistMenu.on('click', '.delete', function(){
if (!permissions.admin) return false;
handleDeletePressed(true);
removeContextMenu();
return false;
}
return true;
}
function onDeleteContextMenu() {
if (!permissions.admin) return false;
removeContextMenu();
handleDeletePressed(true);
return false;
}
var editTagsTrackKeys = null;
var editTagsTrackIndex = null;
function onEditTagsContextMenu() {
if (!permissions.admin) return false;
removeContextMenu();
editTagsTrackKeys = selection.toTrackKeys();
editTagsTrackIndex = 0;
showEditTags();
return false;
}
var EDITABLE_PROPS = {
name: {
type: 'string',
write: true,
},
artistName: {
type: 'string',
write: true,
},
albumArtistName: {
type: 'string',
write: true,
},
albumName: {
type: 'string',
write: true,
},
compilation: {
type: 'boolean',
write: true,
},
track: {
type: 'integer',
write: true,
},
trackCount: {
type: 'integer',
write: true,
},
disc: {
type: 'integer',
write: true,
},
discCount: {
type: 'integer',
write: true,
},
year: {
type: 'integer',
write: true,
},
genre: {
type: 'string',
write: true,
},
composerName: {
type: 'string',
write: true,
},
performerName: {
type: 'string',
write: true,
},
file: {
type: 'string',
write: false,
},
};
var EDIT_TAG_TYPES = {
'string': {
get: function(domItem) {
return domItem.value;
},
set: function(domItem, value) {
domItem.value = value || "";
},
},
'integer': {
get: function(domItem) {
var n = parseInt(domItem.value, 10);
if (isNaN(n)) return null;
return n;
},
set: function(domItem, value) {
domItem.value = value == null ? "" : value;
},
},
'boolean': {
get: function(domItem) {
return domItem.checked;
},
set: function(domItem, value) {
domItem.checked = !!value;
},
},
};
var perDom = document.getElementById('edit-tags-per');
var perLabelDom = document.getElementById('edit-tags-per-label');
var prevDom = document.getElementById('edit-tags-prev');
var nextDom = document.getElementById('edit-tags-next');
var editTagsFocusDom = document.getElementById('edit-tag-name');
function updateEditTagsUi() {
var multiple = editTagsTrackKeys.length > 1;
prevDom.disabled = !perDom.checked || editTagsTrackIndex === 0;
nextDom.disabled = !perDom.checked || (editTagsTrackIndex === editTagsTrackKeys.length - 1);
prevDom.style.visibility = multiple ? 'visible' : 'hidden';
nextDom.style.visibility = multiple ? 'visible' : 'hidden';
perLabelDom.style.visibility = multiple ? 'visible' : 'hidden';
var multiCheckBoxVisible = multiple && !perDom.checked;
var trackKeysToUse = perDom.checked ? [editTagsTrackKeys[editTagsTrackIndex]] : editTagsTrackKeys;
for (var propName in EDITABLE_PROPS) {
var propInfo = EDITABLE_PROPS[propName];
var type = propInfo.type;
var setter = EDIT_TAG_TYPES[type].set;
var domItem = document.getElementById('edit-tag-' + propName);
domItem.disabled = !propInfo.write;
var multiCheckBoxDom = document.getElementById('edit-tag-multi-' + propName);
multiCheckBoxDom.style.visibility = (multiCheckBoxVisible && propInfo.write) ? 'visible' : 'hidden';
var commonValue = null;
var consistent = true;
for (var i = 0; i < trackKeysToUse.length; i += 1) {
var key = trackKeysToUse[i];
var track = player.library.trackTable[key];
var value = track[propName];
if (commonValue == null) {
commonValue = value;
} else if (commonValue !== value) {
consistent = false;
break;
}
}
multiCheckBoxDom.checked = consistent;
setter(domItem, consistent ? commonValue : null);
}
}
function showEditTags() {
$editTagsDialog.dialog({
modal: true,
title: "Edit Tags",
minWidth: 800,
height: $document.height() - 40,
});
perDom.checked = false;
updateEditTagsUi();
editTagsFocusDom.focus();
}
function setUpEditTagsUi() {
$editTagsDialog.find("input").on("keydown", function(event) {
event.stopPropagation();
if (event.which === 27) {
$editTagsDialog.dialog('close');
} else if (event.which === 13) {
saveAndClose();
}
});
for (var propName in EDITABLE_PROPS) {
var domItem = document.getElementById('edit-tag-' + propName);
var multiCheckBoxDom = document.getElementById('edit-tag-multi-' + propName);
var listener = createChangeListener(multiCheckBoxDom);
domItem.addEventListener('change', listener, false);
domItem.addEventListener('keypress', listener, false);
domItem.addEventListener('focus', onFocus, false);
}
function onFocus(event) {
editTagsFocusDom = event.target;
}
function createChangeListener(multiCheckBoxDom) {
return function() {
multiCheckBoxDom.checked = true;
};
}
$("#edit-tags-ok").on('click', saveAndClose);
$("#edit-tags-cancel").on('click', closeDialog);
perDom.addEventListener('click', updateEditTagsUi, false);
nextDom.addEventListener('click', saveAndNext, false);
prevDom.addEventListener('click', saveAndPrev, false);
function saveAndMoveOn(dir) {
save();
editTagsTrackIndex += dir;
updateEditTagsUi();
editTagsFocusDom.focus();
editTagsFocusDom.select();
}
function saveAndNext() {
saveAndMoveOn(1);
}
function saveAndPrev() {
saveAndMoveOn(-1);
}
function save() {
var trackKeysToUse = perDom.checked ? [editTagsTrackKeys[editTagsTrackIndex]] : editTagsTrackKeys;
var cmd = {};
for (var i = 0; i < trackKeysToUse.length; i += 1) {
var key = trackKeysToUse[i];
var track = player.library.trackTable[key];
var props = cmd[track.key] = {};
for (var propName in EDITABLE_PROPS) {
var propInfo = EDITABLE_PROPS[propName];
var type = propInfo.type;
var getter = EDIT_TAG_TYPES[type].get;
var domItem = document.getElementById('edit-tag-' + propName);
var multiCheckBoxDom = document.getElementById('edit-tag-multi-' + propName);
if (multiCheckBoxDom.checked && propInfo.write) {
props[propName] = getter(domItem);
}
}
}
player.sendCommand('updateTags', cmd);
}
function saveAndClose() {
save();
closeDialog();
}
function closeDialog() {
$editTagsDialog.dialog('close');
}
}
function updateSliderUi(value){
var percent = value * 100;
$trackSlider.css('background-size', percent + "% 100%");
$track_slider.css('background-size', percent + "% 100%");
}
function setUpNowPlayingUi(){
var actions = {
var actions, cls, action;
actions = {
toggle: togglePlayback,
prev: function(){
player.prev();
@ -1961,11 +1721,11 @@ function setUpNowPlayingUi(){
player.stop();
}
};
for (var cls in actions) {
var action = actions[cls];
setUpMouseDownListener(cls, action);
for (cls in actions) {
action = actions[cls];
(fn$.call(this, cls, action));
}
$trackSlider.slider({
$track_slider.slider({
step: 0.0001,
min: 0,
max: 1,
@ -1983,30 +1743,32 @@ function setUpNowPlayingUi(){
$nowplaying_elapsed.html(formatTime(ui.value * player.currentItem.track.duration));
},
start: function(event, ui){
userIsSeeking = true;
user_is_seeking = true;
},
stop: function(event, ui){
userIsSeeking = false;
user_is_seeking = false;
}
});
function setVol(event, ui){
if (event.originalEvent == null) return;
if (event.originalEvent == null) {
return;
}
player.setVolume(ui.value);
}
$volSlider.slider({
$vol_slider.slider({
step: 0.01,
min: 0,
max: 1,
change: setVol,
start: function(event, ui){
userIsVolumeSliding = true;
user_is_volume_sliding = true;
},
stop: function(event, ui){
userIsVolumeSliding = false;
user_is_volume_sliding = false;
}
});
setInterval(updateSliderPos, 100);
function setUpMouseDownListener(cls, action){
function fn$(cls, action){
$nowplaying.on('mousedown', "li." + cls, function(event){
action();
return false;
@ -2200,8 +1962,16 @@ function updateSettingsAuthUi() {
streamUrlDom.setAttribute('href', streaming.getUrl());
}
function updateSettingsAdminUi() {
$toggleHardwarePlayback
.button('option', 'label', hardwarePlaybackOn ? 'On' : 'Off')
.prop('checked', hardwarePlaybackOn)
.button('refresh');
}
function setUpSettingsUi(){
$toggleScrobble.button();
$toggleHardwarePlayback.button();
$lastFmSignOut.button();
$settingsAuthCancel.button();
$settingsAuthSave.button();
@ -2234,6 +2004,11 @@ function setUpSettingsUi(){
socket.send(msg, params);
updateLastFmSettingsUi();
});
$toggleHardwarePlayback.on('click', function(event) {
var value = $(this).prop('checked');
socket.send('hardwarePlayback', value);
updateSettingsAdminUi();
});
$settingsAuthEdit.on('click', function(event) {
settings_ui.auth.show_edit = true;
updateSettingsAuthUi();
@ -2352,9 +2127,22 @@ function setUpLibraryUi(){
removeContextMenu();
return false;
});
$libraryMenu.on('click', '.download', onDownloadContextMenu);
$libraryMenu.on('click', '.delete', onDeleteContextMenu);
$libraryMenu.on('click', '.edit-tags', onEditTagsContextMenu);
$libraryMenu.on('click', '.download', function(){
removeContextMenu();
if (selection.isMulti()) {
downloadKeys(selection.toTrackKeys());
return false;
}
return true;
});
$libraryMenu.on('click', '.delete', function(){
if (!permissions.admin) return false;
handleDeletePressed(true);
removeContextMenu();
return false;
});
}
function genericTreeUi($elem, options){
@ -2385,11 +2173,11 @@ function genericTreeUi($elem, options){
function leftMouseDown(event){
event.preventDefault();
removeContextMenu();
var skipDrag = false;
var skip_drag = false;
if (!options.isSelectionOwner()) {
selection.selectOnly(type, key);
} else if (event.ctrlKey || event.shiftKey) {
skipDrag = true;
skip_drag = true;
selection.cursor = key;
selection.type = type;
if (!event.shiftKey && !event.ctrlKey) {
@ -2403,7 +2191,7 @@ function genericTreeUi($elem, options){
selection.selectOnly(type, key);
}
refreshSelection();
if (!skipDrag) {
if (!skip_drag) {
performDrag(event, {
complete: function(result, event){
var delta = {
@ -2444,24 +2232,21 @@ function genericTreeUi($elem, options){
left: event.pageX + 1,
top: event.pageY + 1
});
updateAdminActions($libraryMenu);
if (!permissions.admin) {
$libraryMenu.find('.delete')
.addClass('ui-state-disabled')
.attr('title', "Insufficient privileges. See Settings.");
} else {
$libraryMenu.find('.delete')
.removeClass('ui-state-disabled')
.attr('title', '');
}
}
});
$elem.on('mousedown', function(){
return false;
});
}
function updateAdminActions($menu) {
if (!permissions.admin) {
$menu.find('.delete,.edit-tags')
.addClass('ui-state-disabled')
.attr('title', "Insufficient privileges. See Settings.");
} else {
$menu.find('.delete,.edit-tags')
.removeClass('ui-state-disabled')
.attr('title', '');
}
}
function setUpUi(){
setUpGenericUi();
setUpPlaylistUi();
@ -2470,7 +2255,6 @@ function setUpUi(){
setUpTabsUi();
setUpUploadUi();
setUpSettingsUi();
setUpEditTagsUi();
}
function toAlbumId(s) {
@ -2531,6 +2315,10 @@ $document.ready(function(){
});
return;
}
socket.on('hardwarePlayback', function(isOn) {
hardwarePlaybackOn = isOn;
updateSettingsAdminUi();
});
socket.on('LastFmApiKey', updateLastFmApiKey);
socket.on('permissions', function(data){
permissions = data;
@ -2555,6 +2343,7 @@ $document.ready(function(){
});
socket.on('connect', function(){
socket.send('subscribe', {name: 'dynamicModeOn'});
socket.send('subscribe', {name: 'hardwarePlayback'});
sendAuth();
load_status = LoadStatus.GoodToGo;
render();

View file

@ -10,8 +10,8 @@ module.exports = PlayerClient;
var compareSortKeyAndId = makeCompareProps(['sortKey', 'id']);
PlayerClient.REPEAT_OFF = 0;
PlayerClient.REPEAT_ONE = 1;
PlayerClient.REPEAT_ALL = 2;
PlayerClient.REPEAT_ALL = 1;
PlayerClient.REPEAT_ONE = 2;
util.inherits(PlayerClient, EventEmitter);
function PlayerClient(socket) {

View file

@ -12,11 +12,6 @@ audio.addEventListener('playing', onPlaying, false);
var $ = window.$;
var $streamBtn = $('#stream-btn');
document.getElementById('stream-btn-label').addEventListener('mousedown', onLabelDown, false);
function onLabelDown(event) {
event.stopPropagation();
}
function getButtonLabel() {
if (tryingToStream) {
@ -34,9 +29,14 @@ function getButtonLabel() {
}
}
function getButtonDisabled() {
return false;
}
function renderStreamButton(){
var label = getButtonLabel();
$streamBtn
.button("option", "disabled", getButtonDisabled())
.button("option", "label", label)
.prop("checked", tryingToStream)
.button("refresh");
@ -77,6 +77,8 @@ function updatePlayer() {
stillBuffering = true;
} else {
audio.pause();
audio.src = "";
audio.load();
stillBuffering = false;
}
actuallyStreaming = shouldStream;

View file

@ -45,7 +45,7 @@ body
list-style none outside none
margin 2px
padding 4px
h1
letter-spacing 0.1em
margin-top 8px
@ -138,7 +138,7 @@ body
div.cursor
text-decoration underline
#lib-filter
margin 4px
width 175px
@ -214,7 +214,7 @@ body
div.selected
selected-div()
div.current
border 1px solid #096AC8
background-color #292929
@ -244,22 +244,13 @@ body
padding 10px
#upload-by-url
margin: 4px
width: 90%
.ui-menu
width: 240px
font-size: 1em
#menu-library .ui-state-disabled.ui-state-focus,
#menu-playlist .ui-state-disabled.ui-state-focus
margin: .3em -1px .2em
#menu-library .menu-item-last.ui-state-disabled.ui-state-focus,
#menu-playlist .menu-item-last.ui-state-disabled.ui-state-focus
margin: 5px -1px .2em
height: 23px
#shortcuts
h1
margin-bottom 10px
@ -315,6 +306,3 @@ body
font-size .9em
li:before
content "\2713"
.accesskey
text-decoration: underline

View file

@ -28,7 +28,7 @@
<span class="ui-icon ui-icon-volume-on"></span>
</div>
<div id="more-playback-btns">
<input class="jquery-button" type="checkbox" id="stream-btn"><label id="stream-btn-label" for="stream-btn">Stream</label>
<input type="checkbox" id="stream-btn"><label for="stream-btn">Stream</label>
</div>
<h1 id="track-display"></h1>
<div id="track-slider"></div>
@ -67,7 +67,7 @@
<input type="file" id="upload-input" multiple="multiple" placeholder="Drag and drop or click to browse">
</div>
<div>
Automatically queue uploads: <input class="jquery-button" type="checkbox" id="auto-queue-uploads"><label for="auto-queue-uploads">On</label>
Automatically queue uploads: <input type="checkbox" id="auto-queue-uploads"><label for="auto-queue-uploads">On</label>
</div>
</div>
</div>
@ -103,7 +103,7 @@
</p>
<p>
Scrobbling is
<input class="jquery-button" type="checkbox" id="toggle-scrobble"><label for="toggle-scrobble">Off</label>
<input type="checkbox" id="toggle-scrobble"><label for="toggle-scrobble">Off</label>
</p>
</div>
<div id="settings-lastfm-out">
@ -112,6 +112,13 @@
</p>
</div>
</div>
<div class="section">
<h1>Admin</h1>
<p>
Hardware audio playback is
<input type="checkbox" id="toggle-hardware-playback"><label for="toggle-hardware-playback">On</label>
</p>
</div>
<div class="section">
<h1>About</h1>
<ul>
@ -126,8 +133,8 @@
<div class="window-header">
<button class="jquery-button clear">Clear</button>
<button class="jquery-button shuffle">Shuffle</button>
<input class="jquery-button" type="checkbox" id="dynamic-mode"><label id="dynamic-mode-label" for="dynamic-mode">Dynamic Mode</label>
<input class="jquery-button" type="checkbox" id="pl-btn-repeat"><label id="pl-btn-repeat-label" for="pl-btn-repeat">Repeat: Off</label>
<input class="jquery-button" type="checkbox" id="dynamic-mode"><label for="dynamic-mode">Dynamic Mode</label>
<input class="jquery-button" type="checkbox" id="pl-btn-repeat"><label for="pl-btn-repeat">Repeat: Off</label>
</div>
<div id="playlist">
<div class="header">
@ -148,7 +155,7 @@
<div id="main-err-msg-text">Loading...</div>
</p>
</div>
<div id="shortcuts" style="display: none" tabindex="-1">
<div id="shortcuts" style="display: none">
<h1>Playback</h1>
<dl>
<dt>Space</dt>
@ -289,74 +296,26 @@
<dd>Hold while selecting to select all items in between<dd>
</dl>
</div>
<div id="edit-tags" style="display: none">
<input type="checkbox" id="edit-tag-multi-name">
<label accesskey="i">T<span class="accesskey">i</span>tle: <input id="edit-tag-name"></label><br>
<input type="checkbox" id="edit-tag-multi-track">
<label accesskey="k">Trac<span class="accesskey">k</span> Number: <input id="edit-tag-track"></label><br>
<input type="checkbox" id="edit-tag-multi-file">
<label>Filename: <input id="edit-tag-file"></label><br>
<hr>
<input type="checkbox" id="edit-tag-multi-artistName">
<label accesskey="a"><span class="accesskey">A</span>rtist: <input id="edit-tag-artistName"></label><br>
<input type="checkbox" id="edit-tag-multi-composerName">
<label accesskey="c"><span class="accesskey">C</span>omposer: <input id="edit-tag-composerName"></label><br>
<input type="checkbox" id="edit-tag-multi-performerName">
<label>Performer: <input id="edit-tag-performerName"></label><br>
<input type="checkbox" id="edit-tag-multi-genre">
<label accesskey="g"><span class="accesskey">G</span>enre: <input id="edit-tag-genre"></label><br>
<hr>
<input type="checkbox" id="edit-tag-multi-albumName">
<label accesskey="b">Al<span class="accesskey">b</span>um: <input id="edit-tag-albumName"></label><br>
<input type="checkbox" id="edit-tag-multi-albumArtistName">
<label>Album Artist: <input id="edit-tag-albumArtistName"></label><br>
<input type="checkbox" id="edit-tag-multi-trackCount">
<label>Track Count: <input id="edit-tag-trackCount"></label><br>
<input type="checkbox" id="edit-tag-multi-year">
<label accesskey="y"><span class="accesskey">Y</span>ear: <input id="edit-tag-year"></label><br>
<input type="checkbox" id="edit-tag-multi-disc">
<label accesskey="d"><span class="accesskey">D</span>isc Number: <input id="edit-tag-disc"></label><br>
<input type="checkbox" id="edit-tag-multi-discCount">
<label>Disc Count: <input id="edit-tag-discCount"></label><br>
<input type="checkbox" id="edit-tag-multi-compilation">
<label accesskey="m">Co<span class="accesskey">m</span>pilation: <input type="checkbox" id="edit-tag-compilation"></label><br>
<hr>
<div style="float: right">
<button id="edit-tags-ok" accesskey="v">Sa<span class="accesskey">v</span>e &amp; Close</button>
<button id="edit-tags-cancel">Cancel</button>
</div>
<button id="edit-tags-prev" type="button" accesskey="p"><span class="accesskey">P</span>revious</button>
<button id="edit-tags-next" type="button" accesskey="n"><span class="accesskey">N</span>ext</button>
<label accesskey="r" id="edit-tags-per-label"><input id="edit-tags-per" type="checkbox">Pe<span class="accesskey">r</span> Track</label>
</div>
<ul id="menu-playlist" style="display: none">
<li><a href="#" class="remove">Remove</a></li>
<li><a href="#" class="delete">Delete From Library</a></li>
<li><a href="#" class="download" target="_blank">Download</a></li>
<li><a href="#" class="edit-tags">Edit Tags</a></li>
<li>
<a href="#" class="delete">Delete From Library</a>
</li>
<li>
<a href="#" class="download" target="_blank">Download</a>
</li>
</ul>
<ul id="menu-library" style="display: none">
<li><a href="#" class="queue">Queue</a></li>
<li><a href="#" class="queue-next">Queue Next</a></li>
<li><a href="#" class="queue-random">Queue in Random Order</a></li>
<li><a href="#" class="queue-next-random">Queue Next in Random Order</a></li>
<li><a href="#" class="delete">Delete</a></li>
<li><a href="#" class="download" target="_blank">Download</a></li>
<li><a href="#" class="edit-tags menu-item-last">Edit Tags</a></li>
<li>
<a href="#" class="delete">Delete</a>
</li>
<li>
<a href="#" class="download" target="_blank">Download</a>
</li>
</ul>
<script src="vendor/jquery-2.1.0.min.js"></script>
<script src="vendor/jquery-ui-1.10.4.custom.min.js"></script>