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. 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 [Raspberry Pi](http://www.raspberrypi.org/)) connected to some speakers
in your home or office. Guests can control the music player by connecting 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 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 Groove Basin works with your personal music library; not an external music
service. Groove Basin will never support DRM content. service. Groove Basin will never support DRM content.
Try out the [live demo](http://demo.groovebasin.com/). Live discussion in #libgroove on Freenode.
## Features ## Features
@ -46,10 +46,7 @@ Try out the [live demo](http://demo.groovebasin.com/).
## Install ## Install
1. Install [Node.js](http://nodejs.org) v0.10.x. Note that on Debian and 1. Install [Node.js](http://nodejs.org) v0.10.x.
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.
2. Install [libgroove](https://github.com/andrewrk/libgroove). 2. Install [libgroove](https://github.com/andrewrk/libgroove).
3. Clone the source. 3. Clone the source.
4. `npm run build` 4. `npm run build`
@ -76,11 +73,6 @@ $ npm run dev
This will install dependencies, build generated files, and then start the 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. 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 ### Roadmap
1. Tag Editing 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", lastFmApiSecret: "8713e8e893c5264608e584a232dd10a0",
mpdHost: '0.0.0.0', mpdHost: '0.0.0.0',
mpdPort: 6600, mpdPort: 6600,
acoustidAppKey: 'bgFvC4vW',
instantBufferBytes: 220 * 1024,
}; };
defaultConfig.permissions[genPassword()] = { defaultConfig.permissions[genPassword()] = {
@ -140,7 +138,7 @@ GrooveBasin.prototype.start = function() {
self.initializeDownload(); self.initializeDownload();
self.initializeUpload(); 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) { self.player.initialize(function(err) {
if (err) { if (err) {
console.error("unable to initialize player:", err.stack); 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) { 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; var trackTable = self.player.libraryIndex.trackTable;
if (!caseSensitive) { if (!caseSensitive) {
filters.forEach(function(filter) { filters.forEach(function(filter) {
@ -1329,24 +1329,13 @@ function forEachMatchingTrack(self, filters, caseSensitive, fn) {
} }
for (var key in trackTable) { for (var key in trackTable) {
var track = trackTable[key]; var track = trackTable[key];
var matches = false; var matches = true;
for (var filterIndex = 0; filterIndex < filters.length; filterIndex += 1) { for (var filterIndex = 0; filterIndex < filters.length; filterIndex += 1) {
var filter = filters[filterIndex]; var filter = filters[filterIndex];
var filterField = String(track[filter.field]); var filterField = track[filter.field];
if (!filterField) continue;
if (!caseSensitive && filterField) filterField = filterField.toLowerCase(); if (!caseSensitive && filterField) filterField = filterField.toLowerCase();
if (filterField !== filter.value) {
/* assumes: matches = false;
* 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; break;
} }
} }
@ -1394,23 +1383,12 @@ function parseFindArgs(self, args, caseSensitive, onTrack, cb, onFinish) {
} }
var filters = []; var filters = [];
for (var i = 0; i < args.length; i += 2) { 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()]; var tagType = tagTypes[args[i].toLowerCase()];
if (!tagType) return cb(ERR_CODE_ARG, "\"" + args[i] + "\" is not known"); 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({ filters.push({
field: tagsToSearch[j].grooveTag, field: tagType.grooveTag,
value: args[i+1], value: args[i+1],
}); });
}
forEachMatchingTrack(self, filters, caseSensitive, onTrack); forEachMatchingTrack(self, filters, caseSensitive, onTrack);
} }
onFinish(); onFinish();

View file

@ -17,6 +17,7 @@ var safePath = require('./safe_path');
var PassThrough = require('stream').PassThrough; var PassThrough = require('stream').PassThrough;
var url = require('url'); var url = require('url');
var superagent = require('superagent'); var superagent = require('superagent');
var ytdl = require('ytdl');
module.exports = Player; module.exports = Player;
@ -25,171 +26,40 @@ groove.setLogging(groove.LOG_WARNING);
var cpuCount = require('os').cpus().length; var cpuCount = require('os').cpus().length;
// sorted from worst to best
var YTDL_AUDIO_ENCODINGS = [
'mp3',
'aac',
'wma',
'vorbis',
'wav',
'flac',
];
var PLAYER_KEY_PREFIX = "Player."; var PLAYER_KEY_PREFIX = "Player.";
var LIBRARY_KEY_PREFIX = "Library."; var LIBRARY_KEY_PREFIX = "Library.";
var LIBRARY_DIR_PREFIX = "LibraryDir."; var LIBRARY_DIR_PREFIX = "LibraryDir.";
var PLAYLIST_KEY_PREFIX = "Playlist."; var PLAYLIST_KEY_PREFIX = "Playlist.";
// db: store in the DB // these are the ones we store in the DB, not the ones we send to the web
// read: send to clients var DB_FILE_PROPS = [
// write: accept updates from clients 'key', 'name', 'artistName', 'albumArtistName',
var DB_PROPS = { 'albumName', 'compilation', 'track', 'trackCount',
key: { 'disc', 'discCount', 'duration', 'year', 'genre',
db: true, 'file', 'mtime', 'replayGainAlbumGain', 'replayGainAlbumPeak',
read: true, 'replayGainTrackGain', 'replayGainTrackPeak',
write: false, 'composerName', 'performerName', 'lastQueueDate',
type: 'string', ];
},
name: {
db: true,
read: true,
write: true,
type: 'string',
},
artistName: {
db: true,
read: true,
write: true,
type: 'string',
},
albumArtistName: {
db: true,
read: true,
write: true,
type: 'string',
},
albumName: {
db: true,
read: true,
write: true,
type: 'string',
},
compilation: {
db: true,
read: true,
write: true,
type: 'boolean',
},
track: {
db: true,
read: true,
write: true,
type: 'integer',
},
trackCount: {
db: true,
read: true,
write: true,
type: 'integer',
},
disc: {
db: true,
read: true,
write: true,
type: 'integer',
},
discCount: {
db: true,
read: true,
write: true,
type: 'integer',
},
duration: {
db: true,
read: true,
write: false,
type: 'float',
},
year: {
db: true,
read: true,
write: true,
type: 'integer',
},
genre: {
db: true,
read: true,
write: true,
type: 'string',
},
file: {
db: true,
read: true,
write: false,
type: 'string',
},
mtime: {
db: true,
read: false,
write: false,
type: 'integer',
},
replayGainAlbumGain: {
db: true,
read: false,
write: false,
type: 'float',
},
replayGainAlbumPeak: {
db: true,
read: false,
write: false,
type: 'float',
},
replayGainTrackGain: {
db: true,
read: false,
write: false,
type: 'float',
},
replayGainTrackPeak: {
db: true,
read: false,
write: false,
type: 'float',
},
composerName: {
db: true,
read: true,
write: true,
type: 'string',
},
performerName: {
db: true,
read: true,
write: true,
type: 'string',
},
lastQueueDate: {
db: true,
read: false,
write: false,
type: 'date',
},
};
var PROP_TYPE_PARSERS = {
'string': function(value) {
return value ? String(value) : "";
},
'date': function(value) {
if (!value) return null;
var date = new Date(value);
if (isNaN(date.getTime())) return null;
return date;
},
'integer': parseIntOrNull,
'float': parseFloatOrNull,
'boolean': function(value) {
return value == null ? null : !!value;
},
};
// how many GrooveFiles to keep open, ready to be decoded // how many GrooveFiles to keep open, ready to be decoded
var OPEN_FILE_COUNT = 8; var OPEN_FILE_COUNT = 8;
var PREV_FILE_COUNT = Math.floor(OPEN_FILE_COUNT / 2); var PREV_FILE_COUNT = Math.floor(OPEN_FILE_COUNT / 2);
var NEXT_FILE_COUNT = OPEN_FILE_COUNT - PREV_FILE_COUNT; var NEXT_FILE_COUNT = OPEN_FILE_COUNT - PREV_FILE_COUNT;
// when a streaming client connects we send them many buffers quickly
// in order to get the stream started, then we slow down.
var ENCODE_QUEUE_DURATION = 5;
var DB_SCALE = Math.log(10.0) * 0.05; var DB_SCALE = Math.log(10.0) * 0.05;
var REPLAYGAIN_PREAMP = 0.75; var REPLAYGAIN_PREAMP = 0.75;
var REPLAYGAIN_DEFAULT = 0.25; var REPLAYGAIN_DEFAULT = 0.25;
@ -201,7 +71,7 @@ Player.REPEAT_ALL = 2;
Player.trackWithoutIndex = trackWithoutIndex; Player.trackWithoutIndex = trackWithoutIndex;
util.inherits(Player, EventEmitter); util.inherits(Player, EventEmitter);
function Player(db, musicDirectory, instantBufferBytes) { function Player(db, musicDirectory) {
EventEmitter.call(this); EventEmitter.call(this);
this.setMaxListeners(0); this.setMaxListeners(0);
@ -211,10 +81,6 @@ function Player(db, musicDirectory, instantBufferBytes) {
this.libraryIndex = new MusicLibraryIndex(); this.libraryIndex = new MusicLibraryIndex();
this.addQueue = new DedupedQueue({processOne: this.addToLibrary.bind(this)}); this.addQueue = new DedupedQueue({processOne: this.addToLibrary.bind(this)});
// when a streaming client connects we send them many buffers quickly
// in order to get the stream started, then we slow down.
this.instantBufferBytes = instantBufferBytes;
this.dirs = {}; this.dirs = {};
this.dirScanQueue = new DedupedQueue({ this.dirScanQueue = new DedupedQueue({
processOne: this.refreshFilesIndex.bind(this), processOne: this.refreshFilesIndex.bind(this),
@ -224,9 +90,6 @@ function Player(db, musicDirectory, instantBufferBytes) {
maxAsync: 1, maxAsync: 1,
}); });
this.groovePlayer = null; // initialized by initialize method
this.groovePlaylist = null; // initialized by initialize method
this.playlist = {}; this.playlist = {};
this.currentTrack = null; this.currentTrack = null;
this.tracksInOrder = []; // another way to look at playlist this.tracksInOrder = []; // another way to look at playlist
@ -235,6 +98,7 @@ function Player(db, musicDirectory, instantBufferBytes) {
this.invalidPaths = {}; // files that could not be opened this.invalidPaths = {}; // files that could not be opened
this.repeat = Player.REPEAT_OFF; this.repeat = Player.REPEAT_OFF;
this.hardwarePlayback = null;
this.isPlaying = false; this.isPlaying = false;
this.trackStartDate = null; this.trackStartDate = null;
this.pausedTime = 0; this.pausedTime = 0;
@ -249,25 +113,29 @@ function Player(db, musicDirectory, instantBufferBytes) {
this.headerBuffers = []; this.headerBuffers = [];
this.recentBuffers = []; this.recentBuffers = [];
this.recentBuffersByteCount = 0;
this.newHeaderBuffers = []; this.newHeaderBuffers = [];
this.openStreamers = []; this.openStreamers = [];
this.lastEncodeItem = null;
this.lastEncodePos = null;
this.expectHeaders = true; this.expectHeaders = true;
this.playlistItemDeleteQueue = []; this.groovePlaylist = groove.createPlaylist();
this.groovePlayer = groove.createPlayer();
this.grooveEncoder = groove.createEncoder();
this.importUrlFilters = []; this.manualTimeInterval = null;
this.pendingEncoderAttachDetach = null;
this.desiredEncoderAttachState = false;
this.flushEncodedInterval = null;
this.groovePlaylist.pause();
this.volume = this.groovePlaylist.volume;
this.grooveEncoder.formatShortName = "mp3";
this.grooveEncoder.codecShortName = "mp3";
this.grooveEncoder.bitRate = 256 * 1000;
} }
Player.prototype.initialize = function(cb) { Player.prototype.initialize = function(cb) {
var self = this; var self = this;
var pend = new Pend(); initLibrary(function(err) {
pend.go(initPlayer);
pend.go(initLibrary);
pend.wait(function(err) {
if (err) return cb(err); if (err) return cb(err);
self.requestUpdateDb(); self.requestUpdateDb();
playlistChanged(self); playlistChanged(self);
@ -275,109 +143,6 @@ Player.prototype.initialize = function(cb) {
cacheAllOptions(cb); cacheAllOptions(cb);
}); });
function initPlayer(cb) {
var groovePlaylist = groove.createPlaylist();
var groovePlayer = groove.createPlayer();
var grooveEncoder = groove.createEncoder();
grooveEncoder.formatShortName = "mp3";
grooveEncoder.codecShortName = "mp3";
grooveEncoder.bitRate = 256 * 1000;
var pend = new Pend();
pend.go(function(cb) {
groovePlayer.attach(groovePlaylist, cb);
});
pend.go(function(cb) {
grooveEncoder.attach(groovePlaylist, cb);
});
pend.wait(doneAttaching);
function doneAttaching(err) {
if (err) {
cb(err);
return;
}
self.groovePlaylist = groovePlaylist;
self.groovePlayer = groovePlayer;
self.grooveEncoder = grooveEncoder;
self.groovePlaylist.pause();
self.volume = self.groovePlaylist.volume;
self.groovePlayer.on('nowplaying', onNowPlaying);
self.flushEncodedInterval = setInterval(flushEncoded, 10);
cb();
function flushEncoded() {
// poll the encoder for more buffers until either there are no buffers
// available or we get enough buffered
while (1) {
var bufferedSeconds = self.secondsIntoFuture(self.lastEncodeItem, self.lastEncodePos);
if (bufferedSeconds > 0.5 && self.recentBuffersByteCount >= self.instantBufferBytes) return;
var buf = self.grooveEncoder.getBuffer();
if (!buf) return;
if (buf.buffer) {
if (buf.item) {
if (self.expectHeaders) {
console.log("encoder: got first non-header");
self.headerBuffers = self.newHeaderBuffers;
self.newHeaderBuffers = [];
self.expectHeaders = false;
}
self.recentBuffers.push(buf.buffer);
self.recentBuffersByteCount += buf.buffer.length;
while (self.recentBuffers.length > 0 &&
self.recentBuffersByteCount - self.recentBuffers[0].length >= self.instantBufferBytes)
{
self.recentBuffersByteCount -= self.recentBuffers.shift().length;
}
for (var i = 0; i < self.openStreamers.length; i += 1) {
self.openStreamers[i].write(buf.buffer);
}
self.lastEncodeItem = buf.item;
self.lastEncodePos = buf.pos;
} else if (self.expectHeaders) {
// this is a header
console.log("encoder: got header");
self.newHeaderBuffers.push(buf.buffer);
} else {
// it's a footer, ignore the fuck out of it
console.info("ignoring encoded audio footer");
}
} else {
// end of playlist sentinel
console.log("encoder: end of playlist sentinel");
self.expectHeaders = true;
}
}
}
function onNowPlaying() {
var playHead = self.groovePlayer.position();
var decodeHead = self.groovePlaylist.position();
var playHeadDbKey = playHead.item && self.grooveItems[playHead.item.id].key;
var playHeadDbFile = playHeadDbKey && self.libraryIndex.trackTable[playHeadDbKey];
var playHeadFile = playHeadDbFile && playHeadDbFile.file;
console.info("onNowPlaying event. playhead:", playHeadFile);
if (playHead.item) {
var nowMs = (new Date()).getTime();
var posMs = playHead.pos * 1000;
self.trackStartDate = new Date(nowMs - posMs);
self.currentTrack = self.grooveItems[playHead.item.id];
playlistChanged(self);
self.emit('currentTrack');
} else if (!decodeHead.item) {
// both play head and decode head are null. end of playlist.
console.log("end of playlist");
self.currentTrack = null;
playlistChanged(self);
self.emit('currentTrack');
}
}
}
}
function initLibrary(cb) { function initLibrary(cb) {
var pend = new Pend(); var pend = new Pend();
pend.go(cacheAllDb); pend.go(cacheAllDb);
@ -416,6 +181,7 @@ Player.prototype.initialize = function(cb) {
dynamicModeOn: null, dynamicModeOn: null,
dynamicModeHistorySize: null, dynamicModeHistorySize: null,
dynamicModeFutureSize: null, dynamicModeFutureSize: null,
hardwarePlayback: null,
}; };
var pend = new Pend(); var pend = new Pend();
for (var name in options) { for (var name in options) {
@ -435,6 +201,12 @@ Player.prototype.initialize = function(cb) {
if (options.dynamicModeFutureSize != null) { if (options.dynamicModeFutureSize != null) {
self.setDynamicModeFutureSize(options.dynamicModeFutureSize); self.setDynamicModeFutureSize(options.dynamicModeFutureSize);
} }
var hardwarePlaybackValue = options.hardwarePlayback == null ? true : options.hardwarePlayback;
if (hardwarePlaybackValue) {
self.setHardwarePlayback(hardwarePlaybackValue);
} else {
self.refreshManualTimeInterval();
}
cb(); cb();
}); });
@ -519,6 +291,181 @@ Player.prototype.initialize = function(cb) {
} }
}; };
function startEncoderAttach(self, cb) {
self.desiredEncoderAttachState = true;
if (!self.pendingEncoderAttachDetach) {
self.pendingEncoderAttachDetach = true;
self.grooveEncoder.attach(self.groovePlaylist, function(err) {
if (err) return cb(err);
self.pendingEncoderAttachDetach = false;
if (!self.desiredEncoderAttachState) startEncoderDetach(self, cb);
});
}
}
function startEncoderDetach(self, cb) {
self.desiredEncoderAttachState = false;
if (!self.pendingEncoderAttachDetach) {
self.pendingEncoderAttachDetach = true;
self.grooveEncoder.detach(function(err) {
if (err) return cb(err);
self.pendingEncoderAttachDetach = false;
if (self.desiredEncoderAttachState) startEncoderAttach(self, cb);
});
}
}
Player.prototype.refreshManualTimeInterval = function() {
var self = this;
var wantManualTime = !self.hardwarePlayback && !self.desiredEncoderAttachState;
if (wantManualTime && !self.manualTimeInterval) {
self.manualTimeInterval = setInterval(checkNowPlaying, 100);
} else if (!wantManualTime && self.manualTimeInterval) {
clearInterval(self.manualTimeInterval);
self.manualTimeInterval = null;
}
function checkNowPlaying() {
if (!self.currentTrack) return;
if (!self.isPlaying) return;
var now = new Date();
var dbFile = self.libraryIndex.trackTable[self.currentTrack.key];
var nextTrackBegin = new Date(self.trackStartDate.getTime() + dbFile.duration * 1000);
if (now > nextTrackBegin) {
self.currentTrack = self.tracksInOrder[self.currentTrack.index + 1];
self.trackStartDate = nextTrackBegin;
playlistChanged(self);
self.emit('currentTrack');
}
}
};
Player.prototype.getBufferedSeconds = function() {
if (this.recentBuffers.length < 2) return 0;
var curBuf = this.recentBuffers[0];
var curSongId = curBuf.item.id;
var prevBuf = curBuf;
var totalTime = 0;
for (var i = 1; i < this.recentBuffers.length - 1; i += 1) {
var buf = this.recentBuffers[i];
var thisSongId = buf.item.id;
if (thisSongId !== curSongId) {
curSongId = thisSongId;
totalTime += prevBuf.pos - curBuf.pos;
curBuf = buf;
}
prevBuf = buf;
}
totalTime += prevBuf.pos - curBuf.pos;
return totalTime;
};
Player.prototype.attachEncoder = function(cb) {
var self = this;
cb = cb || logIfError;
if (self.flushEncodedInterval) return cb();
console.info("first streamer connected - attaching encoder");
self.flushEncodedInterval = setInterval(flushEncoded, 20);
self.grooveEncoder.on('buffer', onBuffer);
self.refreshManualTimeInterval();
startEncoderAttach(self, cb);
function onBuffer() {
if (!self.desiredEncoderAttachState) return;
if (self.hardwarePlayback) return;
var encodeHead = self.grooveEncoder.position();
var decodeHead = self.groovePlaylist.position();
var prevCurrentTrack = self.currentTrack;
if (encodeHead.item) {
var nowMs = (new Date()).getTime();
var posMs = encodeHead.pos * 1000;
self.trackStartDate = new Date(nowMs - posMs);
self.currentTrack = self.grooveItems[encodeHead.item.id];
} else if (!decodeHead.item) {
// both play head and decode head are null. end of playlist.
console.log("encoder: end of playlist");
self.currentTrack = null;
}
if (prevCurrentTrack !== self.currentTrack) {
playlistChanged(self);
self.emit('currentTrack');
}
}
function flushEncoded() {
// get rid of old items
var buf;
while (buf = self.recentBuffers[0]) {
var thisBufTrack = self.grooveItems[buf.item.id];
if (!thisBufTrack) return;
if (thisBufTrack !== self.currentTrack || self.getCurPos() > buf.pos) {
self.recentBuffers.shift();
} else {
break;
}
}
// poll the encoder for more buffers until either there are no buffers
// available or we get enough buffered
while (self.getBufferedSeconds() < ENCODE_QUEUE_DURATION) {
buf = self.grooveEncoder.getBuffer();
if (!buf) break;
if (buf.buffer) {
if (buf.item) {
if (self.expectHeaders) {
console.log("encoder: got first non-header");
self.headerBuffers = self.newHeaderBuffers;
self.newHeaderBuffers = [];
self.expectHeaders = false;
}
self.recentBuffers.push(buf);
for (var i = 0; i < self.openStreamers.length; i += 1) {
self.openStreamers[i].write(buf.buffer);
}
} else if (self.expectHeaders) {
// this is a header
console.log("encoder: got header");
self.newHeaderBuffers.push(buf.buffer);
} else {
// it's a footer, ignore the fuck out of it
console.info("ignoring encoded audio footer");
}
} else {
// end of playlist sentinel
console.log("encoder: end of playlist sentinel");
self.expectHeaders = true;
}
}
}
function logIfError(err) {
if (err) {
console.error("Unable to attach encoder:", err.stack);
}
}
};
Player.prototype.detachEncoder = function(cb) {
cb = cb || logIfError;
this.clearEncodedBuffer();
clearInterval(this.flushEncodedInterval);
this.flushEncodedInterval = null;
startEncoderDetach(this, cb);
this.refreshManualTimeInterval();
this.grooveEncoder.removeAllListeners();
function logIfError(err) {
if (err) {
console.error("Unable to attach encoder:", err.stack);
}
}
};
Player.prototype.requestUpdateDb = function(dirName, forceRescan, cb) { Player.prototype.requestUpdateDb = function(dirName, forceRescan, cb) {
var fullPath = path.resolve(this.musicDirectory, dirName || ""); var fullPath = path.resolve(this.musicDirectory, dirName || "");
this.dirScanQueue.add(fullPath, { this.dirScanQueue.add(fullPath, {
@ -681,23 +628,73 @@ Player.prototype.getOrCreateDir = function (dirName, stat) {
return dirEntry; return dirEntry;
}; };
Player.prototype.getCurPos = function() { Player.prototype.getCurPos = function() {
return this.isPlaying ? return this.isPlaying ?
((new Date() - this.trackStartDate) / 1000.0) : this.pausedTime; ((new Date() - this.trackStartDate) / 1000.0) : this.pausedTime;
}; };
Player.prototype.secondsIntoFuture = function(groovePlaylistItem, pos) { Player.prototype.setHardwarePlayback = function(value, cb) {
if (!groovePlaylistItem || !pos) { var self = this;
return 0;
cb = cb || logIfError;
value = !!value;
if (value === self.hardwarePlayback) return cb();
self.hardwarePlayback = value;
if (self.hardwarePlayback) {
self.groovePlayer = groove.createPlayer();
self.groovePlayer.attach(self.groovePlaylist, onAttachPlayer);
} else {
self.groovePlayer.detach(onDetachPlayer);
} }
var item = this.grooveItems[groovePlaylistItem.id]; function onAttachPlayer(err) {
if (err) {
self.hardwarePlayback = false;
return cb(err);
}
self.refreshManualTimeInterval();
self.groovePlayer.on('nowplaying', onNowPlaying);
self.persistOption('hardwarePlayback', self.hardwarePlayback);
self.emit('hardwarePlayback', self.hardwarePlayback);
cb();
}
if (item === this.currentTrack) { function onDetachPlayer(err) {
return pos - this.getCurPos(); if (err) {
self.hardwarePlayback = true;
} else { } else {
return pos; self.refreshManualTimeInterval();
self.persistOption('hardwarePlayback', self.hardwarePlayback);
self.emit('hardwarePlayback', self.hardwarePlayback);
}
cb(err);
}
function logIfError(err) {
if (err) {
console.error("Unable to set hardware playback mode:", err.stack);
}
}
function onNowPlaying() {
var playHead = self.getPlayHead();
var decodeHead = self.groovePlaylist.position();
if (playHead.item) {
var nowMs = (new Date()).getTime();
var posMs = playHead.pos * 1000;
self.trackStartDate = new Date(nowMs - posMs);
self.currentTrack = self.grooveItems[playHead.item.id];
playlistChanged(self);
self.emit('currentTrack');
} else if (!decodeHead.item) {
// both play head and decode head are null. end of playlist.
console.log("end of playlist");
self.currentTrack = null;
playlistChanged(self);
self.emit('currentTrack');
}
} }
}; };
@ -717,15 +714,20 @@ Player.prototype.streamMiddleware = function(req, resp, next) {
resp.write(headerBuffer); resp.write(headerBuffer);
}); });
self.recentBuffers.forEach(function(recentBuffer) { self.recentBuffers.forEach(function(recentBuffer) {
resp.write(recentBuffer); resp.write(recentBuffer.buffer);
}); });
console.log("sent", count, "bytes of headers and", self.recentBuffersByteCount, self.attachEncoder();
"bytes of unthrottled data");
self.openStreamers.push(resp); self.openStreamers.push(resp);
req.on('abort', function() { req.on('close', function() {
for (var i = 0; i < self.openStreamers.length; i += 1) { for (var i = 0; i < self.openStreamers.length; i += 1) {
if (self.openStreamers[i] === resp) { if (self.openStreamers[i] === resp) {
self.openStreamers.splice(i, 1); self.openStreamers.splice(i, 1);
if (self.openStreamers.length === 0) {
console.info("last streamer disconnected. detaching encoder");
self.detachEncoder();
} else {
console.info("streamer count:", self.openStreamers.length);
}
break; break;
} }
} }
@ -788,34 +790,19 @@ Player.prototype.importUrl = function(urlString, cb) {
cb = cb || logIfError; cb = cb || logIfError;
var tmpDir = path.join(self.musicDirectory, '.tmp'); var tmpDir = path.join(self.musicDirectory, '.tmp');
var filterIndex = 0;
mkdirp(tmpDir, function(err) { mkdirp(tmpDir, function(err) {
if (err) return cb(err); if (err) return cb(err);
tryImportFilter();
});
function tryImportFilter() {
var importPlugin = self.importUrlFilters[filterIndex];
if (importPlugin) {
importPlugin.importUrl(urlString, callNextFilter);
} else {
downloadRaw();
}
function callNextFilter(err, dlStream, filename) {
if (err || !dlStream) {
if (err) console.warn("import filter error, skipping:", err.stack);
filterIndex += 1;
tryImportFilter();
return;
}
handleDownload(dlStream, filename);
}
}
function downloadRaw() {
var parsedUrl = url.parse(urlString); var parsedUrl = url.parse(urlString);
// detect youtube downloads
if ((parsedUrl.hostname === 'youtube.com' || parsedUrl.hostname === 'www.youtube.com') &&
parsedUrl.pathname === '/watch')
{
var bestFormat = null;
ytdl.getInfo(urlString, gotYouTubeInfo);
} else {
var remoteFilename = path.basename(parsedUrl.pathname); var remoteFilename = path.basename(parsedUrl.pathname);
var decodedFilename; var decodedFilename;
try { try {
@ -827,6 +814,30 @@ Player.prototype.importUrl = function(urlString, cb) {
handleDownload(req, decodedFilename); handleDownload(req, decodedFilename);
} }
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});
handleDownload(req, info.title + '.' + bestFormat.container);
function filter(format) {
return format.audioBitrate === bestFormat.audioBitrate &&
format.audioEncoding === bestFormat.audioEncoding;
}
}
function handleDownload(req, remoteFilename) { function handleDownload(req, remoteFilename) {
var ext = path.extname(remoteFilename); var ext = path.extname(remoteFilename);
var destPath = path.join(tmpDir, uuid() + ext); var destPath = path.join(tmpDir, uuid() + ext);
@ -859,6 +870,7 @@ Player.prototype.importUrl = function(urlString, cb) {
cb(err); cb(err);
} }
} }
});
function logIfError(err) { function logIfError(err) {
if (err) { if (err) {
@ -995,24 +1007,6 @@ Player.prototype.addToLibrary = function(args, cb) {
}); });
}; };
Player.prototype.updateTags = function(obj) {
for (var key in obj) {
var track = this.libraryIndex.trackTable[key];
if (!track) continue;
var props = obj[key];
if (!props || typeof props !== 'object') continue;
for (var propName in DB_PROPS) {
var prop = DB_PROPS[propName];
if (! prop.write) continue;
if (! (propName in props)) continue;
var parser = PROP_TYPE_PARSERS[prop.type];
track[propName] = parser(props[propName]);
}
this.persist(track);
this.emit('updateDbTrack', track);
}
};
Player.prototype.insertTracks = function(index, keys, tagAsRandom) { Player.prototype.insertTracks = function(index, keys, tagAsRandom) {
if (keys.length === 0) return; if (keys.length === 0) return;
if (index < 0) index = 0; if (index < 0) index = 0;
@ -1078,13 +1072,11 @@ Player.prototype.shufflePlaylist = function() {
shuffle(this.tracksInOrder); shuffle(this.tracksInOrder);
// fix sortKey and index properties // fix sortKey and index properties
var nextSortKey = keese(null, null); var nextSortKey = keese(null, null);
for (var i = 0; i < this.tracksInOrder.length; i += 1) { this.tracksInOrder.forEach(function(track, index) {
var track = this.tracksInOrder[i]; track.index = index;
track.index = i;
track.sortKey = nextSortKey; track.sortKey = nextSortKey;
this.persistPlaylistItem(track);
nextSortKey = keese(nextSortKey, null); nextSortKey = keese(nextSortKey, null);
} });
playlistChanged(this); playlistChanged(this);
}; };
@ -1099,7 +1091,10 @@ Player.prototype.removePlaylistItems = function(ids) {
delCmds.push({type: 'del', key: PLAYLIST_KEY_PREFIX + id}); delCmds.push({type: 'del', key: PLAYLIST_KEY_PREFIX + id});
if (item.grooveFile) this.playlistItemDeleteQueue.push(item); if (item.grooveFile) closeFile(item.grooveFile);
// we set this so that any callbacks that return which were trying to
// set the grooveItem can check if the item got deleted
item.deleted = true;
if (item === this.currentTrack) { if (item === this.currentTrack) {
this.currentTrack = null; this.currentTrack = null;
currentTrackChanged = true; currentTrackChanged = true;
@ -1271,7 +1266,6 @@ Player.prototype.clearEncodedBuffer = function() {
while (this.recentBuffers.length > 0) { while (this.recentBuffers.length > 0) {
this.recentBuffers.shift(); this.recentBuffers.shift();
} }
this.recentBuffersByteCount = 0;
}; };
Player.prototype.getSuggestedPath = function(track, filenameHint) { Player.prototype.getSuggestedPath = function(track, filenameHint) {
@ -1500,6 +1494,16 @@ Player.prototype.checkDynamicMode = function() {
} }
}; };
Player.prototype.getPlayHead = function() {
if (this.hardwarePlayback) {
return this.groovePlayer.position();
} else if (this.desiredEncoderAttachState && !this.pendingEncoderAttachDetach) {
return this.grooveEncoder.position();
} else {
return this.groovePlaylist.position();
}
};
function operatorCompare(a, b) { function operatorCompare(a, b) {
return a < b ? -1 : a > b ? 1 : 0; return a < b ? -1 : a > b ? 1 : 0;
} }
@ -1553,12 +1557,29 @@ function lazyReplayGainScanPlaylist(self) {
} }
function playlistChanged(self) { function playlistChanged(self) {
if (self.desiredEncoderAttachState && !self.pendingEncoderAttachDetach && !self.hardwarePlayback) {
var encodeHead = self.grooveEncoder.position();
var decodeHead = self.groovePlaylist.position();
var prevCurrentTrack = self.currentTrack;
if (encodeHead.item) {
var nowMs = (new Date()).getTime();
var posMs = encodeHead.pos * 1000;
self.trackStartDate = new Date(nowMs - posMs);
self.currentTrack = self.grooveItems[encodeHead.item.id];
} else if (!decodeHead.item) {
// both play head and decode head are null. end of playlist.
console.log("encoder: end of playlist");
self.currentTrack = null;
}
if (prevCurrentTrack !== self.currentTrack) {
playlistChanged(self);
self.emit('currentTrack');
}
}
cacheTracksArray(self); cacheTracksArray(self);
disambiguateSortKeys(self); disambiguateSortKeys(self);
self.lastEncodeItem = null;
self.lastEncodePos = null;
if (self.currentTrack) { if (self.currentTrack) {
self.tracksInOrder.forEach(function(track, index) { self.tracksInOrder.forEach(function(track, index) {
var withinPrev = (self.currentTrack.index - index) <= PREV_FILE_COUNT; var withinPrev = (self.currentTrack.index - index) <= PREV_FILE_COUNT;
@ -1577,23 +1598,16 @@ function playlistChanged(self) {
self.pausedTime = 0; self.pausedTime = 0;
} }
checkUpdateGroovePlaylist(self); checkUpdateGroovePlaylist(self);
performGrooveFileDeletes(self); console.log("Begin Groove Playlist:");
self.groovePlaylist.items().forEach(function(item, index) {
console.log(index, item.file.filename);
});
console.log("End Groove Playlist:");
self.checkDynamicMode(); self.checkDynamicMode();
self.emit('playlistUpdate'); self.emit('playlistUpdate');
} }
function performGrooveFileDeletes(self) {
while (self.playlistItemDeleteQueue.length) {
var item = self.playlistItemDeleteQueue.shift();
// we set this so that any callbacks that return which were trying to
// set the grooveItem can check if the item got deleted
item.deleted = true;
closeFile(item.grooveFile);
}
}
function preloadFile(self, track) { function preloadFile(self, track) {
var relPath = self.libraryIndex.trackTable[track.key].file; var relPath = self.libraryIndex.trackTable[track.key].file;
var fullPath = path.join(self.musicDirectory, relPath); var fullPath = path.join(self.musicDirectory, relPath);
@ -1621,7 +1635,7 @@ function checkUpdateGroovePlaylist(self) {
} }
var groovePlaylist = self.groovePlaylist.items(); var groovePlaylist = self.groovePlaylist.items();
var playHead = self.groovePlayer.position(); var playHead = self.getPlayHead();
var playHeadItemId = playHead.item && playHead.item.id; var playHeadItemId = playHead.item && playHead.item.id;
var groovePlIndex = 0; var groovePlIndex = 0;
var grooveItem; var grooveItem;
@ -1749,14 +1763,7 @@ function isFileIgnored(basename) {
} }
function deserializeFileData(dataStr) { function deserializeFileData(dataStr) {
var dbFile = JSON.parse(dataStr); return JSON.parse(dataStr);
for (var propName in DB_PROPS) {
var propInfo = DB_PROPS[propName];
if (!propInfo) continue;
var parser = PROP_TYPE_PARSERS[propInfo.type];
dbFile[propName] = parser(dbFile[propName]);
}
return dbFile;
} }
function serializePlaylistItem(item) { function serializePlaylistItem(item) {
@ -1768,21 +1775,18 @@ function serializePlaylistItem(item) {
}); });
} }
function trackWithoutIndex(category, dbFile) { function trackWithoutIndex(props, dbFile) {
var out = {}; var out = {};
for (var propName in DB_PROPS) { props.forEach(function(propName) {
var prop = DB_PROPS[propName];
if (!prop[category]) continue;
// save space by leaving out null and undefined values
var value = dbFile[propName]; var value = dbFile[propName];
if (value == null) continue; // save space by leaving out null or undefined values
out[propName] = value; if (value != null) out[propName] = value;
} });
return out; return out;
} }
function serializeFileData(dbFile) { function serializeFileData(dbFile) {
return JSON.stringify(trackWithoutIndex('db', dbFile)); return JSON.stringify(trackWithoutIndex(DB_FILE_PROPS, dbFile));
} }
function serializeDirEntry(dirEntry) { function serializeDirEntry(dirEntry) {
@ -1837,11 +1841,10 @@ function parseFloatOrNull(n) {
function grooveFileToDbFile(file, filenameHint, object) { function grooveFileToDbFile(file, filenameHint, object) {
object = object || {key: uuid()}; object = object || {key: uuid()};
var parsedTrack = parseTrackString(file.getMetadata("track")); var parsedTrack = parseTrackString(file.getMetadata("track"));
var parsedDisc = parseTrackString(file.getMetadata("disc") || file.getMetadata("TPA")); var parsedDisc = parseTrackString(file.getMetadata("disc"));
object.name = (file.getMetadata("title") || trackNameFromFile(filenameHint) || "").trim(); object.name = file.getMetadata("title") || trackNameFromFile(filenameHint);
object.artistName = (file.getMetadata("artist") || "").trim(); object.artistName = (file.getMetadata("artist") || "").trim();
object.composerName = (file.getMetadata("composer") || object.composerName = (file.getMetadata("composer") || "").trim();
file.getMetadata("TCM") || "").trim();
object.performerName = (file.getMetadata("performer") || "").trim(); object.performerName = (file.getMetadata("performer") || "").trim();
object.albumArtistName = (file.getMetadata("album_artist") || "").trim(); object.albumArtistName = (file.getMetadata("album_artist") || "").trim();
object.albumName = (file.getMetadata("album") || "").trim(); object.albumName = (file.getMetadata("album") || "").trim();
@ -1852,7 +1855,7 @@ function grooveFileToDbFile(file, filenameHint, object) {
object.disc = parsedDisc.value; object.disc = parsedDisc.value;
object.discCount = parsedDisc.total; object.discCount = parsedDisc.total;
object.duration = file.duration(); object.duration = file.duration();
object.year = parseIntOrNull(file.getMetadata("date")); object.year = parseInt(file.getMetadata("date") || "0", 10);
object.genre = file.getMetadata("genre"); object.genre = file.getMetadata("genre");
object.replayGainTrackGain = parseFloatOrNull(file.getMetadata("REPLAYGAIN_TRACK_GAIN")); object.replayGainTrackGain = parseFloatOrNull(file.getMetadata("REPLAYGAIN_TRACK_GAIN"));
object.replayGainTrackPeak = parseFloatOrNull(file.getMetadata("REPLAYGAIN_TRACK_PEAK")); object.replayGainTrackPeak = parseFloatOrNull(file.getMetadata("REPLAYGAIN_TRACK_PEAK"));

View file

@ -4,6 +4,14 @@ var Player = require('./player');
module.exports = PlayerServer; 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.plugins = [];
PlayerServer.actions = { PlayerServer.actions = {
@ -58,6 +66,13 @@ PlayerServer.actions = {
self.player.setDynamicModeFutureSize(size); self.player.setDynamicModeFutureSize(size);
}, },
}, },
'hardwarePlayback': {
permission: 'admin',
args: 'boolean',
fn: function(self, client, isOn) {
self.player.setHardwarePlayback(isOn);
},
},
'importUrl': { 'importUrl': {
permission: 'control', permission: 'control',
args: 'object', args: 'object',
@ -73,7 +88,10 @@ PlayerServer.actions = {
} else { } else {
key = dbFile.key; key = dbFile.key;
} }
// client might have disconnected by now
try {
client.sendMessage('importUrl', {id: id, key: key}); 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': { 'unsubscribe': {
permission: 'read', permission: 'read',
args: 'string', args: 'string',
@ -201,6 +212,7 @@ PlayerServer.prototype.initialize = function() {
self.player.on('repeatUpdate', addSubscription('repeat', getRepeat)); self.player.on('repeatUpdate', addSubscription('repeat', getRepeat));
self.player.on('volumeUpdate', addSubscription('volume', getVolume)); self.player.on('volumeUpdate', addSubscription('volume', getVolume));
self.player.on('playlistUpdate', addSubscription('playlist', serializePlaylist)); self.player.on('playlistUpdate', addSubscription('playlist', serializePlaylist));
self.player.on('hardwarePlayback', addSubscription('hardwarePlayback', getHardwarePlayback));
var onLibraryUpdate = addSubscription('library', serializeLibrary); var onLibraryUpdate = addSubscription('library', serializeLibrary);
self.player.on('addDbTrack', onLibraryUpdate); self.player.on('addDbTrack', onLibraryUpdate);
@ -253,6 +265,10 @@ PlayerServer.prototype.initialize = function() {
return new Date(); return new Date();
} }
function getHardwarePlayback(client) {
return self.player.hardwarePlayback;
}
function getRepeat(client) { function getRepeat(client) {
return self.player.repeat; return self.player.repeat;
} }
@ -296,7 +312,7 @@ PlayerServer.prototype.initialize = function() {
var table = {}; var table = {};
for (var key in self.player.libraryIndex.trackTable) { for (var key in self.player.libraryIndex.trackTable) {
var track = self.player.libraryIndex.trackTable[key]; var track = self.player.libraryIndex.trackTable[key];
table[key] = Player.trackWithoutIndex('read', track); table[key] = Player.trackWithoutIndex(DB_FILE_PROPS, track);
} }
return table; 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.player = options.player;
this.buffer = ""; this.buffer = "";
this.alreadyClosed = false;
} }
ProtocolParser.prototype._read = function(size) {}; ProtocolParser.prototype._read = function(size) {}
ProtocolParser.prototype._write = function(chunk, encoding, callback) { ProtocolParser.prototype._write = function(chunk, encoding, callback) {
var self = this; var self = this;
@ -45,18 +44,15 @@ ProtocolParser.prototype._write = function(chunk, encoding, callback) {
} }
self.emit('message', jsonObject.name, jsonObject.args); self.emit('message', jsonObject.name, jsonObject.args);
} }
}; }
ProtocolParser.prototype.sendMessage = function(name, args) { ProtocolParser.prototype.sendMessage = function(name, args) {
if (this.alreadyClosed) return;
var jsonObject = {name: name, args: args}; var jsonObject = {name: name, args: args};
this.push(JSON.stringify(jsonObject)); this.push(JSON.stringify(jsonObject));
}; };
ProtocolParser.prototype.close = function() { ProtocolParser.prototype.close = function() {
if (this.alreadyClosed) return;
this.push(null); this.push(null);
this.alreadyClosed = true;
}; };
function extend(o, src) { function extend(o, src) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -244,22 +244,13 @@ body
padding 10px padding 10px
#upload-by-url #upload-by-url
margin: 4px
width: 90% width: 90%
.ui-menu .ui-menu
width: 240px width: 240px
font-size: 1em 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 #shortcuts
h1 h1
margin-bottom 10px margin-bottom 10px
@ -315,6 +306,3 @@ body
font-size .9em font-size .9em
li:before li:before
content "\2713" content "\2713"
.accesskey
text-decoration: underline

View file

@ -28,7 +28,7 @@
<span class="ui-icon ui-icon-volume-on"></span> <span class="ui-icon ui-icon-volume-on"></span>
</div> </div>
<div id="more-playback-btns"> <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> </div>
<h1 id="track-display"></h1> <h1 id="track-display"></h1>
<div id="track-slider"></div> <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"> <input type="file" id="upload-input" multiple="multiple" placeholder="Drag and drop or click to browse">
</div> </div>
<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> </div>
</div> </div>
@ -103,7 +103,7 @@
</p> </p>
<p> <p>
Scrobbling is 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> </p>
</div> </div>
<div id="settings-lastfm-out"> <div id="settings-lastfm-out">
@ -112,6 +112,13 @@
</p> </p>
</div> </div>
</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"> <div class="section">
<h1>About</h1> <h1>About</h1>
<ul> <ul>
@ -126,8 +133,8 @@
<div class="window-header"> <div class="window-header">
<button class="jquery-button clear">Clear</button> <button class="jquery-button clear">Clear</button>
<button class="jquery-button shuffle">Shuffle</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="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="pl-btn-repeat"><label for="pl-btn-repeat">Repeat: Off</label>
</div> </div>
<div id="playlist"> <div id="playlist">
<div class="header"> <div class="header">
@ -148,7 +155,7 @@
<div id="main-err-msg-text">Loading...</div> <div id="main-err-msg-text">Loading...</div>
</p> </p>
</div> </div>
<div id="shortcuts" style="display: none" tabindex="-1"> <div id="shortcuts" style="display: none">
<h1>Playback</h1> <h1>Playback</h1>
<dl> <dl>
<dt>Space</dt> <dt>Space</dt>
@ -289,74 +296,26 @@
<dd>Hold while selecting to select all items in between<dd> <dd>Hold while selecting to select all items in between<dd>
</dl> </dl>
</div> </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"> <ul id="menu-playlist" style="display: none">
<li><a href="#" class="remove">Remove</a></li> <li><a href="#" class="remove">Remove</a></li>
<li><a href="#" class="delete">Delete From Library</a></li> <li>
<li><a href="#" class="download" target="_blank">Download</a></li> <a href="#" class="delete">Delete From Library</a>
<li><a href="#" class="edit-tags">Edit Tags</a></li> </li>
<li>
<a href="#" class="download" target="_blank">Download</a>
</li>
</ul> </ul>
<ul id="menu-library" style="display: none"> <ul id="menu-library" style="display: none">
<li><a href="#" class="queue">Queue</a></li> <li><a href="#" class="queue">Queue</a></li>
<li><a href="#" class="queue-next">Queue Next</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-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="queue-next-random">Queue Next in Random Order</a></li>
<li><a href="#" class="delete">Delete</a></li> <li>
<li><a href="#" class="download" target="_blank">Download</a></li> <a href="#" class="delete">Delete</a>
<li><a href="#" class="edit-tags menu-item-last">Edit Tags</a></li> </li>
<li>
<a href="#" class="download" target="_blank">Download</a>
</li>
</ul> </ul>
<script src="vendor/jquery-2.1.0.min.js"></script> <script src="vendor/jquery-2.1.0.min.js"></script>
<script src="vendor/jquery-ui-1.10.4.custom.min.js"></script> <script src="vendor/jquery-ui-1.10.4.custom.min.js"></script>