Compare commits
7 commits
master
...
no-hardwar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
091a24ee9d | ||
|
|
1631fdd10f | ||
|
|
4e1c926cd5 | ||
|
|
a9a4796922 | ||
|
|
54fd53000b | ||
|
|
259526c82b | ||
|
|
c5f31269d2 |
19 changed files with 670 additions and 1646 deletions
14
README.md
14
README.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Music player server with a web-based user interface inspired by Amarok 1.4.
|
||||
|
||||
Run it on a server (such as a 512MB
|
||||
Run it on a server (such as a
|
||||
[Raspberry Pi](http://www.raspberrypi.org/)) connected to some speakers
|
||||
in your home or office. Guests can control the music player by connecting
|
||||
with a laptop, tablet, or smart phone. Further, you can stream your music
|
||||
|
|
@ -11,7 +11,7 @@ library remotely.
|
|||
Groove Basin works with your personal music library; not an external music
|
||||
service. Groove Basin will never support DRM content.
|
||||
|
||||
Try out the [live demo](http://demo.groovebasin.com/).
|
||||
Live discussion in #libgroove on Freenode.
|
||||
|
||||
## Features
|
||||
|
||||
|
|
@ -46,10 +46,7 @@ Try out the [live demo](http://demo.groovebasin.com/).
|
|||
|
||||
## Install
|
||||
|
||||
1. Install [Node.js](http://nodejs.org) v0.10.x. Note that on Debian and
|
||||
Ubuntu, sadly the official node package is not sufficient. You will either
|
||||
have to use [Chris Lea's PPA](https://launchpad.net/~chris-lea/+archive/node.js/)
|
||||
or compile from source.
|
||||
1. Install [Node.js](http://nodejs.org) v0.10.x.
|
||||
2. Install [libgroove](https://github.com/andrewrk/libgroove).
|
||||
3. Clone the source.
|
||||
4. `npm run build`
|
||||
|
|
@ -76,11 +73,6 @@ $ npm run dev
|
|||
This will install dependencies, build generated files, and then start the
|
||||
sever. It is up to you to restart it when you modify assets or server files.
|
||||
|
||||
### Community
|
||||
|
||||
Pull requests, feature requests, and bug reports are welcome! Live discussion
|
||||
in #libgroove on Freenode.
|
||||
|
||||
### Roadmap
|
||||
|
||||
1. Tag Editing
|
||||
|
|
|
|||
3
docs/.gitignore
vendored
3
docs/.gitignore
vendored
|
|
@ -1,3 +0,0 @@
|
|||
_build
|
||||
_static
|
||||
_templates
|
||||
|
|
@ -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
|
||||
242
docs/conf.py
242
docs/conf.py
|
|
@ -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'
|
||||
|
|
@ -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.
|
||||
|
|
@ -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`
|
||||
|
||||
|
|
@ -43,8 +43,6 @@ var defaultConfig = {
|
|||
lastFmApiSecret: "8713e8e893c5264608e584a232dd10a0",
|
||||
mpdHost: '0.0.0.0',
|
||||
mpdPort: 6600,
|
||||
acoustidAppKey: 'bgFvC4vW',
|
||||
instantBufferBytes: 220 * 1024,
|
||||
};
|
||||
|
||||
defaultConfig.permissions[genPassword()] = {
|
||||
|
|
@ -140,7 +138,7 @@ GrooveBasin.prototype.start = function() {
|
|||
self.initializeDownload();
|
||||
self.initializeUpload();
|
||||
|
||||
self.player = new Player(self.db, self.config.musicDirectory, self.config.instantBufferBytes);
|
||||
self.player = new Player(self.db, self.config.musicDirectory);
|
||||
self.player.initialize(function(err) {
|
||||
if (err) {
|
||||
console.error("unable to initialize player:", err.stack);
|
||||
|
|
|
|||
|
|
@ -1320,7 +1320,7 @@ function writePlaylistInfo(self, start, end) {
|
|||
}
|
||||
|
||||
function forEachMatchingTrack(self, filters, caseSensitive, fn) {
|
||||
// TODO: support 'in' as tag type
|
||||
// TODO: support 'any' and 'in' as tag types
|
||||
var trackTable = self.player.libraryIndex.trackTable;
|
||||
if (!caseSensitive) {
|
||||
filters.forEach(function(filter) {
|
||||
|
|
@ -1329,25 +1329,14 @@ function forEachMatchingTrack(self, filters, caseSensitive, fn) {
|
|||
}
|
||||
for (var key in trackTable) {
|
||||
var track = trackTable[key];
|
||||
var matches = false;
|
||||
var matches = true;
|
||||
for (var filterIndex = 0; filterIndex < filters.length; filterIndex += 1) {
|
||||
var filter = filters[filterIndex];
|
||||
var filterField = String(track[filter.field]);
|
||||
if (!filterField) continue;
|
||||
var filterField = track[filter.field];
|
||||
if (!caseSensitive && filterField) filterField = filterField.toLowerCase();
|
||||
|
||||
/* assumes:
|
||||
* caseSensitive implies "find"
|
||||
* !caseSensitive implies "search"
|
||||
*/
|
||||
if (caseSensitive) {
|
||||
if (filterField === filter.value) {
|
||||
matches = true;
|
||||
break;
|
||||
}
|
||||
} else if (filterField.indexOf(filter.value) > -1) {
|
||||
matches = true;
|
||||
break;
|
||||
if (filterField !== filter.value) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matches) fn(track);
|
||||
|
|
@ -1394,23 +1383,12 @@ function parseFindArgs(self, args, caseSensitive, onTrack, cb, onFinish) {
|
|||
}
|
||||
var filters = [];
|
||||
for (var i = 0; i < args.length; i += 2) {
|
||||
var tagsToSearch = [];
|
||||
if (args[i].toLowerCase() === "any") {
|
||||
// Special case the any key. Just search everything.
|
||||
for (var tagType in tagTypes) {
|
||||
tagsToSearch.push(tagTypes[tagType]);
|
||||
}
|
||||
} else {
|
||||
var tagType = tagTypes[args[i].toLowerCase()];
|
||||
if (!tagType) return cb(ERR_CODE_ARG, "\"" + args[i] + "\" is not known");
|
||||
tagsToSearch.push(tagType);
|
||||
}
|
||||
for (var j = 0; j < tagsToSearch.length; j++) {
|
||||
filters.push({
|
||||
field: tagsToSearch[j].grooveTag,
|
||||
value: args[i+1],
|
||||
});
|
||||
}
|
||||
var tagType = tagTypes[args[i].toLowerCase()];
|
||||
if (!tagType) return cb(ERR_CODE_ARG, "\"" + args[i] + "\" is not known");
|
||||
filters.push({
|
||||
field: tagType.grooveTag,
|
||||
value: args[i+1],
|
||||
});
|
||||
forEachMatchingTrack(self, filters, caseSensitive, onTrack);
|
||||
}
|
||||
onFinish();
|
||||
|
|
|
|||
825
lib/player.js
825
lib/player.js
File diff suppressed because it is too large
Load diff
|
|
@ -4,6 +4,14 @@ var Player = require('./player');
|
|||
|
||||
module.exports = PlayerServer;
|
||||
|
||||
// these are the ones we send to the web, not the ones we store in the DB
|
||||
var DB_FILE_PROPS = [
|
||||
'key', 'name', 'artistName', 'albumArtistName',
|
||||
'albumName', 'compilation', 'track', 'trackCount',
|
||||
'disc', 'discCount', 'duration', 'year', 'genre',
|
||||
'file'
|
||||
];
|
||||
|
||||
PlayerServer.plugins = [];
|
||||
|
||||
PlayerServer.actions = {
|
||||
|
|
@ -58,6 +66,13 @@ PlayerServer.actions = {
|
|||
self.player.setDynamicModeFutureSize(size);
|
||||
},
|
||||
},
|
||||
'hardwarePlayback': {
|
||||
permission: 'admin',
|
||||
args: 'boolean',
|
||||
fn: function(self, client, isOn) {
|
||||
self.player.setHardwarePlayback(isOn);
|
||||
},
|
||||
},
|
||||
'importUrl': {
|
||||
permission: 'control',
|
||||
args: 'object',
|
||||
|
|
@ -73,7 +88,10 @@ PlayerServer.actions = {
|
|||
} else {
|
||||
key = dbFile.key;
|
||||
}
|
||||
client.sendMessage('importUrl', {id: id, key: key});
|
||||
// client might have disconnected by now
|
||||
try {
|
||||
client.sendMessage('importUrl', {id: id, key: key});
|
||||
} catch (err) {}
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
@ -102,13 +120,6 @@ PlayerServer.actions = {
|
|||
}
|
||||
},
|
||||
},
|
||||
'updateTags': {
|
||||
permission: 'admin',
|
||||
args: 'object',
|
||||
fn: function(self, client, obj) {
|
||||
self.player.updateTags(obj);
|
||||
},
|
||||
},
|
||||
'unsubscribe': {
|
||||
permission: 'read',
|
||||
args: 'string',
|
||||
|
|
@ -201,6 +212,7 @@ PlayerServer.prototype.initialize = function() {
|
|||
self.player.on('repeatUpdate', addSubscription('repeat', getRepeat));
|
||||
self.player.on('volumeUpdate', addSubscription('volume', getVolume));
|
||||
self.player.on('playlistUpdate', addSubscription('playlist', serializePlaylist));
|
||||
self.player.on('hardwarePlayback', addSubscription('hardwarePlayback', getHardwarePlayback));
|
||||
|
||||
var onLibraryUpdate = addSubscription('library', serializeLibrary);
|
||||
self.player.on('addDbTrack', onLibraryUpdate);
|
||||
|
|
@ -253,6 +265,10 @@ PlayerServer.prototype.initialize = function() {
|
|||
return new Date();
|
||||
}
|
||||
|
||||
function getHardwarePlayback(client) {
|
||||
return self.player.hardwarePlayback;
|
||||
}
|
||||
|
||||
function getRepeat(client) {
|
||||
return self.player.repeat;
|
||||
}
|
||||
|
|
@ -296,7 +312,7 @@ PlayerServer.prototype.initialize = function() {
|
|||
var table = {};
|
||||
for (var key in self.player.libraryIndex.trackTable) {
|
||||
var track = self.player.libraryIndex.trackTable[key];
|
||||
table[key] = Player.trackWithoutIndex('read', track);
|
||||
table[key] = Player.trackWithoutIndex(DB_FILE_PROPS, track);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -10,10 +10,9 @@ function ProtocolParser(options) {
|
|||
this.player = options.player;
|
||||
|
||||
this.buffer = "";
|
||||
this.alreadyClosed = false;
|
||||
}
|
||||
|
||||
ProtocolParser.prototype._read = function(size) {};
|
||||
ProtocolParser.prototype._read = function(size) {}
|
||||
|
||||
ProtocolParser.prototype._write = function(chunk, encoding, callback) {
|
||||
var self = this;
|
||||
|
|
@ -45,18 +44,15 @@ ProtocolParser.prototype._write = function(chunk, encoding, callback) {
|
|||
}
|
||||
self.emit('message', jsonObject.name, jsonObject.args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ProtocolParser.prototype.sendMessage = function(name, args) {
|
||||
if (this.alreadyClosed) return;
|
||||
var jsonObject = {name: name, args: args};
|
||||
this.push(JSON.stringify(jsonObject));
|
||||
};
|
||||
|
||||
ProtocolParser.prototype.close = function() {
|
||||
if (this.alreadyClosed) return;
|
||||
this.push(null);
|
||||
this.alreadyClosed = true;
|
||||
};
|
||||
|
||||
function extend(o, src) {
|
||||
|
|
|
|||
|
|
@ -11,15 +11,10 @@ function WebSocketApiClient(ws) {
|
|||
}
|
||||
|
||||
WebSocketApiClient.prototype.sendMessage = function(name, args) {
|
||||
try {
|
||||
this.ws.send(JSON.stringify({
|
||||
name: name,
|
||||
args: args,
|
||||
}));
|
||||
} catch (err) {
|
||||
// nothing to do
|
||||
// client might have disconnected by now
|
||||
}
|
||||
this.ws.send(JSON.stringify({
|
||||
name: name,
|
||||
args: args,
|
||||
}));
|
||||
};
|
||||
|
||||
WebSocketApiClient.prototype.close = function() {
|
||||
|
|
|
|||
16
package.json
16
package.json
|
|
@ -18,7 +18,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"lastfm": "~0.9.0",
|
||||
"express": "^4.0.0",
|
||||
"express": "^4.0.0-rc4",
|
||||
"superagent": "^0.17.0",
|
||||
"mkdirp": "~0.3.5",
|
||||
"mv": "~2.0.0",
|
||||
|
|
@ -26,25 +26,25 @@
|
|||
"zfill": "0.0.1",
|
||||
"requireindex": "^1.1.0",
|
||||
"mess": "~0.1.1",
|
||||
"groove": "~1.4.1",
|
||||
"groove": "^1.3.2",
|
||||
"osenv": "0.0.3",
|
||||
"level": "^0.18.0",
|
||||
"level": "~0.18.0",
|
||||
"findit": "~1.1.1",
|
||||
"archiver": "^0.6.1",
|
||||
"uuid": "~1.4.1",
|
||||
"music-library-index": "^1.1.1",
|
||||
"music-library-index": "^1.1.0",
|
||||
"keese": "~1.0.0",
|
||||
"ws": "^0.4.31",
|
||||
"ws": "~0.4.31",
|
||||
"jsondiffpatch": "~0.1.4",
|
||||
"connect-static": "^1.1.0",
|
||||
"multiparty": "^3.2.4",
|
||||
"ytdl": "^0.2.4",
|
||||
"multiparty": "^3.2.3",
|
||||
"ytdl": "^0.2.2",
|
||||
"serve-static": "^1.0.3",
|
||||
"body-parser": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"stylus": "^0.42.3",
|
||||
"browserify": "^3.41.0"
|
||||
"browserify": "~3.32.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node lib/server.js",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ var Socket = require('./socket');
|
|||
var uuid = require('uuid');
|
||||
|
||||
var dynamicModeOn = false;
|
||||
var hardwarePlaybackOn = false;
|
||||
|
||||
var selection = {
|
||||
ids: {
|
||||
|
|
@ -47,13 +48,13 @@ var selection = {
|
|||
this.rangeSelectAnchor = null;
|
||||
this.rangeSelectAnchorType = null;
|
||||
},
|
||||
selectOnly: function(selName, key){
|
||||
selectOnly: function(sel_name, key){
|
||||
this.clear();
|
||||
this.type = selName;
|
||||
this.ids[selName][key] = true;
|
||||
this.type = sel_name;
|
||||
this.ids[sel_name][key] = true;
|
||||
this.cursor = key;
|
||||
this.rangeSelectAnchor = key;
|
||||
this.rangeSelectAnchorType = selName;
|
||||
this.rangeSelectAnchorType = sel_name;
|
||||
},
|
||||
isMulti: function(){
|
||||
var result, k;
|
||||
|
|
@ -198,13 +199,8 @@ var selection = {
|
|||
if (isAlbumExpanded(pos.album)) {
|
||||
pos.track = pos.album.trackList[0];
|
||||
} else {
|
||||
var nextAlbum = pos.artist.albumList[pos.album.index + 1];
|
||||
if (nextAlbum) {
|
||||
pos.album = nextAlbum;
|
||||
} else {
|
||||
pos.artist = player.searchResults.artistList[pos.artist.index + 1];
|
||||
pos.album = null;
|
||||
}
|
||||
pos.artist = player.searchResults.artistList[pos.artist.index + 1];
|
||||
pos.album = null;
|
||||
}
|
||||
} else if (pos.artist != null) {
|
||||
if (isArtistExpanded(pos.artist)) {
|
||||
|
|
@ -357,8 +353,8 @@ var ICON_EXPANDED = 'ui-icon-triangle-1-se';
|
|||
var permissions = {};
|
||||
var socket = null;
|
||||
var player = null;
|
||||
var userIsSeeking = false;
|
||||
var userIsVolumeSliding = false;
|
||||
var user_is_seeking = false;
|
||||
var user_is_volume_sliding = false;
|
||||
var started_drag = false;
|
||||
var abortDrag = function(){};
|
||||
var clickTab = null;
|
||||
|
|
@ -399,11 +395,11 @@ var $tabs = $('#tabs');
|
|||
var $upload_tab = $tabs.find('.upload-tab');
|
||||
var $library = $('#library');
|
||||
var $lib_filter = $('#lib-filter');
|
||||
var $trackSlider = $('#track-slider');
|
||||
var $track_slider = $('#track-slider');
|
||||
var $nowplaying = $('#nowplaying');
|
||||
var $nowplaying_elapsed = $nowplaying.find('.elapsed');
|
||||
var $nowplaying_left = $nowplaying.find('.left');
|
||||
var $volSlider = $('#vol-slider');
|
||||
var $vol_slider = $('#vol-slider');
|
||||
var $settings = $('#settings');
|
||||
var $uploadByUrl = $('#upload-by-url');
|
||||
var $main_err_msg = $('#main-err-msg');
|
||||
|
|
@ -436,9 +432,9 @@ var $settingsLastFmOut = $('#settings-lastfm-out');
|
|||
var settingsLastFmUserDom = document.getElementById('settings-lastfm-user');
|
||||
var $toggleScrobble = $('#toggle-scrobble');
|
||||
var $shortcuts = $('#shortcuts');
|
||||
var $editTagsDialog = $('#edit-tags');
|
||||
var $playlistMenu = $('#menu-playlist');
|
||||
var $libraryMenu = $('#menu-library');
|
||||
var $toggleHardwarePlayback = $('#toggle-hardware-playback');
|
||||
|
||||
function saveLocalState(){
|
||||
localStorage.setItem('state', JSON.stringify(localState));
|
||||
|
|
@ -718,7 +714,6 @@ function refreshSelection() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getValidIds(selection_type) {
|
||||
switch (selection_type) {
|
||||
case 'playlist': return player.playlist.itemTable;
|
||||
|
|
@ -824,7 +819,7 @@ function getCurrentTrackPosition(){
|
|||
}
|
||||
|
||||
function updateSliderPos() {
|
||||
if (userIsSeeking) return;
|
||||
if (user_is_seeking) return;
|
||||
|
||||
var duration, disabled, elapsed, sliderPos;
|
||||
if (player.currentItem && player.isPlaying != null && player.currentItem.track) {
|
||||
|
|
@ -836,19 +831,19 @@ function updateSliderPos() {
|
|||
disabled = true;
|
||||
elapsed = duration = sliderPos = 0;
|
||||
}
|
||||
$trackSlider.slider("option", "disabled", disabled).slider("option", "value", sliderPos);
|
||||
$track_slider.slider("option", "disabled", disabled).slider("option", "value", sliderPos);
|
||||
$nowplaying_elapsed.html(formatTime(elapsed));
|
||||
$nowplaying_left.html(formatTime(duration));
|
||||
}
|
||||
|
||||
function renderVolumeSlider() {
|
||||
if (userIsVolumeSliding) return;
|
||||
if (user_is_volume_sliding) return;
|
||||
|
||||
var enabled = player.volume != null;
|
||||
if (enabled) {
|
||||
$volSlider.slider('option', 'value', player.volume);
|
||||
$vol_slider.slider('option', 'value', player.volume);
|
||||
}
|
||||
$volSlider.slider('option', 'disabled', !enabled);
|
||||
$vol_slider.slider('option', 'disabled', !enabled);
|
||||
}
|
||||
|
||||
function renderNowPlaying(){
|
||||
|
|
@ -888,7 +883,7 @@ function renderNowPlaying(){
|
|||
new_class = 'ui-icon-play';
|
||||
}
|
||||
$nowplaying.find(".toggle span").removeClass(old_class).addClass(new_class);
|
||||
$trackSlider.slider("option", "disabled", player.isPlaying == null);
|
||||
$track_slider.slider("option", "disabled", player.isPlaying == null);
|
||||
updateSliderPos();
|
||||
renderVolumeSlider();
|
||||
}
|
||||
|
|
@ -1053,34 +1048,29 @@ function nextRepeatState(){
|
|||
player.setRepeatMode((player.repeat + 1) % repeatModeNames.length);
|
||||
}
|
||||
|
||||
var keyboardHandlers = (function(){
|
||||
var keyboard_handlers = (function(){
|
||||
function upDownHandler(event){
|
||||
var defaultIndex, dir, nextPos;
|
||||
var default_index, dir, next_pos;
|
||||
if (event.which === 38) {
|
||||
// up
|
||||
defaultIndex = player.currentItem ? player.currentItem.index - 1 : player.playlist.itemList.length - 1;
|
||||
default_index = player.playlist.itemList.length - 1;
|
||||
dir = -1;
|
||||
} else {
|
||||
// down
|
||||
defaultIndex = player.currentItem ? player.currentItem.index + 1 : 0;
|
||||
default_index = 0;
|
||||
dir = 1;
|
||||
}
|
||||
if (defaultIndex >= player.playlist.itemList.length) {
|
||||
defaultIndex = player.playlist.itemList.length - 1;
|
||||
} else if (defaultIndex < 0) {
|
||||
defaultIndex = 0;
|
||||
}
|
||||
if (event.altKey) {
|
||||
if (selection.isPlaylist()) {
|
||||
player.shiftIds(selection.ids.playlist, dir);
|
||||
}
|
||||
} else {
|
||||
if (selection.isPlaylist()) {
|
||||
nextPos = player.playlist.itemTable[selection.cursor].index + dir;
|
||||
if (nextPos < 0 || nextPos >= player.playlist.itemList.length) {
|
||||
next_pos = player.playlist.itemTable[selection.cursor].index + dir;
|
||||
if (next_pos < 0 || next_pos >= player.playlist.itemList.length) {
|
||||
return;
|
||||
}
|
||||
selection.cursor = player.playlist.itemList[nextPos].id;
|
||||
selection.cursor = player.playlist.itemList[next_pos].id;
|
||||
if (!event.ctrlKey && !event.shiftKey) {
|
||||
// single select
|
||||
selection.clear();
|
||||
|
|
@ -1096,22 +1086,24 @@ var keyboardHandlers = (function(){
|
|||
selection.rangeSelectAnchorType = selection.type;
|
||||
}
|
||||
} else if (selection.isLibrary()) {
|
||||
nextPos = selection.getPos();
|
||||
next_pos = selection.getPos();
|
||||
if (dir > 0) {
|
||||
selection.incrementPos(nextPos);
|
||||
selection.incrementPos(next_pos);
|
||||
} else {
|
||||
prevLibPos(nextPos);
|
||||
prevLibPos(next_pos);
|
||||
}
|
||||
if (nextPos.artist == null) return;
|
||||
if (nextPos.track != null) {
|
||||
if (next_pos.artist == null) {
|
||||
return;
|
||||
}
|
||||
if (next_pos.track != null) {
|
||||
selection.type = 'track';
|
||||
selection.cursor = nextPos.track.key;
|
||||
} else if (nextPos.album != null) {
|
||||
selection.cursor = next_pos.track.key;
|
||||
} else if (next_pos.album != null) {
|
||||
selection.type = 'album';
|
||||
selection.cursor = nextPos.album.key;
|
||||
selection.cursor = next_pos.album.key;
|
||||
} else {
|
||||
selection.type = 'artist';
|
||||
selection.cursor = nextPos.artist.key;
|
||||
selection.cursor = next_pos.artist.key;
|
||||
}
|
||||
if (!event.ctrlKey && !event.shiftKey) {
|
||||
// single select
|
||||
|
|
@ -1126,12 +1118,16 @@ var keyboardHandlers = (function(){
|
|||
}
|
||||
} else {
|
||||
if (player.playlist.itemList.length === 0) return;
|
||||
selection.selectOnly('playlist', player.playlist.itemList[defaultIndex].id);
|
||||
selection.selectOnly('playlist', player.playlist.itemList[default_index].id);
|
||||
}
|
||||
refreshSelection();
|
||||
}
|
||||
if (selection.isPlaylist()) scrollPlaylistToSelection();
|
||||
if (selection.isLibrary()) scrollLibraryToSelection();
|
||||
if (selection.isPlaylist()) {
|
||||
scrollPlaylistToSelection();
|
||||
}
|
||||
if (selection.isLibrary()) {
|
||||
scrollLibraryToSelection();
|
||||
}
|
||||
}
|
||||
function leftRightHandler(event){
|
||||
var dir = event.which === 37 ? -1 : 1;
|
||||
|
|
@ -1366,8 +1362,10 @@ var keyboardHandlers = (function(){
|
|||
title: "Keyboard Shortcuts",
|
||||
minWidth: 600,
|
||||
height: $document.height() - 40,
|
||||
close: function(){
|
||||
$('#shortcuts').remove();
|
||||
}
|
||||
});
|
||||
$shortcuts.focus();
|
||||
} else {
|
||||
clickTab('library');
|
||||
$lib_filter.focus().select();
|
||||
|
|
@ -1398,28 +1396,29 @@ function isArtistExpanded(artist){
|
|||
return $li.find("> ul").is(":visible");
|
||||
}
|
||||
function isAlbumExpanded(album){
|
||||
var $li = $("#lib-album-" + toHtmlId(album.key)).closest("li");
|
||||
var $li;
|
||||
$li = $("#lib-album-" + toHtmlId(album.key)).closest("li");
|
||||
return $li.find("> ul").is(":visible");
|
||||
}
|
||||
function isStoredPlaylistExpanded(stored_playlist){
|
||||
var $li = $("#stored-pl-pl-" + toHtmlId(stored_playlist.name)).closest("li");
|
||||
var $li;
|
||||
$li = $("#stored-pl-pl-" + toHtmlId(stored_playlist.name)).closest("li");
|
||||
return $li.find("> ul").is(":visible");
|
||||
}
|
||||
|
||||
function prevLibPos(libPos){
|
||||
if (libPos.track != null) {
|
||||
libPos.track = libPos.track.album.trackList[libPos.track.index - 1];
|
||||
} else if (libPos.album != null) {
|
||||
libPos.album = libPos.artist.albumList[libPos.album.index - 1];
|
||||
if (libPos.album != null && isAlbumExpanded(libPos.album)) {
|
||||
libPos.track = libPos.album.trackList[libPos.album.trackList.length - 1];
|
||||
function prevLibPos(lib_pos){
|
||||
if (lib_pos.track != null) {
|
||||
lib_pos.track = lib_pos.track.album.trackList[lib_pos.track.index - 1];
|
||||
} else if (lib_pos.album != null) {
|
||||
lib_pos.album = lib_pos.artist.albumList[lib_pos.album.index - 1];
|
||||
if (lib_pos.album != null && isAlbumExpanded(lib_pos.album)) {
|
||||
lib_pos.track = lib_pos.album.trackList[lib_pos.album.trackList.length - 1];
|
||||
}
|
||||
} else if (libPos.artist != null) {
|
||||
libPos.artist = player.searchResults.artistList[libPos.artist.index - 1];
|
||||
if (libPos.artist != null && isArtistExpanded(libPos.artist)) {
|
||||
libPos.album = libPos.artist.albumList[libPos.artist.albumList.length - 1];
|
||||
if (libPos.album != null && isAlbumExpanded(libPos.album)) {
|
||||
libPos.track = libPos.album.trackList[libPos.album.trackList.length - 1];
|
||||
} else if (lib_pos.artist != null) {
|
||||
lib_pos.artist = player.searchResults.artistList[lib_pos.artist.index - 1];
|
||||
if (lib_pos.artist != null && isArtistExpanded(lib_pos.artist)) {
|
||||
lib_pos.album = lib_pos.artist.albumList[lib_pos.artist.albumList.length - 1];
|
||||
if (lib_pos.album != null && isAlbumExpanded(lib_pos.album)) {
|
||||
lib_pos.track = lib_pos.album.trackList[lib_pos.album.trackList.length - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1549,14 +1548,14 @@ function setUpGenericUi(){
|
|||
$document.on('mouseout', '.hoverable', function(event){
|
||||
$(this).removeClass("ui-state-hover");
|
||||
});
|
||||
$(".jquery-button").button().on('click', blur);
|
||||
$(".jquery-button").button();
|
||||
$document.on('mousedown', function(){
|
||||
removeContextMenu();
|
||||
selection.fullClear();
|
||||
selection.type = null;
|
||||
refreshSelection();
|
||||
});
|
||||
$document.on('keydown', function(event){
|
||||
var handler = keyboardHandlers[event.which];
|
||||
var handler = keyboard_handlers[event.which];
|
||||
if (handler == null) return true;
|
||||
if (handler.ctrl != null && handler.ctrl !== event.ctrlKey) return true;
|
||||
if (handler.alt != null && handler.alt !== event.altKey) return true;
|
||||
|
|
@ -1564,41 +1563,23 @@ function setUpGenericUi(){
|
|||
handler.handler(event);
|
||||
return false;
|
||||
});
|
||||
$shortcuts.on('keydown', function(event) {
|
||||
event.stopPropagation();
|
||||
if (event.which === 27) {
|
||||
$shortcuts.dialog('close');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function blur() {
|
||||
$(this).blur();
|
||||
}
|
||||
|
||||
var dynamicModeLabel = document.getElementById('dynamic-mode-label');
|
||||
var plBtnRepeatLabel = document.getElementById('pl-btn-repeat-label');
|
||||
function setUpPlaylistUi(){
|
||||
$pl_window.on('click', 'button.clear', function(event){
|
||||
$pl_window.on('click', 'button.clear', function(){
|
||||
player.clear();
|
||||
});
|
||||
$pl_window.on('mousedown', 'button.clear', stopPropagation);
|
||||
|
||||
$pl_window.on('click', 'button.shuffle', function(){
|
||||
player.shuffle();
|
||||
});
|
||||
$pl_window.on('mousedown', 'button.shuffle', stopPropagation);
|
||||
|
||||
$pl_btn_repeat.on('click', nextRepeatState);
|
||||
plBtnRepeatLabel.addEventListener('mousedown', stopPropagation, false);
|
||||
|
||||
$pl_btn_repeat.on('click', function(){
|
||||
nextRepeatState();
|
||||
});
|
||||
$dynamicMode.on('click', function(){
|
||||
var value = $(this).prop("checked");
|
||||
setDynamicMode(value);
|
||||
return false;
|
||||
});
|
||||
dynamicModeLabel.addEventListener('mousedown', stopPropagation, false);
|
||||
|
||||
$playlistItems.on('dblclick', '.pl-item', function(event){
|
||||
var trackId = $(this).attr('data-id');
|
||||
player.seek(trackId, 0);
|
||||
|
|
@ -1607,18 +1588,20 @@ function setUpPlaylistUi(){
|
|||
return event.altKey;
|
||||
});
|
||||
$playlistItems.on('mousedown', '.pl-item', function(event){
|
||||
var trackId, skipDrag;
|
||||
if (started_drag) return true;
|
||||
var trackId, skip_drag;
|
||||
if (started_drag) {
|
||||
return true;
|
||||
}
|
||||
$(document.activeElement).blur();
|
||||
if (event.which === 1) {
|
||||
event.preventDefault();
|
||||
removeContextMenu();
|
||||
trackId = $(this).attr('data-id');
|
||||
skipDrag = false;
|
||||
skip_drag = false;
|
||||
if (!selection.isPlaylist()) {
|
||||
selection.selectOnly('playlist', trackId);
|
||||
} else if (event.ctrlKey || event.shiftKey) {
|
||||
skipDrag = true;
|
||||
skip_drag = true;
|
||||
if (event.shiftKey && !event.ctrlKey) {
|
||||
// range select click
|
||||
selection.cursor = trackId;
|
||||
|
|
@ -1634,7 +1617,7 @@ function setUpPlaylistUi(){
|
|||
selection.selectOnly('playlist', trackId);
|
||||
}
|
||||
refreshSelection();
|
||||
if (!skipDrag) {
|
||||
if (!skip_drag) {
|
||||
return performDrag(event, {
|
||||
complete: function(result, event){
|
||||
var delta, id;
|
||||
|
|
@ -1657,7 +1640,9 @@ function setUpPlaylistUi(){
|
|||
});
|
||||
}
|
||||
} else if (event.which === 3) {
|
||||
if (event.altKey) return;
|
||||
if (event.altKey) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
removeContextMenu();
|
||||
trackId = $(this).attr('data-id');
|
||||
|
|
@ -1675,7 +1660,15 @@ function setUpPlaylistUi(){
|
|||
left: event.pageX + 1,
|
||||
top: event.pageY + 1
|
||||
});
|
||||
updateAdminActions($playlistMenu);
|
||||
if (!permissions.admin) {
|
||||
$playlistMenu.find('.delete')
|
||||
.addClass('ui-state-disabled')
|
||||
.attr('title', "Insufficient privileges. See Settings.");
|
||||
} else {
|
||||
$playlistMenu.find('.delete')
|
||||
.removeClass('ui-state-disabled')
|
||||
.attr('title', '');
|
||||
}
|
||||
}
|
||||
});
|
||||
$playlistItems.on('mousedown', function(){
|
||||
|
|
@ -1690,266 +1683,33 @@ function setUpPlaylistUi(){
|
|||
removeContextMenu();
|
||||
return false;
|
||||
});
|
||||
$playlistMenu.on('click', '.download', onDownloadContextMenu);
|
||||
$playlistMenu.on('click', '.delete', onDeleteContextMenu);
|
||||
$playlistMenu.on('click', '.edit-tags', onEditTagsContextMenu);
|
||||
}
|
||||
$playlistMenu.on('click', '.download', function(){
|
||||
removeContextMenu();
|
||||
|
||||
function stopPropagation(event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
if (selection.isMulti()) {
|
||||
downloadKeys(selection.toTrackKeys());
|
||||
return false;
|
||||
}
|
||||
|
||||
function onDownloadContextMenu() {
|
||||
removeContextMenu();
|
||||
|
||||
if (selection.isMulti()) {
|
||||
downloadKeys(selection.toTrackKeys());
|
||||
return true;
|
||||
});
|
||||
$playlistMenu.on('click', '.delete', function(){
|
||||
if (!permissions.admin) return false;
|
||||
handleDeletePressed(true);
|
||||
removeContextMenu();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
function onDeleteContextMenu() {
|
||||
if (!permissions.admin) return false;
|
||||
removeContextMenu();
|
||||
handleDeletePressed(true);
|
||||
return false;
|
||||
}
|
||||
var editTagsTrackKeys = null;
|
||||
var editTagsTrackIndex = null;
|
||||
function onEditTagsContextMenu() {
|
||||
if (!permissions.admin) return false;
|
||||
removeContextMenu();
|
||||
editTagsTrackKeys = selection.toTrackKeys();
|
||||
editTagsTrackIndex = 0;
|
||||
showEditTags();
|
||||
return false;
|
||||
}
|
||||
var EDITABLE_PROPS = {
|
||||
name: {
|
||||
type: 'string',
|
||||
write: true,
|
||||
},
|
||||
artistName: {
|
||||
type: 'string',
|
||||
write: true,
|
||||
},
|
||||
albumArtistName: {
|
||||
type: 'string',
|
||||
write: true,
|
||||
},
|
||||
albumName: {
|
||||
type: 'string',
|
||||
write: true,
|
||||
},
|
||||
compilation: {
|
||||
type: 'boolean',
|
||||
write: true,
|
||||
},
|
||||
track: {
|
||||
type: 'integer',
|
||||
write: true,
|
||||
},
|
||||
trackCount: {
|
||||
type: 'integer',
|
||||
write: true,
|
||||
},
|
||||
disc: {
|
||||
type: 'integer',
|
||||
write: true,
|
||||
},
|
||||
discCount: {
|
||||
type: 'integer',
|
||||
write: true,
|
||||
},
|
||||
year: {
|
||||
type: 'integer',
|
||||
write: true,
|
||||
},
|
||||
genre: {
|
||||
type: 'string',
|
||||
write: true,
|
||||
},
|
||||
composerName: {
|
||||
type: 'string',
|
||||
write: true,
|
||||
},
|
||||
performerName: {
|
||||
type: 'string',
|
||||
write: true,
|
||||
},
|
||||
file: {
|
||||
type: 'string',
|
||||
write: false,
|
||||
},
|
||||
};
|
||||
var EDIT_TAG_TYPES = {
|
||||
'string': {
|
||||
get: function(domItem) {
|
||||
return domItem.value;
|
||||
},
|
||||
set: function(domItem, value) {
|
||||
domItem.value = value || "";
|
||||
},
|
||||
},
|
||||
'integer': {
|
||||
get: function(domItem) {
|
||||
var n = parseInt(domItem.value, 10);
|
||||
if (isNaN(n)) return null;
|
||||
return n;
|
||||
},
|
||||
set: function(domItem, value) {
|
||||
domItem.value = value == null ? "" : value;
|
||||
},
|
||||
},
|
||||
'boolean': {
|
||||
get: function(domItem) {
|
||||
return domItem.checked;
|
||||
},
|
||||
set: function(domItem, value) {
|
||||
domItem.checked = !!value;
|
||||
},
|
||||
},
|
||||
};
|
||||
var perDom = document.getElementById('edit-tags-per');
|
||||
var perLabelDom = document.getElementById('edit-tags-per-label');
|
||||
var prevDom = document.getElementById('edit-tags-prev');
|
||||
var nextDom = document.getElementById('edit-tags-next');
|
||||
var editTagsFocusDom = document.getElementById('edit-tag-name');
|
||||
function updateEditTagsUi() {
|
||||
var multiple = editTagsTrackKeys.length > 1;
|
||||
prevDom.disabled = !perDom.checked || editTagsTrackIndex === 0;
|
||||
nextDom.disabled = !perDom.checked || (editTagsTrackIndex === editTagsTrackKeys.length - 1);
|
||||
prevDom.style.visibility = multiple ? 'visible' : 'hidden';
|
||||
nextDom.style.visibility = multiple ? 'visible' : 'hidden';
|
||||
perLabelDom.style.visibility = multiple ? 'visible' : 'hidden';
|
||||
var multiCheckBoxVisible = multiple && !perDom.checked;
|
||||
var trackKeysToUse = perDom.checked ? [editTagsTrackKeys[editTagsTrackIndex]] : editTagsTrackKeys;
|
||||
|
||||
for (var propName in EDITABLE_PROPS) {
|
||||
var propInfo = EDITABLE_PROPS[propName];
|
||||
var type = propInfo.type;
|
||||
var setter = EDIT_TAG_TYPES[type].set;
|
||||
var domItem = document.getElementById('edit-tag-' + propName);
|
||||
domItem.disabled = !propInfo.write;
|
||||
var multiCheckBoxDom = document.getElementById('edit-tag-multi-' + propName);
|
||||
multiCheckBoxDom.style.visibility = (multiCheckBoxVisible && propInfo.write) ? 'visible' : 'hidden';
|
||||
var commonValue = null;
|
||||
var consistent = true;
|
||||
for (var i = 0; i < trackKeysToUse.length; i += 1) {
|
||||
var key = trackKeysToUse[i];
|
||||
var track = player.library.trackTable[key];
|
||||
var value = track[propName];
|
||||
if (commonValue == null) {
|
||||
commonValue = value;
|
||||
} else if (commonValue !== value) {
|
||||
consistent = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
multiCheckBoxDom.checked = consistent;
|
||||
setter(domItem, consistent ? commonValue : null);
|
||||
}
|
||||
}
|
||||
function showEditTags() {
|
||||
$editTagsDialog.dialog({
|
||||
modal: true,
|
||||
title: "Edit Tags",
|
||||
minWidth: 800,
|
||||
height: $document.height() - 40,
|
||||
});
|
||||
perDom.checked = false;
|
||||
updateEditTagsUi();
|
||||
editTagsFocusDom.focus();
|
||||
}
|
||||
|
||||
function setUpEditTagsUi() {
|
||||
$editTagsDialog.find("input").on("keydown", function(event) {
|
||||
event.stopPropagation();
|
||||
if (event.which === 27) {
|
||||
$editTagsDialog.dialog('close');
|
||||
} else if (event.which === 13) {
|
||||
saveAndClose();
|
||||
}
|
||||
});
|
||||
for (var propName in EDITABLE_PROPS) {
|
||||
var domItem = document.getElementById('edit-tag-' + propName);
|
||||
var multiCheckBoxDom = document.getElementById('edit-tag-multi-' + propName);
|
||||
var listener = createChangeListener(multiCheckBoxDom);
|
||||
domItem.addEventListener('change', listener, false);
|
||||
domItem.addEventListener('keypress', listener, false);
|
||||
domItem.addEventListener('focus', onFocus, false);
|
||||
}
|
||||
|
||||
function onFocus(event) {
|
||||
editTagsFocusDom = event.target;
|
||||
}
|
||||
|
||||
function createChangeListener(multiCheckBoxDom) {
|
||||
return function() {
|
||||
multiCheckBoxDom.checked = true;
|
||||
};
|
||||
}
|
||||
$("#edit-tags-ok").on('click', saveAndClose);
|
||||
$("#edit-tags-cancel").on('click', closeDialog);
|
||||
perDom.addEventListener('click', updateEditTagsUi, false);
|
||||
nextDom.addEventListener('click', saveAndNext, false);
|
||||
prevDom.addEventListener('click', saveAndPrev, false);
|
||||
|
||||
function saveAndMoveOn(dir) {
|
||||
save();
|
||||
editTagsTrackIndex += dir;
|
||||
updateEditTagsUi();
|
||||
editTagsFocusDom.focus();
|
||||
editTagsFocusDom.select();
|
||||
}
|
||||
|
||||
function saveAndNext() {
|
||||
saveAndMoveOn(1);
|
||||
}
|
||||
|
||||
function saveAndPrev() {
|
||||
saveAndMoveOn(-1);
|
||||
}
|
||||
|
||||
function save() {
|
||||
var trackKeysToUse = perDom.checked ? [editTagsTrackKeys[editTagsTrackIndex]] : editTagsTrackKeys;
|
||||
var cmd = {};
|
||||
for (var i = 0; i < trackKeysToUse.length; i += 1) {
|
||||
var key = trackKeysToUse[i];
|
||||
var track = player.library.trackTable[key];
|
||||
var props = cmd[track.key] = {};
|
||||
for (var propName in EDITABLE_PROPS) {
|
||||
var propInfo = EDITABLE_PROPS[propName];
|
||||
var type = propInfo.type;
|
||||
var getter = EDIT_TAG_TYPES[type].get;
|
||||
var domItem = document.getElementById('edit-tag-' + propName);
|
||||
var multiCheckBoxDom = document.getElementById('edit-tag-multi-' + propName);
|
||||
if (multiCheckBoxDom.checked && propInfo.write) {
|
||||
props[propName] = getter(domItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
player.sendCommand('updateTags', cmd);
|
||||
}
|
||||
|
||||
function saveAndClose() {
|
||||
save();
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
$editTagsDialog.dialog('close');
|
||||
}
|
||||
}
|
||||
|
||||
function updateSliderUi(value){
|
||||
var percent = value * 100;
|
||||
$trackSlider.css('background-size', percent + "% 100%");
|
||||
$track_slider.css('background-size', percent + "% 100%");
|
||||
}
|
||||
|
||||
function setUpNowPlayingUi(){
|
||||
var actions = {
|
||||
var actions, cls, action;
|
||||
actions = {
|
||||
toggle: togglePlayback,
|
||||
prev: function(){
|
||||
player.prev();
|
||||
|
|
@ -1961,11 +1721,11 @@ function setUpNowPlayingUi(){
|
|||
player.stop();
|
||||
}
|
||||
};
|
||||
for (var cls in actions) {
|
||||
var action = actions[cls];
|
||||
setUpMouseDownListener(cls, action);
|
||||
for (cls in actions) {
|
||||
action = actions[cls];
|
||||
(fn$.call(this, cls, action));
|
||||
}
|
||||
$trackSlider.slider({
|
||||
$track_slider.slider({
|
||||
step: 0.0001,
|
||||
min: 0,
|
||||
max: 1,
|
||||
|
|
@ -1983,30 +1743,32 @@ function setUpNowPlayingUi(){
|
|||
$nowplaying_elapsed.html(formatTime(ui.value * player.currentItem.track.duration));
|
||||
},
|
||||
start: function(event, ui){
|
||||
userIsSeeking = true;
|
||||
user_is_seeking = true;
|
||||
},
|
||||
stop: function(event, ui){
|
||||
userIsSeeking = false;
|
||||
user_is_seeking = false;
|
||||
}
|
||||
});
|
||||
function setVol(event, ui){
|
||||
if (event.originalEvent == null) return;
|
||||
if (event.originalEvent == null) {
|
||||
return;
|
||||
}
|
||||
player.setVolume(ui.value);
|
||||
}
|
||||
$volSlider.slider({
|
||||
$vol_slider.slider({
|
||||
step: 0.01,
|
||||
min: 0,
|
||||
max: 1,
|
||||
change: setVol,
|
||||
start: function(event, ui){
|
||||
userIsVolumeSliding = true;
|
||||
user_is_volume_sliding = true;
|
||||
},
|
||||
stop: function(event, ui){
|
||||
userIsVolumeSliding = false;
|
||||
user_is_volume_sliding = false;
|
||||
}
|
||||
});
|
||||
setInterval(updateSliderPos, 100);
|
||||
function setUpMouseDownListener(cls, action){
|
||||
function fn$(cls, action){
|
||||
$nowplaying.on('mousedown', "li." + cls, function(event){
|
||||
action();
|
||||
return false;
|
||||
|
|
@ -2200,8 +1962,16 @@ function updateSettingsAuthUi() {
|
|||
streamUrlDom.setAttribute('href', streaming.getUrl());
|
||||
}
|
||||
|
||||
function updateSettingsAdminUi() {
|
||||
$toggleHardwarePlayback
|
||||
.button('option', 'label', hardwarePlaybackOn ? 'On' : 'Off')
|
||||
.prop('checked', hardwarePlaybackOn)
|
||||
.button('refresh');
|
||||
}
|
||||
|
||||
function setUpSettingsUi(){
|
||||
$toggleScrobble.button();
|
||||
$toggleHardwarePlayback.button();
|
||||
$lastFmSignOut.button();
|
||||
$settingsAuthCancel.button();
|
||||
$settingsAuthSave.button();
|
||||
|
|
@ -2234,6 +2004,11 @@ function setUpSettingsUi(){
|
|||
socket.send(msg, params);
|
||||
updateLastFmSettingsUi();
|
||||
});
|
||||
$toggleHardwarePlayback.on('click', function(event) {
|
||||
var value = $(this).prop('checked');
|
||||
socket.send('hardwarePlayback', value);
|
||||
updateSettingsAdminUi();
|
||||
});
|
||||
$settingsAuthEdit.on('click', function(event) {
|
||||
settings_ui.auth.show_edit = true;
|
||||
updateSettingsAuthUi();
|
||||
|
|
@ -2352,9 +2127,22 @@ function setUpLibraryUi(){
|
|||
removeContextMenu();
|
||||
return false;
|
||||
});
|
||||
$libraryMenu.on('click', '.download', onDownloadContextMenu);
|
||||
$libraryMenu.on('click', '.delete', onDeleteContextMenu);
|
||||
$libraryMenu.on('click', '.edit-tags', onEditTagsContextMenu);
|
||||
$libraryMenu.on('click', '.download', function(){
|
||||
removeContextMenu();
|
||||
|
||||
if (selection.isMulti()) {
|
||||
downloadKeys(selection.toTrackKeys());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
$libraryMenu.on('click', '.delete', function(){
|
||||
if (!permissions.admin) return false;
|
||||
handleDeletePressed(true);
|
||||
removeContextMenu();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function genericTreeUi($elem, options){
|
||||
|
|
@ -2385,11 +2173,11 @@ function genericTreeUi($elem, options){
|
|||
function leftMouseDown(event){
|
||||
event.preventDefault();
|
||||
removeContextMenu();
|
||||
var skipDrag = false;
|
||||
var skip_drag = false;
|
||||
if (!options.isSelectionOwner()) {
|
||||
selection.selectOnly(type, key);
|
||||
} else if (event.ctrlKey || event.shiftKey) {
|
||||
skipDrag = true;
|
||||
skip_drag = true;
|
||||
selection.cursor = key;
|
||||
selection.type = type;
|
||||
if (!event.shiftKey && !event.ctrlKey) {
|
||||
|
|
@ -2403,7 +2191,7 @@ function genericTreeUi($elem, options){
|
|||
selection.selectOnly(type, key);
|
||||
}
|
||||
refreshSelection();
|
||||
if (!skipDrag) {
|
||||
if (!skip_drag) {
|
||||
performDrag(event, {
|
||||
complete: function(result, event){
|
||||
var delta = {
|
||||
|
|
@ -2444,24 +2232,21 @@ function genericTreeUi($elem, options){
|
|||
left: event.pageX + 1,
|
||||
top: event.pageY + 1
|
||||
});
|
||||
updateAdminActions($libraryMenu);
|
||||
if (!permissions.admin) {
|
||||
$libraryMenu.find('.delete')
|
||||
.addClass('ui-state-disabled')
|
||||
.attr('title', "Insufficient privileges. See Settings.");
|
||||
} else {
|
||||
$libraryMenu.find('.delete')
|
||||
.removeClass('ui-state-disabled')
|
||||
.attr('title', '');
|
||||
}
|
||||
}
|
||||
});
|
||||
$elem.on('mousedown', function(){
|
||||
return false;
|
||||
});
|
||||
}
|
||||
function updateAdminActions($menu) {
|
||||
if (!permissions.admin) {
|
||||
$menu.find('.delete,.edit-tags')
|
||||
.addClass('ui-state-disabled')
|
||||
.attr('title', "Insufficient privileges. See Settings.");
|
||||
} else {
|
||||
$menu.find('.delete,.edit-tags')
|
||||
.removeClass('ui-state-disabled')
|
||||
.attr('title', '');
|
||||
}
|
||||
}
|
||||
function setUpUi(){
|
||||
setUpGenericUi();
|
||||
setUpPlaylistUi();
|
||||
|
|
@ -2470,7 +2255,6 @@ function setUpUi(){
|
|||
setUpTabsUi();
|
||||
setUpUploadUi();
|
||||
setUpSettingsUi();
|
||||
setUpEditTagsUi();
|
||||
}
|
||||
|
||||
function toAlbumId(s) {
|
||||
|
|
@ -2531,6 +2315,10 @@ $document.ready(function(){
|
|||
});
|
||||
return;
|
||||
}
|
||||
socket.on('hardwarePlayback', function(isOn) {
|
||||
hardwarePlaybackOn = isOn;
|
||||
updateSettingsAdminUi();
|
||||
});
|
||||
socket.on('LastFmApiKey', updateLastFmApiKey);
|
||||
socket.on('permissions', function(data){
|
||||
permissions = data;
|
||||
|
|
@ -2555,6 +2343,7 @@ $document.ready(function(){
|
|||
});
|
||||
socket.on('connect', function(){
|
||||
socket.send('subscribe', {name: 'dynamicModeOn'});
|
||||
socket.send('subscribe', {name: 'hardwarePlayback'});
|
||||
sendAuth();
|
||||
load_status = LoadStatus.GoodToGo;
|
||||
render();
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ module.exports = PlayerClient;
|
|||
var compareSortKeyAndId = makeCompareProps(['sortKey', 'id']);
|
||||
|
||||
PlayerClient.REPEAT_OFF = 0;
|
||||
PlayerClient.REPEAT_ONE = 1;
|
||||
PlayerClient.REPEAT_ALL = 2;
|
||||
PlayerClient.REPEAT_ALL = 1;
|
||||
PlayerClient.REPEAT_ONE = 2;
|
||||
|
||||
util.inherits(PlayerClient, EventEmitter);
|
||||
function PlayerClient(socket) {
|
||||
|
|
|
|||
|
|
@ -12,11 +12,6 @@ audio.addEventListener('playing', onPlaying, false);
|
|||
var $ = window.$;
|
||||
var $streamBtn = $('#stream-btn');
|
||||
|
||||
document.getElementById('stream-btn-label').addEventListener('mousedown', onLabelDown, false);
|
||||
|
||||
function onLabelDown(event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
function getButtonLabel() {
|
||||
if (tryingToStream) {
|
||||
|
|
@ -34,9 +29,14 @@ function getButtonLabel() {
|
|||
}
|
||||
}
|
||||
|
||||
function getButtonDisabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function renderStreamButton(){
|
||||
var label = getButtonLabel();
|
||||
$streamBtn
|
||||
.button("option", "disabled", getButtonDisabled())
|
||||
.button("option", "label", label)
|
||||
.prop("checked", tryingToStream)
|
||||
.button("refresh");
|
||||
|
|
@ -77,6 +77,8 @@ function updatePlayer() {
|
|||
stillBuffering = true;
|
||||
} else {
|
||||
audio.pause();
|
||||
audio.src = "";
|
||||
audio.load();
|
||||
stillBuffering = false;
|
||||
}
|
||||
actuallyStreaming = shouldStream;
|
||||
|
|
|
|||
|
|
@ -244,22 +244,13 @@ body
|
|||
padding 10px
|
||||
|
||||
#upload-by-url
|
||||
margin: 4px
|
||||
width: 90%
|
||||
|
||||
|
||||
.ui-menu
|
||||
width: 240px
|
||||
font-size: 1em
|
||||
|
||||
#menu-library .ui-state-disabled.ui-state-focus,
|
||||
#menu-playlist .ui-state-disabled.ui-state-focus
|
||||
margin: .3em -1px .2em
|
||||
|
||||
#menu-library .menu-item-last.ui-state-disabled.ui-state-focus,
|
||||
#menu-playlist .menu-item-last.ui-state-disabled.ui-state-focus
|
||||
margin: 5px -1px .2em
|
||||
height: 23px
|
||||
|
||||
#shortcuts
|
||||
h1
|
||||
margin-bottom 10px
|
||||
|
|
@ -315,6 +306,3 @@ body
|
|||
font-size .9em
|
||||
li:before
|
||||
content "\2713"
|
||||
|
||||
.accesskey
|
||||
text-decoration: underline
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
<span class="ui-icon ui-icon-volume-on"></span>
|
||||
</div>
|
||||
<div id="more-playback-btns">
|
||||
<input class="jquery-button" type="checkbox" id="stream-btn"><label id="stream-btn-label" for="stream-btn">Stream</label>
|
||||
<input type="checkbox" id="stream-btn"><label for="stream-btn">Stream</label>
|
||||
</div>
|
||||
<h1 id="track-display"></h1>
|
||||
<div id="track-slider"></div>
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
<input type="file" id="upload-input" multiple="multiple" placeholder="Drag and drop or click to browse">
|
||||
</div>
|
||||
<div>
|
||||
Automatically queue uploads: <input class="jquery-button" type="checkbox" id="auto-queue-uploads"><label for="auto-queue-uploads">On</label>
|
||||
Automatically queue uploads: <input type="checkbox" id="auto-queue-uploads"><label for="auto-queue-uploads">On</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -103,7 +103,7 @@
|
|||
</p>
|
||||
<p>
|
||||
Scrobbling is
|
||||
<input class="jquery-button" type="checkbox" id="toggle-scrobble"><label for="toggle-scrobble">Off</label>
|
||||
<input type="checkbox" id="toggle-scrobble"><label for="toggle-scrobble">Off</label>
|
||||
</p>
|
||||
</div>
|
||||
<div id="settings-lastfm-out">
|
||||
|
|
@ -112,6 +112,13 @@
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h1>Admin</h1>
|
||||
<p>
|
||||
Hardware audio playback is
|
||||
<input type="checkbox" id="toggle-hardware-playback"><label for="toggle-hardware-playback">On</label>
|
||||
</p>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h1>About</h1>
|
||||
<ul>
|
||||
|
|
@ -126,8 +133,8 @@
|
|||
<div class="window-header">
|
||||
<button class="jquery-button clear">Clear</button>
|
||||
<button class="jquery-button shuffle">Shuffle</button>
|
||||
<input class="jquery-button" type="checkbox" id="dynamic-mode"><label id="dynamic-mode-label" for="dynamic-mode">Dynamic Mode</label>
|
||||
<input class="jquery-button" type="checkbox" id="pl-btn-repeat"><label id="pl-btn-repeat-label" for="pl-btn-repeat">Repeat: Off</label>
|
||||
<input class="jquery-button" type="checkbox" id="dynamic-mode"><label for="dynamic-mode">Dynamic Mode</label>
|
||||
<input class="jquery-button" type="checkbox" id="pl-btn-repeat"><label for="pl-btn-repeat">Repeat: Off</label>
|
||||
</div>
|
||||
<div id="playlist">
|
||||
<div class="header">
|
||||
|
|
@ -148,7 +155,7 @@
|
|||
<div id="main-err-msg-text">Loading...</div>
|
||||
</p>
|
||||
</div>
|
||||
<div id="shortcuts" style="display: none" tabindex="-1">
|
||||
<div id="shortcuts" style="display: none">
|
||||
<h1>Playback</h1>
|
||||
<dl>
|
||||
<dt>Space</dt>
|
||||
|
|
@ -289,74 +296,26 @@
|
|||
<dd>Hold while selecting to select all items in between<dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div id="edit-tags" style="display: none">
|
||||
<input type="checkbox" id="edit-tag-multi-name">
|
||||
<label accesskey="i">T<span class="accesskey">i</span>tle: <input id="edit-tag-name"></label><br>
|
||||
|
||||
<input type="checkbox" id="edit-tag-multi-track">
|
||||
<label accesskey="k">Trac<span class="accesskey">k</span> Number: <input id="edit-tag-track"></label><br>
|
||||
|
||||
<input type="checkbox" id="edit-tag-multi-file">
|
||||
<label>Filename: <input id="edit-tag-file"></label><br>
|
||||
<hr>
|
||||
|
||||
<input type="checkbox" id="edit-tag-multi-artistName">
|
||||
<label accesskey="a"><span class="accesskey">A</span>rtist: <input id="edit-tag-artistName"></label><br>
|
||||
|
||||
<input type="checkbox" id="edit-tag-multi-composerName">
|
||||
<label accesskey="c"><span class="accesskey">C</span>omposer: <input id="edit-tag-composerName"></label><br>
|
||||
|
||||
<input type="checkbox" id="edit-tag-multi-performerName">
|
||||
<label>Performer: <input id="edit-tag-performerName"></label><br>
|
||||
|
||||
<input type="checkbox" id="edit-tag-multi-genre">
|
||||
<label accesskey="g"><span class="accesskey">G</span>enre: <input id="edit-tag-genre"></label><br>
|
||||
<hr>
|
||||
|
||||
<input type="checkbox" id="edit-tag-multi-albumName">
|
||||
<label accesskey="b">Al<span class="accesskey">b</span>um: <input id="edit-tag-albumName"></label><br>
|
||||
|
||||
<input type="checkbox" id="edit-tag-multi-albumArtistName">
|
||||
<label>Album Artist: <input id="edit-tag-albumArtistName"></label><br>
|
||||
|
||||
<input type="checkbox" id="edit-tag-multi-trackCount">
|
||||
<label>Track Count: <input id="edit-tag-trackCount"></label><br>
|
||||
|
||||
<input type="checkbox" id="edit-tag-multi-year">
|
||||
<label accesskey="y"><span class="accesskey">Y</span>ear: <input id="edit-tag-year"></label><br>
|
||||
|
||||
<input type="checkbox" id="edit-tag-multi-disc">
|
||||
<label accesskey="d"><span class="accesskey">D</span>isc Number: <input id="edit-tag-disc"></label><br>
|
||||
|
||||
<input type="checkbox" id="edit-tag-multi-discCount">
|
||||
<label>Disc Count: <input id="edit-tag-discCount"></label><br>
|
||||
|
||||
<input type="checkbox" id="edit-tag-multi-compilation">
|
||||
<label accesskey="m">Co<span class="accesskey">m</span>pilation: <input type="checkbox" id="edit-tag-compilation"></label><br>
|
||||
<hr>
|
||||
|
||||
<div style="float: right">
|
||||
<button id="edit-tags-ok" accesskey="v">Sa<span class="accesskey">v</span>e & Close</button>
|
||||
<button id="edit-tags-cancel">Cancel</button>
|
||||
</div>
|
||||
<button id="edit-tags-prev" type="button" accesskey="p"><span class="accesskey">P</span>revious</button>
|
||||
<button id="edit-tags-next" type="button" accesskey="n"><span class="accesskey">N</span>ext</button>
|
||||
<label accesskey="r" id="edit-tags-per-label"><input id="edit-tags-per" type="checkbox">Pe<span class="accesskey">r</span> Track</label>
|
||||
</div>
|
||||
<ul id="menu-playlist" style="display: none">
|
||||
<li><a href="#" class="remove">Remove</a></li>
|
||||
<li><a href="#" class="delete">Delete From Library</a></li>
|
||||
<li><a href="#" class="download" target="_blank">Download</a></li>
|
||||
<li><a href="#" class="edit-tags">Edit Tags</a></li>
|
||||
<li>
|
||||
<a href="#" class="delete">Delete From Library</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="download" target="_blank">Download</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul id="menu-library" style="display: none">
|
||||
<li><a href="#" class="queue">Queue</a></li>
|
||||
<li><a href="#" class="queue-next">Queue Next</a></li>
|
||||
<li><a href="#" class="queue-random">Queue in Random Order</a></li>
|
||||
<li><a href="#" class="queue-next-random">Queue Next in Random Order</a></li>
|
||||
<li><a href="#" class="delete">Delete</a></li>
|
||||
<li><a href="#" class="download" target="_blank">Download</a></li>
|
||||
<li><a href="#" class="edit-tags menu-item-last">Edit Tags</a></li>
|
||||
<li>
|
||||
<a href="#" class="delete">Delete</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="download" target="_blank">Download</a>
|
||||
</li>
|
||||
</ul>
|
||||
<script src="vendor/jquery-2.1.0.min.js"></script>
|
||||
<script src="vendor/jquery-ui-1.10.4.custom.min.js"></script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue