Compare commits

..

469 commits

Author SHA1 Message Date
Andrew Kelley
a27807082a Merge pull request #251 from CalebMorris/master
Changed doc link to redirect locally.
2014-05-07 22:41:31 -07:00
Caleb Morris
fae50a8d69 Changed doc link to redirect locally. 2014-05-07 20:31:54 -07:00
Andrew Kelley
fa21bdc3d2 refactor import by youtube url into plugin 2014-05-04 20:28:07 -07:00
Andrew Kelley
cb6077bc87 Merge pull request #237 from seansaleh/master
Chrome waits for 2mb before playing song, leading to long buffering times
2014-04-28 11:23:22 -07:00
seansaleh
7ea657e0d4 instantBufferBytes are now a configurable option 2014-04-28 11:19:54 -07:00
Andrew Kelley
0e3f87b8f1 Merge pull request #238 from yoasif/master
added conf.py to docs because of build failure.
2014-04-27 20:28:34 -07:00
Asif Youssuff
ec7ebcfda1 added conf.py to docs because of build failure. 2014-04-27 23:23:33 -04:00
Andrew Kelley
bb1effc161 Merge pull request #231 from yoasif/master
Initial documentation commit
2014-04-27 20:09:05 -07:00
Andrew Kelley
656d3068c4 Merge pull request #233 from toofar/mpd
Make MPD server more like MPD.
2014-04-27 20:06:10 -07:00
jimmy
9a24b61d81 MPD: Support "any" as a search type in find and search.
Currently I am just searching all the defined tag types. I am not sure if
there should be a specific order they are searched in or whether we should
just be searching string ones.

Now coerces the tag value to a string.
2014-04-27 21:29:54 +12:00
jimmy
6745455a74 MPD: Make "search" a substring match.
This seems to be what mpd does I and am rather used to that behaviour. I am
switching on caseSensitive here which may not be what it is intended for but
for now that only get passed through as false from one place so everything
else should still work fine.
2014-04-27 21:29:54 +12:00
Asif Youssuff
26474c9002 Initial documentation commit 2014-04-25 22:56:29 -04:00
Andrew Kelley
35648a0a1d README: mention memory constraints of rpi. see #228 2014-04-24 09:41:12 -07:00
Andrew Kelley
7527911bb2 Merge pull request #226 from jeffrom/menuglitch
fix disabled menu item jumping on focus
2014-04-23 21:36:51 -07:00
jeffrom
3971dc65f6 fix disabled menu item focus jumping
unfortunately this causes a new issue. the container gets 1px larger i
think due to box model issues. but this is better than before. le sigh...
2014-04-23 20:13:43 -07:00
Andrew Kelley
92f0c80e9f close grooveFile instances at the correct time. closes #200 2014-04-23 19:40:30 -07:00
Andrew Kelley
d17157fcc9 fix potential crash when users disconnect from client 2014-04-23 17:06:56 -07:00
Andrew Kelley
45b8d28dcb README: add note about debian and ubuntu node package. closes #215 2014-04-23 00:42:38 -07:00
Andrew Kelley
4133c55fa7 Merge pull request #208 from rbuch/patch-1
Add margin to URL upload bar
2014-04-22 15:48:40 -07:00
Ronak Buch
53d1cbe9b0 Add margin to URL upload bar
This makes margins for text input consistent across tabs.
2014-04-22 17:40:40 -05:00
Andrew Kelley
fa8fbf60fc update node-groove. closes #203 2014-04-21 10:06:39 -07:00
Andrew Kelley
3c02511c9e add acoustid app key 2014-04-19 15:41:40 -07:00
Andrew Kelley
d642629fd0 update to node-groove 1.4.0 which has acoustid scanning support 2014-04-19 15:34:11 -07:00
Andrew Kelley
b861d4776c fix dynamic mode not sorting songs correctly
Previously when track information was loaded from the DB dates (such as
lastQueueDate) were read as strings since they were simply deserialized
with JSON.parse. Now type information is used to turn date properties
into actual dates so that sorting and date operations can work.
2014-04-18 12:16:28 -07:00
Andrew Kelley
656aab43ab player: add debug statement for nowplaying event 2014-04-15 14:42:26 -07:00
Andrew Kelley
876e72db0c readme: link to the live demo 2014-04-15 13:51:46 -07:00
Andrew Kelley
f36efe16c7 client: fix incorrect enum values for repeat. closes #189 2014-04-14 18:36:21 -07:00
Andrew Kelley
8c2b78efc8 client: default selected queue item is the current track. closes #162 2014-04-13 20:55:22 -07:00
Andrew Kelley
8fe56b52bf update dependencies 2014-04-13 20:42:35 -07:00
Andrew Kelley
b1a48d5b27 client: fix UI issues with buttons
* fix hover state getting stuck after clicking a button. closes #166
 * fix clicking buttons clearing the selection. closes #183
 * minor code style cleanups
2014-04-13 20:31:41 -07:00
Andrew Kelley
43d96fb348 client: shortcuts window scrollable with arrows. closes #28 2014-04-13 19:36:20 -07:00
Andrew Kelley
8f13aa795a client: wire up cancel button on edit tags dialog 2014-04-13 19:17:10 -07:00
Andrew Kelley
7d0ac9dfd4 tag editing: support per track mode and access keys 2014-04-13 18:58:09 -07:00
Andrew Kelley
2b3aff220f client: support editing multiple tags at once 2014-04-13 16:12:52 -07:00
Andrew Kelley
cc8f0abea4 update music-library-index to fix library selection bug 2014-04-13 01:43:04 -07:00
Andrew Kelley
145e39ff02 client code cleanup 2014-04-13 01:42:20 -07:00
Andrew Kelley
52e54b0152 Merge branch 'master' into edit-tags
Conflicts:
	src/client/app.js
2014-04-13 00:15:34 -07:00
Andrew Kelley
e1bd006566 client: code cleanup 2014-04-13 00:13:55 -07:00
Andrew Kelley
453143040d client: fix not stopping keyboard propagation for text boxes 2014-04-12 23:58:43 -07:00
Andrew Kelley
6dbfb820ea server: parse updateTags messages more carefully 2014-04-12 23:58:40 -07:00
Andrew Kelley
7cc928e1e9 refactor db props listings 2014-04-12 23:10:21 -07:00
Josh Wolfe
deed3a7eb8 obstruct hotkeys in the edit tags dialog 2014-04-12 23:01:35 -07:00
Josh Wolfe
bf83c04902 edit dialog edits all the tags 2014-04-12 22:16:59 -07:00
Josh Wolfe
ac69158091 edit tags dialog can edit some tags 2014-04-12 20:58:56 -07:00
Andrew Kelley
94ea856fae client: fix selection.incrementPos for albums in a list. closes #38 2014-04-12 20:48:57 -07:00
Josh Wolfe
f9c820bdbf edit tags button appends a lol to the song name 2014-04-12 19:28:29 -07:00
Andrew Kelley
78cd66d8df server: add updateTags message to update db 2014-04-12 19:26:13 -07:00
Andrew Kelley
259f247290 persist queue on shuffle. closes #190 2014-04-10 09:50:13 -07:00
Andrew Kelley
cc7183ce53 recognize TPA and TCM tags
TPA is an alternate "disc" tag
TCM is an alternate "composer" tag
2014-04-08 11:05:46 -07:00
Andrew Kelley
e02c986e86 client: fix library items not always expanding consistently 2014-03-31 18:27:49 -07:00
Andrew Kelley
057a2e8776 client: fix displaying incorrect track number
when the track number is not known.
2014-03-31 17:48:04 -07:00
Andrew Kelley
c6ada32047 importURL: URI decode filename
Fixes _20 etc showing up in filenames.
2014-03-30 18:48:36 -07:00
Andrew Kelley
3325a248ee send update to client when replaygain scan completes 2014-03-30 18:40:24 -07:00
Josh Wolfe
f6054e662b fixing shift up/down in library. closes #46. 2014-03-29 22:07:04 -07:00
Andrew Kelley
f91849db47 update to latest express/connect 2014-03-28 14:56:00 -07:00
Andrew Kelley
3fee3df88c client: use encodeURI on download links. closes #168 2014-03-28 14:55:23 -07:00
Andrew Kelley
9fa3ebac52 fix not watching music root folder 2014-03-28 13:34:45 -07:00
Andrew Kelley
6a959e6d12 import by URL supports YouTube. closes #49
There will be a constant arms race with this node module keeping up
with YouTube. Currently YouTube is ahead but soon the ytdl module will
catch up.

This means the feature will not always be reliable, but this is the best
support we can get for it.
2014-03-28 13:04:44 -07:00
Andrew Kelley
09463c9d0a update keyboard shortcut documentation 2014-03-27 09:40:47 -07:00
Andrew Kelley
5c8f45b6e9 improve import by URL feature. closes #171
* Drop dependency on temp module
 * Download files to music-folder/.tmp/ before moving them.
 * Don't watch folders that start with a '.'
 * Don't allow generated paths to start with a '.'

Fixes import file race condition.

Prevents needless file copy operation when importing in situations where
the music directory is in a different device than /tmp.
2014-03-26 17:17:07 -07:00
Andrew Kelley
4e560b93e8 don't end directory names with ".". closes #177 2014-03-26 16:21:33 -07:00
Andrew Kelley
c230081b3c fix crash - writing to closed web socket. closes #176 2014-03-26 16:14:02 -07:00
Andrew Kelley
0d6e6cb203 streaming: increase instant buffer size by 20KB 2014-03-26 13:42:46 -07:00
Andrew Kelley
ab1b828f4d update duration info in db when replaygain scan finishes 2014-03-26 08:59:21 -07:00
Andrew Kelley
1ec74cea49 README: update MPDroid project link 2014-03-25 23:57:25 -07:00
Andrew Kelley
693e06a137 add roadmap to README 2014-03-25 23:26:35 -07:00
Andrew Kelley
207f77e4c9 I can render html with no handlebars, no handlebars, no handlebars... 2014-03-25 17:50:59 -07:00
Andrew Kelley
cde56a3e6b client: fix incorrect expand icon shown sometimes 2014-03-25 14:27:04 -07:00
Andrew Kelley
c7e2387325 client: use textContent instead of innerText
Fixes UI for Firefox
2014-03-25 13:57:20 -07:00
Andrew Kelley
dec92a1b39 client: fix incorrectly displaying songs as random 2014-03-25 13:00:05 -07:00
Andrew Kelley
b0bc750c9b client: ditch handlebars for rendering artists 2014-03-25 12:53:24 -07:00
Andrew Kelley
398e3e9db8 client: ditch handlebars for playlist rendering 2014-03-25 10:56:10 -07:00
Josh Wolfe
9fa80e43ff implementing shift+arrows in queue. 2014-03-25 01:50:54 -07:00
Josh Wolfe
1c0129a779 ctrl+arrows and ctrl+space in library work like in the queue. 2014-03-25 00:02:32 -07:00
Josh Wolfe
b0839774f0 queue uses ctrl to move the cursor without selecting, and alt to bump selected tracks up or down. 2014-03-25 00:02:22 -07:00
Andrew Kelley
113f008115 client: ditch handlebars for stored playlists 2014-03-24 23:50:49 -07:00
Andrew Kelley
a4b0fa4a12 client: ditch handlebars for context menus 2014-03-24 23:42:05 -07:00
Josh Wolfe
b693b9f6f6 ctrl+space to toggle selection under the cursor 2014-03-24 23:04:23 -07:00
Josh Wolfe
de234fde4f fix cursor selection not showing up 2014-03-24 23:04:23 -07:00
Andrew Kelley
825fa5d5d0 client: ditch handlebars for stored playlists 2014-03-24 22:05:55 -07:00
Andrew Kelley
889e8b0bec client: ditch handlebars for shortcuts dialog 2014-03-24 21:58:49 -07:00
Andrew Kelley
540266c058 client: ditch handlebars in settings pane. closes #35 2014-03-24 21:51:52 -07:00
Josh Wolfe
bf02f0d3c5 recover from others deleting our selection.cursor. closes #158 2014-03-24 21:23:33 -07:00
Josh Wolfe
17f86d9735 uncocoify some of app.js 2014-03-24 21:02:18 -07:00
Andrew Kelley
42f66c817f Revert "ability to disable streaming in config"
This reverts commit baf5ebeb4e.

Instead of this, we're going to detach the encoder when nobody is
connected and re-attach it when somebody connects.
2014-03-24 17:38:42 -07:00
Andrew Kelley
11068623b5 fix upload for multiple files
also when auto queuing, sort by position in library
2014-03-24 17:12:54 -07:00
Andrew Kelley
3be4ad9cb6 upload improvements. closes #45
* ditch qq file uploader in favor of native xhr2
 * implement client-side optional auto-queuing
2014-03-24 15:49:51 -07:00
Andrew Kelley
1a7dc0e0a3 update connect-static to latest. enables static asset caching with etags 2014-03-18 16:11:16 -07:00
Andrew Kelley
d30276f415 Release 1.0.1 2014-03-18 15:29:10 -07:00
Andrew Kelley
af72b80c60 update connect-static to latest. fixes last.fm authentication 2014-03-18 15:19:56 -07:00
Andrew Kelley
515845e3f9 also recognize "TCMP" ID3 tag as compilation album flag 2014-03-16 21:52:29 -07:00
Andrew Kelley
44609d7ca3 default import path includes artist 2014-03-16 21:22:37 -07:00
Andrew Kelley
0115580727 fix race condition when removing tracks from playlist
closes #160
2014-03-16 16:02:53 -07:00
Andrew Kelley
b6479f868c Release 1.0.0 2014-03-15 18:58:04 -07:00
Andrew Kelley
628b1f2e4c remove chat and stored playlist stub from UI 2014-03-15 18:25:21 -07:00
Josh Wolfe
ce9f89aa3f moving mpd client listener pattern out of eventemitter and into an array. closes #154. 2014-03-15 18:15:46 -07:00
Andrew Kelley
b5cc67ab7e fix deletion detection not recursive. closes #159 2014-03-15 17:36:58 -07:00
Josh Wolfe
be90006315 music-library-index version 1.1.0. closes #72. 2014-03-15 17:09:15 -07:00
Andrew Kelley
90e2bf10d5 upload: fix not sending response.end()
moves #45 out of milestone 1.0.0
2014-03-15 15:42:04 -07:00
Josh Wolfe
565aed7915 don't expect album gain consistency when the album is undefined. closes #155. 2014-03-15 15:38:23 -07:00
Josh Wolfe
c59a4befae after deleting tracks, select the next one, not some random one. closes #157. 2014-03-15 14:45:18 -07:00
Josh Wolfe
56358da460 don't tolerate missing semicolons 2014-03-15 14:26:54 -07:00
Josh Wolfe
2b4225e156 fix shift click going up in the queue. closes #156. 2014-03-15 14:06:14 -07:00
Josh Wolfe
ece81343c6 deleting library items removes them from the queue as well. closes #117 2014-03-15 13:28:42 -07:00
Andrew Kelley
929260899c client: ask for library first 2014-03-15 15:28:54 -04:00
Andrew Kelley
a2816003be fix disc and discCount undefined. actually fixes #66 2014-03-12 19:50:25 -07:00
Andrew Kelley
9ccec6485a MPD protocol: implement rescan command 2014-03-12 12:48:01 -07:00
Andrew Kelley
d811c2e7f6 client-side code cleanup 2014-03-12 11:47:53 -07:00
Andrew Kelley
e0a6b97fff update outdated dependencies 2014-03-11 22:00:33 -07:00
Andrew Kelley
0358e5270f update music-library-index to latest. closes #66 2014-03-11 21:11:55 -07:00
Andrew Kelley
baf5ebeb4e ability to disable streaming in config 2014-03-07 03:10:56 -05:00
Andrew Kelley
2756f4c06f fix crash: call socket.destroy() not socket.close() on error 2014-03-05 00:37:05 -05:00
Andrew Kelley
87f4657484 MPD protocol: fix upgrading protocol ignoring part of the buffer
closes #146

Also fixed a crash when setting consume on/off
2014-03-05 00:33:34 -05:00
Andrew Kelley
9c24952e3f player: fix race condition on initialization. closes #153 2014-03-04 23:58:37 -05:00
Andrew Kelley
72cf1783b7 player: set "don't cache this" headers on stream 2014-03-04 17:19:22 -05:00
Andrew Kelley
543a735848 update README 2014-03-04 15:44:04 -05:00
Andrew Kelley
1d3bc7c3db simplify build process 2014-03-04 15:20:06 -05:00
Andrew Kelley
70e445c80d watching dev files for changes is no longer supported
You must manually kill the server and restart it when you change
assets or server code.
2014-03-04 15:00:40 -05:00
Andrew Kelley
71521d90f6 library scanning: always use a trailing slash for readdir 2014-03-04 14:26:01 -05:00
Andrew Kelley
f748199894 extract gzip static middleware to a separate module 2014-03-04 05:46:02 -05:00
Andrew Kelley
c736632282 gzip static assets: store only gzipped assets 2014-03-04 05:03:40 -05:00
Andrew Kelley
9768c840b9 static assets are gzipped and held permanently in memory 2014-03-04 04:58:20 -05:00
Andrew Kelley
0852ab8cff upgrade jquery and jquery ui to latest stable. closes #74
* jquery 1.8.2 -> 2.1.0
 * jquery ui 1.8.24 -> 1.10.4
2014-03-04 03:48:40 -05:00
Andrew Kelley
223afa1ed0 fix playlist items not being deleted from db during dynamic mode
Might also have fixed a memory leak. Looks like we weren't closing files
when dynamic mode deleted songs from the queue.
2014-03-04 03:36:39 -05:00
Andrew Kelley
3525d6a4e0 MPD protcol: remove unimplemented commands. closes #112 2014-03-04 02:58:05 -05:00
Andrew Kelley
59d0f8cd62 MPD protocol: implement swap and swapid commands 2014-03-04 02:50:26 -05:00
Andrew Kelley
99c92a9203 MPD protocol: implement password and notcommands commands 2014-03-04 02:36:58 -05:00
Andrew Kelley
ea77be50b5 persist playlist on server restart. closes #98 2014-03-04 02:27:31 -05:00
Andrew Kelley
6068a19f3e remove commented out code 2014-03-03 20:51:15 -05:00
Andrew Kelley
a345f24f93 fix crash during upload 2014-03-03 20:40:29 -05:00
Andrew Kelley
5c8e0fcf9e websocket default port to 80 not 443 2014-03-03 17:35:33 -05:00
Andrew Kelley
bc6ecfb65d default password: multiple of 3 for base64 to avoid ending in '=' 2014-03-03 14:17:50 -05:00
Andrew Kelley
725f10ef97 streaming: 8KB more instant buffering 2014-03-03 12:58:22 -05:00
Andrew Kelley
c5bb5b2e66 web ui: fix current track not displayed sometimes 2014-03-03 12:43:21 -05:00
Andrew Kelley
09f64ecb5c api server: listen for better player events to detect db update 2014-03-03 05:41:54 -05:00
Andrew Kelley
f2712ddaad re-write interface between client and server
* Use raw web sockets instead of socket.io. closes #114
 * promote most plugin code to be actually integrated with
   groovebasin, except last.fm
 * ability to upgrade from mpd protocol to groovebasin protocol
 * serialize/deserialize lastQueueDate and update it for all types of queuing.
   Fixes DynamicMode losing randomness data on server restart.
 * repeat state is saved between restarts
 * fixed a race condition in player.importFile
 * a smaller subset of db file properties are sent to the server, reducing
   library payload size
 * diffs are sent to client for playlist and library. closes #142
 * information is no longer requested in the groovebasin API; it is only
   subscribed to. closes #115
 * all commands go through the permissions framework. see #37
 * chat is deleted for now. closes #17
 * update to latest superagent and leveldb
 * fix stream activating and deactivating when seeking and not streaming
2014-03-03 05:25:50 -05:00
Andrew Kelley
a0c6d8c36d streaming improvements
* server sends data as fast as it can when under the buffer limit
 * client responds to 'seek' events by reconnecting to the stream
2014-02-28 15:27:29 -05:00
Andrew Kelley
5102ef205c MPD protocol: remove unsupported commands 2014-02-28 12:08:27 -05:00
Andrew Kelley
7696b9791f MPD protocol: implement outputs and plchangesposid commands 2014-02-28 12:08:27 -05:00
Andrew Kelley
0ca3c286f2 detect file deletions on startup. closes #116 2014-02-28 12:08:12 -05:00
Andrew Kelley
fbc6386960 fix stupid default password 2014-02-27 18:19:26 -05:00
Andrew Kelley
35354ac81e MPD protocol improvements
* correct permissions for some commands
 * implement search and searchadd commands
 * remove commands that groove basin does not support
 * fix idle not checking read permission
 * fix noidle returning OK twice
2014-02-27 17:53:33 -05:00
Andrew Kelley
406eae55aa MPD protocol: emit Last-Modified-Date for directories where appropriate 2014-02-27 16:38:25 -05:00
Andrew Kelley
667fd0c76f MPD protocol: update command supports uri argument 2014-02-27 11:45:50 -05:00
Andrew Kelley
9be515c764 MPD protocol: listallinfo takes a uri parameter 2014-02-27 10:38:54 -05:00
Andrew Kelley
e937b798e4 MPD protocol: make it more robust
* play command: no args or -1 does "unpause"
 * random command: noop instead of error
 * single command: fix the logic to accept the argument
 * optional ranges allow -1 to mean no argument
2014-02-26 22:29:44 -05:00
Andrew Kelley
c018b6cb3a MPD protocol: implement listall command 2014-02-26 18:52:06 -05:00
Andrew Kelley
b1755a4fd9 MPD protocol: implement findadd command 2014-02-26 18:01:33 -05:00
Andrew Kelley
3a953c22e4 MPD protocol: implement count command 2014-02-26 17:49:09 -05:00
Andrew Kelley
d004ca79e5 MPD protocol: implement find command 2014-02-26 17:16:50 -05:00
Andrew Kelley
585b13bad1 MPD protocol: 'consume' status updated correctly 2014-02-26 14:34:51 -05:00
Andrew Kelley
6e567086d9 route dynamicmode through permissions framework
and connect it to 'consume' MPD protocol command
2014-02-26 14:32:11 -05:00
Andrew Kelley
920884263b MPD protocol: implement currentsong command 2014-02-26 12:55:00 -05:00
Andrew Kelley
f23a31d5db fix updated songs getting duplicate db entries
and implement the 'database' subsystem notification in MPD protocol
2014-02-26 12:46:24 -05:00
Andrew Kelley
e51e19846d MPD protocol: implement the lsinfo command 2014-02-26 05:37:09 -05:00
Andrew Kelley
e8fa0e83a3 rework file scanning and watching to not use polling
use fs.watch instead
2014-02-26 04:46:37 -05:00
Andrew Kelley
054f025064 scrub invalid db entries on startup and other misc cleanups 2014-02-26 02:02:06 -05:00
Andrew Kelley
22a0507595 use a deduped queue for importing tracks. fixes #128 2014-02-25 22:11:09 -05:00
Andrew Kelley
c2e4429e48 MPD protocol: code organization 2014-02-25 21:19:13 -05:00
Andrew Kelley
69aefc0739 MPD protocol: implement the list command and fix batch commands 2014-02-25 03:44:45 -05:00
Andrew Kelley
5951806436 MPD protocol: add a forgotten callback invocation 2014-02-25 01:41:55 -05:00
Andrew Kelley
cf30edab0f MPD protocol: implement playlistid command 2014-02-25 01:33:01 -05:00
Andrew Kelley
b258ac5ce9 MPD protocol: implement move and moveid 2014-02-25 01:09:33 -05:00
Andrew Kelley
0a5cab8370 MPD protocol: use numeric IDs and implement more cmds
Now it (mostly) works with MPDroid Android app
2014-02-24 23:47:37 -05:00
Andrew Kelley
866c7e62d5 MPD protocol: implement deleteid command 2014-02-24 22:46:31 -05:00
Andrew Kelley
3196ba9494 MPD Protocol: implement delete command and fix bug
in player.insertTracks
2014-02-24 22:40:47 -05:00
Andrew Kelley
de7f535e50 MPD protocol: generic parameter parsing
and subject all commands to permissions framework
2014-02-24 22:02:53 -05:00
Andrew Kelley
65c58a95da MPD protocol: implement addid command 2014-02-24 19:06:48 -05:00
Andrew Kelley
03404fa01f MPD protocol: support async commands and implement add 2014-02-24 18:24:38 -05:00
Andrew Kelley
4c78a11b64 streaming: save the last n buffers of data to send to clients immediately 2014-02-24 16:12:28 -05:00
Andrew Kelley
cc2b619978 MPD Protocol: ability to set the host and disable 2014-02-24 13:59:45 -05:00
Andrew Kelley
5eb430da3b MPD protocol: handle idle and noidle commands 2014-02-24 13:03:55 -05:00
Andrew Kelley
f23ec23c0e MPD protocol: implement the status and stats commands 2014-02-24 04:14:51 -05:00
Andrew Kelley
990b4177e2 MPD protocol: implement seekcur and seekid 2014-02-24 03:32:11 -05:00
Andrew Kelley
5aaaa26348 MPD protocol: implement seek 2014-02-24 03:26:06 -05:00
Andrew Kelley
78648898b9 MPD protocol: more commands implemented
* close
 * play
 * playid
 * playlist
 * playlistinfo
 * stop
2014-02-24 03:14:39 -05:00
Andrew Kelley
1c13ec2068 MPD protocol: implement previous and next 2014-02-24 02:19:09 -05:00
Andrew Kelley
0b345463da MPD protocol: implement pause command 2014-02-24 01:52:51 -05:00
Andrew Kelley
3adc2b8759 basic framework for MPD support and a few commands 2014-02-24 01:47:28 -05:00
Andrew Kelley
14540b14b1 delete warning suppresing code
At some point this will be incorporated into #30
2014-02-23 23:41:36 -05:00
Andrew Kelley
5c1f18abf1 add a .jshintrc to the project 2014-02-23 18:09:44 -05:00
Andrew Kelley
a796e51724 fix volume keyboard shortcuts in firefox. closes #130 2014-02-21 19:25:58 -05:00
Andrew Kelley
2843faa39b fix last.fm scrobbling. closes #113 2014-02-21 18:55:42 -05:00
Andrew Kelley
3e270b68d1 rename github.com/superjoe30 to github.com/andrewrk 2014-01-23 20:36:00 -05:00
Andrew Kelley
6acf4a89ed add meta charset=utf8 to index.html. hopefully closes #140 2014-01-06 10:24:16 -07:00
Andrew Kelley
2e36bfa2f3 Merge remote-tracking branch 'origin/npmjsisdown' 2014-01-05 14:17:47 -07:00
Josh Wolfe
ee83c996fa fixing multiselect shiftIds. closes #119 2013-12-31 17:12:13 -07:00
Josh Wolfe
972d2cd0f8 replacing player.state with player.isPlaying 2013-12-02 20:11:11 -07:00
Josh Wolfe
11ea30fecb fixing the stop button to properly seek to 0 2013-12-02 19:37:05 -07:00
Andrew Kelley
22c13caeda update README 2013-11-29 15:21:14 -05:00
Andrew Kelley
5331c55306 Merge remote-tracking branch 'origin/npmjsisdown' 2013-11-25 11:39:13 -05:00
Andrew Kelley
2fec70a126 update groove dependency to 1.3.0 2013-11-24 15:35:40 -05:00
Josh Wolfe
76dcbe7c15 synchronizing client and server clocks. closes #132. 2013-11-21 19:47:49 -07:00
Josh Wolfe
a265f082e0 fixing formatTime() for negative time? 2013-11-21 19:11:50 -07:00
Andrew Kelley
d21f81877d update to latest node-groove API 2013-11-18 11:55:24 -05:00
Josh Wolfe
31375a5afa fixing delete UI 2013-11-15 16:40:20 -07:00
Andrew Kelley
e859d86940 remove unused soundmanager2 <script> 2013-11-03 16:26:29 -05:00
Andrew Kelley
e4538f67a5 client streaming: adhere to naming conventions 2013-11-01 12:49:26 -04:00
Andrew Kelley
601f1d054e streaming shows when it is buffering 2013-10-31 17:59:47 -04:00
Andrew Kelley
c9c5241c83 native html5 instead of soundmanager 2 2013-10-31 17:31:01 -04:00
Andrew Kelley
1c660be774 don't allow clients slow streaming to slow down other clients 2013-10-31 17:04:04 -04:00
Andrew Kelley
8c1b01cd7d fix stream URL extension and mime type 2013-10-30 17:03:53 -04:00
Andrew Kelley
d45530e5e7 fix changing volume not showing up on other computers. closes #125 2013-10-30 16:23:46 -04:00
Andrew Kelley
a6fb766787 fix volume ui going higher than 1.0. closes #126 2013-10-30 15:54:42 -04:00
Andrew Kelley
69acca675e depend on latest node-groove 2013-10-29 16:07:17 -04:00
Andrew Kelley
0dd8994b0b using mp3 streaming until the libav vorbis issue is resolved :crying: 2013-10-29 14:13:15 -04:00
Andrew Kelley
90792a3a4f proper stream buffering behavior on paused audio 2013-10-29 11:38:56 -04:00
Andrew Kelley
ae8de85c07 http streaming somewhat working 2013-10-28 16:31:48 -04:00
Andrew Kelley
11a306c4df fix crash on attempted scan of bogus db entry 2013-10-25 12:33:55 -04:00
Andrew Kelley
bf4800b278 update to latest groove backend 2013-10-23 15:05:03 -04:00
Andrew Kelley
d5be670a27 fix readme typo from last commit 2013-10-21 23:10:19 -04:00
Andrew Kelley
f77e7d534c readme update 2013-10-21 22:49:49 -04:00
Andrew Kelley
514cb3eb66 remove dead reference to diacritics package 2013-10-21 10:43:48 -04:00
Andrew Kelley
4ad9700e2e Revert "update installation instructions with note about newest ubuntu"
ubuntu node package is completely fucked.
This reverts commit 634015bc89.
2013-10-19 16:19:21 -04:00
Andrew Kelley
634015bc89 update installation instructions with note about newest ubuntu 2013-10-18 20:04:50 -04:00
Andrew Kelley
d1421ca7d1 add note about IRC channel 2013-10-18 16:26:04 -04:00
Andrew Kelley
593de05286 readme: add note about DRM 2013-10-17 12:57:26 -04:00
Andrew Kelley
e5abea9b2a depend on latest groove which hopefully solves memory leak 2013-10-10 00:25:11 -04:00
Andrew Kelley
faf3a9abf0 faster rebuilding of album table index 2013-10-08 16:38:45 -04:00
Andrew Kelley
ccb7781909 update music-library-index for fewer server freezes 2013-10-08 16:24:38 -04:00
Andrew Kelley
cc1e779926 repeat goes off, 1, all instead of off, all, 1 2013-10-08 12:33:36 -04:00
Andrew Kelley
258a8d35eb lazy replaygain scanning working reasonably well 2013-10-08 12:24:59 -04:00
Andrew Kelley
d1bf652f87 a little more reliable replaygain scan 2013-10-07 23:34:38 -04:00
Andrew Kelley
7b1433b3c1 more efficient replaygain scanning
except that it's causing an assertion failure in libuv
2013-10-07 22:59:51 -04:00
Andrew Kelley
c9f6fc22a2 tracks with unkwown albums are scanned independently 2013-10-07 18:10:26 -04:00
Andrew Kelley
07a75eea39 first pass at auto replaygain volume adjustment 2013-10-07 17:11:51 -04:00
Andrew Kelley
7d53ecf42c albums are replaygain scanned upon import 2013-10-07 15:47:19 -04:00
Andrew Kelley
7a38834093 update engines in package.json 2013-10-04 03:45:51 -04:00
Andrew Kelley
8724737bbc README update 2013-10-04 03:42:25 -04:00
Andrew Kelley
f19fd10bb7 move TODO items to github issues 2013-10-04 03:36:30 -04:00
Andrew Kelley
25c816872a update README 2013-10-04 03:01:45 -04:00
Andrew Kelley
5302a2880d update docs 2013-10-04 02:40:56 -04:00
Andrew Kelley
1349bf6f91 factored out music library index. and more...
* use keese
 * relative file path is no longer the db key
2013-10-04 02:23:02 -04:00
Andrew Kelley
983937132e fix db regression 2013-10-02 21:03:33 -04:00
Andrew Kelley
02ffef20bc update to latest node-groove 2013-10-02 16:45:44 -04:00
Andrew Kelley
3d73555f20 fix dynamic mode plugin 2013-09-27 07:12:15 -04:00
Andrew Kelley
d841d01ed3 use latest groove package to eliminate 100% cpu 2013-09-27 00:40:41 -04:00
Andrew Kelley
4d949ccb33 fix upload plugin 2013-09-26 07:19:16 -04:00
Andrew Kelley
0770149c55 fix download plugin 2013-09-26 05:21:27 -04:00
Andrew Kelley
83ec17de20 fix download plugin
also use config.js for config vars instead of leveldb
2013-09-26 04:21:31 -04:00
Andrew Kelley
d6a685507a chat plugin fixed 2013-09-26 03:00:28 -04:00
Andrew Kelley
83f93be3ab empty track number instead of 0 2013-09-26 01:49:39 -04:00
Andrew Kelley
53d17666ff pressing prev on first track with repeat all on goes to end
and other misc repeat cleanups
2013-09-26 01:21:53 -04:00
Andrew Kelley
dc268cda0a fix setting volume 2013-09-26 01:02:59 -04:00
Andrew Kelley
6d9d2846a0 fix shuffle 2013-09-26 00:01:48 -04:00
Andrew Kelley
e84277a0b5 dev script: use chokidar instead of watch 2013-09-25 22:53:28 -04:00
Andrew Kelley
0a11734015 fix repeat one and repeat all 2013-09-25 22:15:05 -04:00
Andrew Kelley
7294ed2397 move status message thing to player server 2013-09-25 18:35:50 -04:00
Andrew Kelley
ddbdac738a extract playerserver to separate file and fix pause 2013-09-25 18:30:47 -04:00
Andrew Kelley
64aacb4312 fix playId pausing song
I can't find any more playback bugs :)
2013-09-24 16:45:24 -04:00
Andrew Kelley
5bbfe519c2 fix several playback bugs 2013-09-24 16:13:45 -04:00
Andrew Kelley
940396e588 cleanup 2013-09-24 15:32:57 -04:00
Andrew Kelley
d6bf428cc3 seeking and clicking around on the playlist working 2013-09-24 15:24:46 -04:00
Andrew Kelley
a2c0a5e62b we have audio!
...this code is really fucking complicated. Am I doing it all wrong?
2013-09-24 12:45:51 -04:00
Andrew Kelley
da02be78af cleanup 2013-09-24 08:01:51 -04:00
Andrew Kelley
e07ea0c3b8 library displays client side. playing doesn't work 2013-09-24 05:23:34 -04:00
Andrew Kelley
c13241484c fix bugs with new library code 2013-09-24 04:50:03 -04:00
Andrew Kelley
27ac56243f refactor groovebasin config 2013-09-24 04:28:27 -04:00
Andrew Kelley
1804bc40d9 use leveldb for state 2013-09-24 04:09:58 -04:00
Andrew Kelley
28174551a9 library scanning with libgroove 2013-09-20 02:40:43 -04:00
Josh Wolfe
55c5c088e4 starting to read metadata tags with groove 2013-09-18 23:04:03 -07:00
Andrew Kelley
d32fff0a3f mpd killing rampage 2013-09-18 18:34:05 -04:00
Andrew Kelley
4226183402 update TODO 2013-09-05 19:32:44 -04:00
Andrew Kelley
653148cd39 cleanup 2013-09-05 11:15:07 -04:00
Andrew Kelley
f60b414852 client side: extract streaming code into separate file 2013-09-05 08:46:24 -04:00
Andrew Kelley
77c3f6b831 update and remove some dependencies 2013-09-05 07:03:30 -04:00
Andrew Kelley
f0a8309f84 add certain file extensions to library always
regardless of whether they were identified as audio files
2013-09-05 06:51:56 -04:00
Andrew Kelley
317d7dc7fb update TODO 2013-09-05 06:38:59 -04:00
Andrew Kelley
f13e58155e playerclient cleanup 2013-09-04 16:13:34 -04:00
Andrew Kelley
493d7d26f2 playerclient passes jshint 2013-09-04 16:06:43 -04:00
Andrew Kelley
454a326442 ditch jspackage in favor of browserify 2013-09-04 15:52:48 -04:00
Andrew Kelley
cdf4068ef7 support WMA files. include files in library even if missing metadata 2013-09-04 13:18:37 -04:00
Andrew Kelley
77c74e34ac add TODO 2013-08-30 00:50:37 -04:00
Andrew Kelley
e490c171c2 get duration from mpd on playlist update 2013-08-12 09:37:00 -04:00
Andrew Kelley
dd22d6465e refactor PlayerServer - separate playback logic from client commands 2013-08-12 00:28:55 -04:00
Andrew Kelley
13edb5c3aa lots of refactoring
* plugins are structured as in mineflayer
   - lib/plugin.js is deleted
   - download, delete, lastfm, upload, dynamicmode disabled until they
     can be updated to work directly with PlayerServer
 * GrooveBasin class represents the main server
 * Killer is cleaned up
 * Library does not respond to bus; instead is constructed with
   musicLibPath
 * MpdConf is cleaned up
 * MpdParser is deleted in favor of mpd.js module
 * lib/player.js is deleted. wtf was that doing there?
 * server-side PlayerClient is deleted
 * PlayerServer is cleaned up a bit. ability to directly proxy mpd
   requests is deleted.
2013-08-11 18:54:19 -04:00
Andrew Kelley
e1da9b3339 cleaning up 2013-08-11 13:28:02 -04:00
Andrew Kelley
704d1bbb3c clean up MpdParser 2013-08-11 01:36:41 -04:00
Andrew Kelley
8b9923c07e clean up server.js 2013-08-11 01:25:25 -04:00
Josh Wolfe
b3876c4c0e added TODO 2013-08-10 21:13:21 -07:00
Josh Wolfe
a6ad49ca53 javascriptizing the coco OOP in plugins and passing lint 2013-08-10 21:02:09 -07:00
Andrew Kelley
2b4adcd8c5 use zfill module 2013-08-10 22:19:29 -04:00
Josh Wolfe
9e97651153 disambiguating sort keys in the server 2013-08-10 19:15:16 -07:00
Josh Wolfe
2f8c64bc77 privatizing methods in PlayerServer 2013-08-10 18:29:39 -07:00
Josh Wolfe
d7e8f875fa localizing var declarations 2013-08-10 18:15:16 -07:00
Andrew Kelley
b9e1101c54 use musicmetadata module instead of mutagen 2013-08-10 21:10:50 -04:00
Josh Wolfe
b1d7750da7 app.js almost passes lint 2013-08-10 17:35:52 -07:00
Josh Wolfe
1e31ce3651 playerclient.js almost passes lint 2013-08-10 17:24:34 -07:00
Josh Wolfe
3b91ee6514 cleaning up server.js and playerserver.js 2013-08-10 17:15:54 -07:00
Andrew Kelley
2b897818d0 bye bye coco-land. it was fun while it lasted 2013-08-10 19:26:29 -04:00
Andrew Kelley
5d332aef87 lock handlebars version 2013-08-10 18:41:13 -04:00
Josh Wolfe
fa27a28fac storing is_random in playlist items 2013-05-22 10:13:14 -07:00
Josh Wolfe
7b5f7b6f41 using sort keys instead of sorting by id 2013-05-16 00:25:05 -07:00
Josh Wolfe
92c40b661c fixing play with no current item 2013-05-01 09:14:06 -07:00
Josh Wolfe
c2488d6ca2 fixing skip and stop not resetting seek position 2013-05-01 07:52:32 -07:00
Josh Wolfe
1cd7b916ad fixing some fraction id things 2013-04-28 10:32:33 -07:00
Josh Wolfe
067e40e288 fixing next/previous. breaking random_ids. cleaning up queueFiles interface. 2013-04-28 10:12:56 -07:00
Josh Wolfe
77fae4847a pause and seek work. next track plays after current track. 2013-04-28 06:31:47 -07:00
Josh Wolfe
33bd38d333 noticing when mpd stops playing 2013-04-27 14:55:08 -07:00
Josh Wolfe
8c25b9056f fixing song time duration display 2013-04-02 09:59:54 -07:00
Josh Wolfe
ffa639b126 mpd plays 1 song, and then stops 2013-04-01 00:17:05 -07:00
Josh Wolfe
058768262d WIP. the playlist is a lie 2013-03-31 21:50:39 -07:00
Josh Wolfe
1acb3d4345 a single instance of PlayerServer serves all the clients 2013-03-31 02:29:34 -07:00
Josh Wolfe
4f184bedef renaming player.co to playerclient.co 2013-03-31 01:05:33 -07:00
Josh Wolfe
b1afc21542 fixing dynamic mode again 2013-03-31 00:57:19 -07:00
Josh Wolfe
0d4200b7ba renaming Player to PlayerClient. moving trackNameFromFile to futils. 2013-03-31 00:25:44 -07:00
Josh Wolfe
9026ca64b9 DRY, and fixing a startup race condition 2013-02-06 01:33:06 -07:00
Josh Wolfe
dd3b94b395 cleaning up mpdparser 2013-02-04 22:16:19 -07:00
Josh Wolfe
ae4d4ece5a removing console.log 2013-02-03 17:51:17 -07:00
Josh Wolfe
4251c21504 moving accounts out of mpd_conf. fixing some startup race conditions. 2013-02-03 17:44:19 -07:00
Josh Wolfe
8151a09535 the localhost:16244 mpd connection is only for debugging 2013-02-03 15:57:29 -07:00
Josh Wolfe
f3fe53f8c6 everyone shares a single mpd connect 2013-02-03 14:37:37 -07:00
Josh Wolfe
6caabeb2b7 fixing authentication events 2013-02-03 12:58:49 -07:00
Josh Wolfe
de63299146 PlayerServer does its own permission checking 2013-02-03 12:26:26 -07:00
Josh Wolfe
83dcfa22ec converting moveid to new protocol 2013-01-27 22:13:05 -07:00
Josh Wolfe
7884a544c4 converting some commands to use objects instead of mpd strings 2013-01-27 21:03:46 -07:00
Josh Wolfe
9df89da399 more sendCommands cleanup 2013-01-27 20:00:26 -07:00
Josh Wolfe
9b83c23ee0 simplifying calls to sendCommands where possible 2013-01-27 19:16:50 -07:00
Josh Wolfe
4ab273e09c Merge remote-tracking branch 'origin/master' into mutagen 2013-01-27 14:56:55 -07:00
Josh Wolfe
72f359c358 clients and server both use socket.io for Player objects 2013-01-27 14:51:27 -07:00
Andrew Kelley
8090be3fa3 fix readme screenshots 2013-01-08 13:47:20 -05:00
Josh Wolfe
a9226285b1 Player callbacks can be out of order 2013-01-06 18:55:27 -07:00
Josh Wolfe
8149bbab11 fixing authentication race condition 2013-01-06 15:15:57 -07:00
Josh Wolfe
bcd0dd62b9 using proper length field from mutagen 2013-01-04 03:40:16 -07:00
Josh Wolfe
22def50783 moving mpdparser into this project. getting rid of mpd.js. 2013-01-04 03:19:08 -07:00
Josh Wolfe
2cb1a5de9c using mutagen tags instead of mpd tags 2013-01-04 03:12:32 -07:00
Josh Wolfe
3e2a86ff6c library is too important to be a plugin 2013-01-04 01:34:57 -07:00
Josh Wolfe
c4de6edf44 adding a PlayerServer layer that does nothing 2013-01-02 00:50:43 -07:00
Josh Wolfe
7114b72176 using a better mutagen api 2013-01-01 21:46:51 -07:00
Josh Wolfe
f636e7d6c7 mergining, and reading all the tags with no errors 2013-01-01 18:31:25 -07:00
Josh Wolfe
6940043de2 cleanup 2012-12-28 22:40:35 -07:00
Josh Wolfe
171e741138 mpd.js parses currentsong 2012-12-28 21:32:49 -07:00
Josh Wolfe
25902935c5 parser parses status and playlistinfo 2012-12-28 21:13:37 -07:00
Josh Wolfe
d45a4ecad3 add search tags to tracks outside the parser 2012-12-28 20:36:44 -07:00
Josh Wolfe
790a1d3689 mpd.js parses lsinfo 2012-12-28 20:04:03 -07:00
Josh Wolfe
8336da00da mpd.js parses listallinfo for us 2012-12-28 19:50:00 -07:00
Josh Wolfe
041be54efa MpdParser takes a socket 2012-12-28 16:23:02 -07:00
Josh Wolfe
d15f0792fa containing the idle/noidle pattern in the MpdParser 2012-12-28 16:15:18 -07:00
Josh Wolfe
42e8b9bc40 Library should really be called Player 2012-12-28 12:47:59 -07:00
Josh Wolfe
db1a14b0b1 Library should really be called Player 2012-12-28 12:45:51 -07:00
Josh Wolfe
cb4b0302c8 using mpd.js as just a parser 2012-12-28 12:38:58 -07:00
Josh Wolfe
eb24d4827e rename the old mpd class to Library 2012-12-28 11:06:16 -07:00
Josh Wolfe
38722a72ec put the old mpd class in groovebasin land 2012-12-28 10:57:24 -07:00
Josh Wolfe
e691b6cf02 fixing audio_alsa mpd.conf generation 2012-12-19 03:46:09 -07:00
Andrew Kelley
48bd88b27a fix npm stop and npm deploy commands 2012-10-25 14:47:51 -04:00
Andrew Kelley
c789febe7f simplify upload capability and plug security hole 2012-10-24 10:08:12 -04:00
Andrew Kelley
c8afc5b569 prevent server from connecting to mpd twice 2012-10-24 10:07:54 -04:00
Andrew Kelley
55988b45de fix playlist to display artist name 2012-10-23 20:13:31 -04:00
Andrew Kelley
8c69710f7f fix regression for handling unknown artist/album 2012-10-23 19:52:16 -04:00
Andrew Kelley
f88758bb1a add dates to release notes 2012-10-18 14:01:06 -04:00
Andrew Kelley
bd51ef0647 0.2.0 release notes 2012-10-18 13:56:48 -04:00
Andrew Kelley
1f16013436 fix previous commit 2012-10-18 12:04:59 -04:00
Andrew Kelley
a35e3dbcf9 fix media urls when enabling/disabling download plugin 2012-10-18 12:02:18 -04:00
Andrew Kelley
297e031555 groove basin is in charge of mpd.conf and starting mpd
closes #85
2012-10-18 11:21:27 -04:00
Andrew Kelley
ba5a955053 Release 0.2.0 2012-10-16 17:00:05 -04:00
Andrew Kelley
6fb72a7f1a fix .npmignore to not ignore public 2012-10-16 16:56:15 -04:00
Andrew Kelley
fd8ac91623 fix crash when .state.json doesn't exist 2012-10-16 16:29:34 -04:00
Andrew Kelley
61a1474ef2 add color to the first part of the track slider. closes #15 2012-10-15 10:19:22 -04:00
Andrew Kelley
8e22ebbee9 upload files through express instead of formidable directly 2012-10-14 03:38:17 -04:00
Andrew Kelley
ca3183458c don't bold menu option text 2012-10-12 01:01:51 -04:00
Andrew Kelley
09eafbcf36 fix lingering reference to fs-extra 2012-10-12 00:52:38 -04:00
Andrew Kelley
5adea466bc refactor download plugin 2012-10-12 00:17:37 -04:00
Andrew Kelley
f53b878869 use mv module instead of hand rolling moveFile 2012-10-11 23:38:41 -04:00
Andrew Kelley
b8275a4170 use mkdirp instead of fs-extra 2012-10-11 21:49:18 -04:00
Josh Wolfe
88d96dc11c using mutagen to read some tags 2012-10-11 05:57:53 -07:00
Andrew Kelley
7ad7e21a0a use superagent instead of request. closes #59 2012-10-11 08:33:19 -04:00
Andrew Kelley
a596786a58 be more careful about resizing things. closes #75 2012-10-10 19:11:20 -04:00
Andrew Kelley
528653d2d5 cache more jQuery objects. refactor some tab things 2012-10-10 18:26:47 -04:00
Andrew Kelley
ffa5c4dd75 remove the track number from stored playlist items 2012-10-10 10:07:57 -04:00
Andrew Kelley
90fba721c4 add uploaded songs to "Incoming" playlist. closes #80 2012-10-09 22:08:23 -04:00
Andrew Kelley
2909750c9d stored playlist ui works as far as the mouse is concerned 2012-10-09 22:06:17 -04:00
Andrew Kelley
b229318c77 update mpd. fixes playlist display when empty 2012-10-09 22:06:17 -04:00
Andrew Kelley
9cd649507f further genericize library's ui 2012-10-09 22:06:17 -04:00
Andrew Kelley
abca154122 wip 2012-10-09 22:06:17 -04:00
Andrew Kelley
bef0b5940b fix regression: dynamic mode not restoring state 2012-10-09 19:29:26 +02:00
Andrew Kelley
ebdcc6aa7d fix upload regression 2012-10-09 19:26:03 +02:00
Andrew Kelley
431e95fc37 toggle expansion for playlists works 2012-10-09 00:37:38 -04:00
Andrew Kelley
60fed15769 genericize toggleExpansion 2012-10-09 00:04:00 -04:00
Andrew Kelley
d5ba226320 refactor library ui to use a generic tree ui 2012-10-08 23:23:09 -04:00
Andrew Kelley
ffb718d6a4 organize setUpUi a bit 2012-10-08 22:51:31 -04:00
Andrew Kelley
03a3d7fca9 style stored playlists better 2012-10-08 22:36:16 -04:00
Andrew Kelley
736de99f05 fix regressions, merge all js into app.js, and list stored playlists 2012-10-08 22:25:07 -04:00
Andrew Kelley
e79f3f647c simplify download plugin 2012-10-08 20:53:37 -04:00
Andrew Kelley
f5c2b8b054 refactor the way localStorage is used 2012-10-08 20:37:10 -04:00
Andrew Kelley
31e266d77e use function where appropriate 2012-10-08 19:55:33 -04:00
Andrew Kelley
ff8e794587 hush auto return where appropriate 2012-10-08 19:41:54 -04:00
Andrew Kelley
3cca9f2e26 send a 404 when downloading can't find artist or album. closes #70 2012-10-08 03:23:35 -04:00
Andrew Kelley
1f125afa64 better error handling if getFileInfo is not supported 2012-10-08 03:07:20 -04:00
Andrew Kelley
2a1f171a78 app.coffee -> coco 2012-10-08 03:06:22 -04:00
Andrew Kelley
e5f399c30a combine css 2012-10-07 20:44:00 -04:00
Andrew Kelley
4bcd76c3ad update mpd.js - clean up API interface 2012-10-07 19:53:18 -04:00
Andrew Kelley
eb23c8bc4a make toHtmlId faster 2012-10-06 04:16:00 -04:00
Andrew Kelley
27649f6838 socketmpd.coffee -> coco 2012-10-06 04:12:45 -04:00
Andrew Kelley
ca98326cb8 util.coffee -> util.co 2012-10-06 04:04:26 -04:00
Andrew Kelley
ec458146de server no longer depends on coffee-script 2012-10-06 03:42:43 -04:00
Andrew Kelley
2b05602357 dynamicmode -> coco 2012-10-06 03:41:49 -04:00
Andrew Kelley
c80bbffb85 last.fm -> coco. fix parseInt parsing as octal 2012-10-06 03:21:02 -04:00
Andrew Kelley
ce0dcee396 upload -> coco.
also don't crash if temp filesystem is across boundary of library
2012-10-06 03:04:01 -04:00
Andrew Kelley
b50ec33d6c Cakefile -> Cokefile 2012-10-06 02:31:29 -04:00
Andrew Kelley
3ab3b52745 put link to stream URL in settings. closes #69 2012-10-06 02:21:01 -04:00
Andrew Kelley
fff4566018 disable x-powered-by 2012-10-06 01:50:45 -04:00
Andrew Kelley
f07a2292c4 update to express 3.0.0rc5 2012-10-06 01:47:14 -04:00
Andrew Kelley
212d1b3388 update stylus to 0.29.0 2012-10-06 01:35:49 -04:00
Andrew Kelley
dc293281e2 update to handlebars 1.0.7 2012-10-06 01:31:32 -04:00
Andrew Kelley
880569f9c4 update to latest version of some packages 2012-10-06 01:30:51 -04:00
Andrew Kelley
e37f483259 fix Cakefile using a module we don't have 2012-10-06 01:30:36 -04:00
Andrew Kelley
696ab6ae13 fix race condition when building from clean checkout 2012-10-06 01:26:33 -04:00
Andrew Kelley
3e9eb5244c update to node-dev 2.8 - fixes run dev 2012-10-06 01:05:24 -04:00
Andrew Kelley
8108cd7d88 handle uploading errors. fixes #59 2012-10-05 21:15:09 -04:00
Andrew Kelley
65e6c01461 update mpd.js. closes #19 2012-10-05 20:34:14 -04:00
Andrew Kelley
90a5b9ed2a setEncoding('utf8') on sockets and stop using toString. closes #67
cleaner code, remove potential for glitches
2012-10-05 20:11:03 -04:00
Andrew Kelley
bccd690fd4 log chats 2012-10-05 10:53:49 -04:00
Andrew Kelley
9f600dffea upgrade jquery to 1.8.2. closes #64 2012-10-05 10:45:51 -04:00
Andrew Kelley
22d68befc3 add npm run reload 2012-10-05 10:24:08 -04:00
Andrew Kelley
1bda313393 better error reporting when state json file is corrupted 2012-10-05 10:24:00 -04:00
Andrew Kelley
e5f3febf6d ability to download arbitrary playlist selections. closes #9 2012-10-05 10:16:06 -04:00
Andrew Kelley
55a0cf0f59 ability to download arbitrary selection in library as zip. #9 2012-10-05 09:50:28 -04:00
Andrew Kelley
8ffa8ddf39 use event.which instead of event.button and event.keyCode 2012-10-04 22:25:47 -04:00
Andrew Kelley
e35a778e3e upgrade mpd.js. closes #65 2012-10-04 22:13:42 -04:00
Andrew Kelley
18c1743618 downloading albums and artists as zip. #9
* refactor file utils
* download -> coco
* fix plugin framework thinking swap files are plugins
2012-10-04 22:10:50 -04:00
Andrew Kelley
c5acea0572 make bootup more async 2012-10-04 18:00:24 -04:00
Andrew Kelley
fe39aa4501 bootup code is more async 2012-10-04 16:32:56 -04:00
Andrew Kelley
525d5072a2 mpdconf -> coco 2012-10-04 16:18:55 -04:00
Andrew Kelley
76b8f602a3 don't write state to disk multiple times per event loop 2012-10-04 16:11:46 -04:00
Andrew Kelley
d856321789 stream: hush auto return 2012-10-04 16:11:35 -04:00
Andrew Kelley
f6ea244c44 chat -> coco 2012-10-04 16:11:26 -04:00
Andrew Kelley
89a1865fe5 stream plugin -> coco 2012-10-04 15:10:09 -04:00
Andrew Kelley
872dca5916 more robust walk 2012-10-04 15:06:50 -04:00
Andrew Kelley
fbb53ee861 remove code duplication 2012-10-04 15:05:29 -04:00
Andrew Kelley
8c64fa74ec use fs-extra instead of rolling our own moveFile 2012-10-04 14:56:38 -04:00
Andrew Kelley
5dfa9c8a24 reorganize with async and middleware 2012-10-04 14:42:09 -04:00
Andrew Kelley
8f1a2d685e changes to plugin initialization process 2012-10-04 14:22:37 -04:00
Andrew Kelley
cb31102976 hush implicit return where applicable 2012-10-04 13:02:38 -04:00
Andrew Kelley
97cd77dab5 organize server.co - shuffle code around 2012-10-04 12:56:15 -04:00
Andrew Kelley
3f228e51d3 get rid of node.extend dependency and remove setuid feature 2012-10-04 12:36:51 -04:00
Andrew Kelley
ded94c9e2a convert server.coffee to coco 2012-10-04 12:09:53 -04:00
Andrew Kelley
b31531722d don't rely on socket.io's log 2012-10-04 10:59:32 -04:00
Andrew Kelley
afffbed6eb avoid race condition on first time running watch 2012-10-04 10:15:57 -04:00
Josh Wolfe
5e177ae037 uploading with mpd <0.17 falls back to upload name 2012-10-04 04:39:12 -07:00
Josh Wolfe
4cd519089d fix dynamic mode with no library or no tags file 2012-10-04 04:38:25 -07:00
Andrew Kelley
994f84acc6 update demo url 2012-10-02 00:46:11 -04:00
Andrew Kelley
d8e0cecd20 use git submodule for client side mpd.js 2012-10-02 00:18:38 -04:00
Andrew Kelley
5e70b17795 avoid crashing when root pass is not set 2012-10-02 00:15:57 -04:00
Andrew Kelley
e45fc6a9fb move configuration to environment variables. use naught. 2012-10-02 00:11:47 -04:00
Andrew Kelley
73e0cb0961 better way to develop mpd.js and groovebasin at the same time 2012-10-01 23:13:00 -04:00
Josh Wolfe
45dcded470 only scrub random ids if we're sure we're not still loading 2012-08-13 21:28:01 -07:00
Andrew Kelley
36d20e2c45 mpd => 0.3.3. fixes playlist not rendering 2012-08-11 02:00:23 -06:00
Andrew Kelley
3f5f96ceb5 move TODO to github issues 2012-08-09 23:32:37 -04:00
Andrew Kelley
76c715255b mpd.js => 0.3.2. closes #29 2012-08-09 23:00:42 -04:00
Andrew Kelley
e7cc261be0 update launching instructions 2012-08-09 22:29:50 -04:00
Andrew Kelley
6b70bbc1c3 fix build script to not hang 2012-08-09 22:27:52 -04:00
Andrew Kelley
599d1e0ad5 fix dynamicmode; use higher level sticker api. closes #22 2012-08-09 22:20:54 -04:00
Andrew Kelley
bf531b39a0 mpd => 0.3.0 2012-08-09 20:19:37 -04:00
Andrew Kelley
2c1222254f mpd => 0.2.0 2012-08-09 17:54:03 -04:00
Andrew Kelley
5387067af0 depend on mpd.js as a module. closes #25 2012-08-09 17:26:44 -04:00
Andrew Kelley
9d7904cd90 use is instead of == 2012-08-09 09:41:11 -04:00
Andrew Kelley
725ef9659d move some TODO items to github issues 2012-08-08 19:10:07 -04:00
Andrew Kelley
6d682593f5 better connection error messages. closes #21 2012-08-08 18:44:26 -04:00
Andrew Kelley
7a3fdfaab3 update style to not resize on selection. closes #23 2012-08-08 18:02:46 -04:00
Andrew Kelley
67b7b81375 ability to import songs by pasting a URL 2012-08-08 13:02:25 -04:00
Andrew Kelley
490227b168 npm build script 2012-08-07 03:02:36 -04:00
Andrew Kelley
d7a57bd342 use express instead of node-static 2012-08-07 02:28:28 -04:00
Andrew Kelley
027fde923b move imgs to img/ 2012-08-07 01:46:06 -04:00
Andrew Kelley
3f04a55552 socket.io => 0.9.9 2012-08-07 01:37:54 -04:00
Andrew Kelley
09b26d0312 update handlebars to 1.0.6-2 2012-08-07 01:28:23 -04:00
Andrew Kelley
efc3880ebf build and development process is now a no-brainer 2012-08-07 01:16:38 -04:00
Andrew Kelley
6e49bb5617 0.1.2 release notes 2012-07-17 17:44:21 -04:00
Andrew Kelley
6fce8e21f6 remove server.js softlink from build 2012-07-12 01:54:54 -04:00
Andrew Kelley
94663d5617 version bump to 0.1.2 2012-07-12 01:50:39 -04:00
102 changed files with 9646 additions and 10493 deletions

17
.gitignore vendored
View file

@ -1,12 +1,7 @@
Makefile
public/library
node_modules/
*.tmp
.build.timestamp
.state.json
/node_modules
/groovebasin.db
/config.js
# generated code below here
public/app.js
public/app.css
server.js
lib/
# not shared with .npmignore
/public/app.js
/public/app.css

74
.jshintrc Normal file
View file

@ -0,0 +1,74 @@
{
// Settings
"passfail" : false, // Stop on first error.
"maxerr" : 100, // Maximum errors before stopping.
// Predefined globals whom JSHint will ignore.
"browser" : true, // Standard browser globals e.g. `window`, `document`.
"node" : true,
"predef" : [
"setImmediate",
"clearImmediate"
],
"rhino" : false,
"couch" : false,
"wsh" : false, // Windows Scripting Host.
"jquery" : false,
"prototypejs" : false,
"mootools" : false,
"dojo" : false,
// Development.
"debug" : true, // Allow debugger statements e.g. browser breakpoints.
"devel" : true, // Allow development statements e.g. `console.log();`.
// EcmaScript 5.
"es5" : true, // Allow EcmaScript 5 syntax.
"strict" : false, // Require `use strict` pragma in every file.
"globalstrict" : true, // Allow global "use strict" (also enables 'strict').
// The Good Parts.
"asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons).
"laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons.
"laxcomma" : true,
"bitwise" : false, // Prohibit bitwise operators (&, |, ^, etc.).
"boss" : true, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments.
"curly" : false, // Require {} for every new block or scope.
"eqeqeq" : true, // Require triple equals i.e. `===`.
"eqnull" : true, // Tolerate use of `== null`.
"evil" : false, // Tolerate use of `eval`.
"expr" : false, // Tolerate `ExpressionStatement` as Programs.
"forin" : false, // Prohibt `for in` loops without `hasOwnProperty`.
"immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );`
"latedef" : false, // Prohibit variable use before definition.
"loopfunc" : false, // Allow functions to be defined within loops.
"noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`.
"regexp" : false, // Prohibit `.` and `[^...]` in regular expressions.
"regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`.
"scripturl" : false, // Tolerate script-targeted URLs.
"shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`.
"supernew" : false, // Tolerate `new function () { ... };` and `new Object;`.
"undef" : true, // Require all non-global variables be declared before they are used.
// Persone styling prefrences.
"newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`.
"noempty" : true, // Prohibit use of empty blocks.
"nonew" : true, // Prohibit use of constructors for side-effects.
"nomen" : false, // Prohibit use of initial or trailing underbars in names.
"onevar" : false, // Allow only one `var` statement per function.
"plusplus" : false, // Prohibit use of `++` & `--`.
"sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`.
"trailing" : true, // Prohibit trailing whitespaces.
"white" : false // Check against strict whitespace and indentation rules.
}

View file

@ -1,12 +1,5 @@
Makefile
public/library
node_modules/
*.tmp
.build.timestamp
.state.json
/node_modules
/groovebasin.db
/config.js
# not shared with .gitignore
Cakefile
src/
README.md
TODO

126
Cakefile
View file

@ -1,126 +0,0 @@
fs = require("fs")
path = require("path")
# returns a list of all files in the folder and subfolders
walk = (start, test) ->
results = []
processDir = (dir) ->
names = fs.readdirSync(dir)
for name in names
file_path = "#{dir}/#{name}"
stat = fs.statSync(file_path)
if stat.isDirectory()
processDir file_path
else
results.push file_path
processDir start
results
# explicit list of client src files, in dependency order
client_src_files = [
"src/client/util.coffee"
"src/shared/mpd.coffee"
"src/client/socketmpd.coffee"
"src/client/app.coffee"
]
makeMakefile = (o) ->
"""
# input
client_src=#{o.client_src_files}
server_src=src/server/server.coffee
styles=src/client/app.styl
# output
appjs=public/app.js
appcss=public/app.css
serverjs=server.js
# compilers
coffee=./node_modules/coffee-script/bin/coffee
handlebars=./node_modules/handlebars/bin/handlebars
stylus=./node_modules/stylus/bin/stylus
.PHONY: build clean watch
SHELL=bash
build: $(serverjs) $(appjs) $(appcss) #{o.server_js_files}
\t@: # suppress "Nothing to be done" message.
#{o.server_js_rules}
$(serverjs): ./lib/server.js
\tln -sf ./lib/server.js $(serverjs)
$(appjs): #{o.view_files} #{o.client_src_files}
\t$(handlebars) #{o.view_files} >$@.tmp
\tfor f in $(client_src); do $(coffee) -p -c $$f >>$@.tmp; done
\tmv $@{.tmp,}
$(appcss): $(styles)
\t$(stylus) <$(styles) >$@.tmp
\tmv $@{.tmp,}
clean:
\trm -f ./$(appjs){,.tmp}
\trm -f ./$(appcss){,.tmp}
\trm -f ./$(serverjs){,.tmp}
\trm -rf ./lib
\trm -f ./public/library
\trm -f ./Makefile
"""
makeJsRule = (src, dest) ->
"""
#{dest}: #{src}
\tmkdir -p #{path.dirname(dest)}
\t$(coffee) -cbj #{dest} #{src}
"""
{spawn} = require("child_process")
exec = (cmd, args=[], cb=->) ->
bin = spawn(cmd, args)
bin.stdout.on 'data', (data) ->
process.stdout.write data
bin.stderr.on 'data', (data) ->
process.stderr.write data
bin.on 'exit', cb
changeExtension = (filename, new_ext) ->
ext = path.extname(filename)
new_path = filename.substring(0, filename.length - ext.length)
new_path + new_ext
configure = ->
js_rules = []
js_files = []
for src in walk("./src/server").concat(walk("./src/shared"))
if /\.coffee$/.test(src)
dest = changeExtension(src, ".js").replace("./src/server/", "./lib/").replace("./src/shared/", "./lib/")
js_rules.push(makeJsRule(src, dest))
js_files.push(dest)
view_files = (f for f in walk("./src/client/views") when /\.handlebars$/.test(f))
makefile = makeMakefile
view_files: view_files.join(" ")
server_js_rules: js_rules.join("\n")
server_js_files: js_files.join(" ")
client_src_files: client_src_files.join(" ")
fs.writeFileSync "./Makefile", makefile, 'utf8'
build = -> exec "make"
clean = -> exec "make", ["clean"]
task "build", ->
configure()
build()
task "clean", ->
configure()
clean()
task "configure", ->
configure()

317
README.md Normal file
View file

@ -0,0 +1,317 @@
# Groove Basin
Music player server with a web-based user interface inspired by Amarok 1.4.
Run it on a server (such as a 512MB
[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
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/).
## Features
* Fast, responsive UI. It feels like a desktop app, not a web app.
* Dynamic playlist mode which automatically queues random songs, favoring
songs that have not been queued recently.
* Drag and drop upload. Drag and drop playlist editing. Rich keyboard
shortcuts.
* Lazy multi-core
[EBU R128 loudness scanning](http://tech.ebu.ch/loudness) (tags compatible
with [ReplayGain](http://wiki.hydrogenaudio.org/index.php?title=ReplayGain_1.0_specification))
and automatic switching between track and album mode.
["Loudness Zen"](http://www.youtube.com/watch?v=iuEtQqC-Sqo)
* Streaming support. You can listen to your music library - or share it with
your friends - even when you are not physically near your home speakers.
* MPD protocol support. This means you already have a selection of
[clients](http://mpd.wikia.com/wiki/Clients) which integrate with Groove Basin.
For example [MPDroid](https://github.com/abarisain/dmix).
* [Last.fm](http://www.last.fm/) scrobbling.
* File system monitoring. Add songs anywhere inside your music directory and
they instantly appear in your library in real time.
* Supports GrooveBasin Protocol on the same port as MPD Protocol - use the
`protocolupgrade` command to upgrade.
## 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.
2. Install [libgroove](https://github.com/andrewrk/libgroove).
3. Clone the source.
4. `npm run build`
5. `npm start`
## Screenshots
![Search + drag/drop support](http://superjoesoftware.com/temp/groove-basin-0.0.4.png)
![Multi-select and context menu](http://superjoesoftware.com/temp/groove-basin-0.0.4-lib-menu.png)
![Keyboard shortcuts](http://superjoesoftware.com/temp/groove-basin-0.0.4-shortcuts.png)
![Last.fm Scrobbling](http://superjoesoftware.com/temp/groove-basin-0.0.4-lastfm.png)
## Configuration
When Groove Basin starts it will look for `config.js` in the current directory.
If not found it creates one for you with default values.
## Developing
```
$ 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
2. Music library organization
3. Accoustid Integration
4. Playlists
5. User accounts / permissions rehaul
6. Event history / chat
7. Finalize GrooveBasin protocol spec
## Release Notes
### 1.0.1 (Mar 18 2014)
* Andrew Kelley:
* Fix race condition when removing tracks from playlist. Closes #160
* Default import path includes artist directory.
* Also recognize "TCMP" ID3 tag as compilation album flag
* Fix Last.fm authentication
### 1.0.0 (Mar 15 2014)
* Andrew Kelley:
* Remove dependency on MPD. Groove Basin now works independently of MPD.
It uses [libgroove](https://github.com/andrewrk/libgroove) for audio
playback and streaming support.
* Support MPD protocol on (default) port 6600. Groove Basin now functions as
an MPD server.
* Fix regression for handling unknown artist/album
* Fix playlist to display artist name
* Plug upload security hole
* Groove Basin is no longer written in coco. Hopefully this will enable more
code contributions.
* Simpler config file that can survive new version releases.
* Simpler and more efficient protocol between client and server.
* Pressing prev on first track with repeat all on goes to end
* Automatic loudness detection (ReplayGain) using EBU R128.
- Lazy playlist scanning.
- Automatic switching between album and track mode.
- Takes advantage of multi-core systems.
* Faster rebuilding of album table index
* HTTP audio stream buffers much more quickly and flushes the buffer on seek.
* Fix volume ui going higher than 1.0.
* Fix changing volume not showing up on other clients.
* Native html5 audio streaming instead of soundmanager 2
* Streaming shows when it is buffering
* add meta charset=utf8 to index.html.
* fix volume keyboard shortcuts in firefox.
* Watches music library for updates and quickly updates library.
* Route dynamicmode through permissions framework
* Better default password generation
* web ui: fix current track not displayed sometimes
* upgrade jquery and jquery ui to latest stable. Fixes some UI glitches.
* static assets are gzipped and held permanently in memory. Makes the
web interface load faster.
* player: set "don't cache this" headers on stream
* Remove chat. It's not quite ready yet. Chat will be reimplemented better
in a future release.
* Remove stored playlist stub from UI. Stored playlists will be reimplemented
better in a future release.
* Josh Wolfe:
* Converting the code to not use MPD
* fix multiselect shiftIds
* deleting library items removes them from the queue as well.
* fix shift click going up in the queue
* after deleting tracks, select the next one, not some random one.
### 0.2.0 (Oct 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 (Jul 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 (Apr 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 (Mar 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 (Mar 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 (Mar 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 (Mar 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

85
TODO
View file

@ -1,85 +0,0 @@
* ability to delete songs from library
- pre-emptively remove from library to have a speedy response time
* status updates screw up the settings UI
* should send authentication before getting the library info
* butter ui for authentication in settings
Version 0.0.7
* bug: if you select the last track in blink 182 enema of the state and press
down, it goes to the wrong track next
* if you move a blue song such that it touches a non-blue song, it should lose
its blueness
* ability to ban from random
- keyboard shortcut 'B'
* ability to mark a playlist item as "stop after this track"
Version 0.0.8
* ability to password protect - make it so that not everybody can kill mpd,
enable/disable audio outputs, edit tags, upload tracks, change playback
state, etc.
- disable buttons that we lack permission to press
- upload only available if you have `add` permission
* ability to edit tags
* ability to move a file to a better location based on its tags
Backlog
* streaming button should show buffering percentage
* when uploading songs into library, make sure not to overwrite existing ones
* left in library on collapsed element should jump to parent
* right in library after expanding element should jump to 1st child
* online user interaction:
- join/left messages
- user is typing notices (typically this is not done in a room with > 2 people)
- attention grabbers for chat activity
- surround chatted urls with <a>
- queued tracks should know which user queued them
> displayed as colors?
- library songs should know which user uploaded them if any
> this would require user authentication :-/
- server-side chat logs
* shortcuts window should be scrollable with arrows
* shortcuts window doesn't close with Escape after a Ctrl+F in Chrome
* Time column disappears when window is too thin
* Time column can cut off part of 4-digit times
* option to not auto-queue uploaded songs
* smarter anticipation of commands to avoid the glitchy behavior when you do
a repetitive action quickly, such as turning the volume down incrementally
or moving a track down one space at a time
* make the list of uploaded files go away after they're all done.
* as part of lower casing for artist/album keys, change ñ to n, etc.
- like http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/api/core/org/apache/lucene/analysis/ASCIIFoldingFilter.html
* shift+down select another item, shift+up unselect it
* hold ctrl to move cursor without selection
- use alt for moving tracks up/down
* dynamic mode populates twice when user clicks Clear
- (due to mpd 'player' and 'playlist' events both being handled with empty playlist)
* display any mpd status error message
* ability to download multiple songs at once. Issue #9.
* library management
- duplicate detection and elimination
> if a song is byte for byte the same (check md5's) then ignore the
new song
> use heuristics to guess if songs are probably the same (using tags). if
we are reasonably confident that the songs are the same, delete the one
with the lower quality.
> if we're not confident enough, there will be an
api that lists possible duplicates and actions to resolve them.
- when a song is added to the library, automatically replaygain scan it
and the album from whence it came. Do this for update as well.
* ability to add song to library by URL
* take mpd's status into account. Make them editable?
- consume
- random
* prepend '!' to a search word to NOT match the word. '\!' to literally match
'!'. '\\' to literally match '\'
* ability to upload zip files
* ability to upload via a url
* ability to filter playlist
* playlist management
- save
- display
- grab individual tracks
- switch to
* make dynamic playlist mode options configurable
* ability to import songs to library from youtube URL
* file folder inbox to import stuff

4
build Executable file
View file

@ -0,0 +1,4 @@
#!/bin/bash
mkdir -p public
./node_modules/.bin/stylus -o public/ -c --include-css src/client/styles
./node_modules/.bin/browserify src/client/app.js --outfile public/app.js

3
docs/.gitignore vendored Normal file
View file

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

287
docs/changelog.rst Normal file
View file

@ -0,0 +1,287 @@
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 Normal file
View file

@ -0,0 +1,242 @@
# -*- 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'

69
docs/guides/main.rst Normal file
View file

@ -0,0 +1,69 @@
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.

29
docs/index.rst Normal file
View file

@ -0,0 +1,29 @@
.. 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`

96
lib/deduped_queue.js Normal file
View file

@ -0,0 +1,96 @@
var cpuCount = require('os').cpus().length;
var EventEmitter = require('events').EventEmitter;
var util = require('util');
module.exports = DedupedQueue;
util.inherits(DedupedQueue, EventEmitter);
function DedupedQueue(options) {
EventEmitter.call(this);
this.maxAsync = options.maxAsync || cpuCount;
this.processOne = options.processOne;
this.pendingQueue = [];
this.pendingSet = {};
this.processingCount = 0;
this.processingSet = {};
}
DedupedQueue.prototype.idInQueue = function(id) {
return !!(this.pendingSet[id] || this.processingSet[id]);
};
DedupedQueue.prototype.add = function(id, item, cb) {
var queueItem = this.pendingSet[id];
if (queueItem) {
if (cb) queueItem.cbs.push(cb);
return;
}
queueItem = new QueueItem(id, item);
if (cb) queueItem.cbs.push(cb);
this.pendingSet[id] = queueItem;
this.pendingQueue.push(queueItem);
this.flush();
};
DedupedQueue.prototype.waitForId = function(id, cb) {
var queueItem = this.pendingSet[id] || this.processingSet[id];
if (!queueItem) return cb();
queueItem.cbs.push(cb);
};
DedupedQueue.prototype.flush = function() {
// if an item cannot go into the processing queue because an item with the
// same ID is already there, it goes into deferred
var deferred = [];
while (this.processingCount < this.maxAsync && this.pendingQueue.length > 0) {
var queueItem = this.pendingQueue.shift();
if (this.processingSet[queueItem.id]) {
deferred.push(queueItem);
} else {
delete this.pendingSet[queueItem.id];
this.processingSet[queueItem.id] = queueItem;
this.processingCount += 1;
this.startOne(queueItem);
}
}
for (var i = 0; i < deferred.length; i += 1) {
this.pendingQueue.push(deferred[i]);
}
};
DedupedQueue.prototype.startOne = function(queueItem) {
var self = this;
var callbackCalled = false;
self.processOne(queueItem.item, function(err) {
if (callbackCalled) {
self.emit('error', new Error("callback called more than once"));
return;
}
callbackCalled = true;
delete self.processingSet[queueItem.id];
self.processingCount -= 1;
if (queueItem.cbs.length === 0) {
defaultCb(err);
} else {
for (var i = 0; i < queueItem.cbs.length; i += 1) {
queueItem.cbs[i](err);
}
}
self.flush();
function defaultCb(err) {
if (err) self.emit('error', err);
self.emit('oneEnd');
}
});
};
function QueueItem(id, item) {
this.id = id;
this.item = item;
this.cbs = [];
}

379
lib/groovebasin.js Normal file
View file

@ -0,0 +1,379 @@
var EventEmitter = require('events').EventEmitter;
var http = require('http');
var assert = require('assert');
var WebSocketServer = require('ws').Server;
var fs = require('fs');
var archiver = require('archiver');
var util = require('util');
var path = require('path');
var Pend = require('pend');
var express = require('express');
var osenv = require('osenv');
var spawn = require('child_process').spawn;
var requireIndex = require('requireindex');
var plugins = requireIndex(path.join(__dirname, 'plugins'));
var Player = require('./player');
var PlayerServer = require('./player_server');
var MpdProtocol = require('./mpd_protocol');
var MpdApiServer = require('./mpd_api_server');
var WebSocketApiClient = require('./web_socket_api_client');
var levelup = require('level');
var crypto = require('crypto');
var net = require('net');
var safePath = require('./safe_path');
var MultipartForm = require('multiparty').Form;
var createGzipStatic = require('connect-static');
var serveStatic = require('serve-static');
var bodyParser = require('body-parser');
module.exports = GrooveBasin;
var defaultConfig = {
host: '0.0.0.0',
port: 16242,
dbPath: "groovebasin.db",
musicDirectory: path.join(osenv.home(), "music"),
permissions: {},
defaultPermissions: {
read: true,
add: true,
control: true,
},
lastFmApiKey: "7d831eff492e6de5be8abb736882c44d",
lastFmApiSecret: "8713e8e893c5264608e584a232dd10a0",
mpdHost: '0.0.0.0',
mpdPort: 6600,
acoustidAppKey: 'bgFvC4vW',
instantBufferBytes: 220 * 1024,
};
defaultConfig.permissions[genPassword()] = {
admin: true,
read: true,
add: true,
control: true,
};
util.inherits(GrooveBasin, EventEmitter);
function GrooveBasin() {
EventEmitter.call(this);
this.app = express();
}
GrooveBasin.prototype.initConfigVar = function(name, defaultValue) {
this.configVars.push(name);
this[name] = defaultValue;
};
GrooveBasin.prototype.loadConfig = function(cb) {
var self = this;
var pathToConfig = "config.js";
fs.readFile(pathToConfig, {encoding: 'utf8'}, function(err, contents) {
var anythingAdded = false;
var config;
if (err) {
if (err.code === 'ENOENT') {
anythingAdded = true;
self.config = defaultConfig;
console.warn("No config.js found; writing default.");
} else {
return cb(err);
}
} else {
try {
self.config = JSON.parse(contents);
} catch (err) {
cb(err);
}
}
// this ensures that even old files get new config values when we add them
for (var key in defaultConfig) {
if (self.config[key] === undefined) {
anythingAdded = true;
self.config[key] = defaultConfig[key];
}
}
if (anythingAdded) {
fs.writeFile(pathToConfig, JSON.stringify(self.config, null, 4), cb);
} else {
cb();
}
});
};
GrooveBasin.prototype.start = function() {
var self = this;
var pend = new Pend();
pend.go(function(cb) {
self.loadConfig(cb);
});
pend.go(function(cb) {
var options = {
dir: path.join(__dirname, "../public"),
aliases: [],
};
createGzipStatic(options, function(err, middleware) {
if (err) return cb(err);
self.app.use(middleware);
cb();
});
});
pend.go(function(cb) {
createGzipStatic({dir: path.join(__dirname, "../src/public")}, function(err, middleware) {
if (err) return cb(err);
self.app.use(middleware);
cb();
});
});
pend.wait(function(err) {
if (err) {
console.error(err.stack);
return;
}
self.httpHost = self.config.host;
self.httpPort = self.config.port;
self.db = levelup(self.config.dbPath);
self.initializeDownload();
self.initializeUpload();
self.player = new Player(self.db, self.config.musicDirectory, self.config.instantBufferBytes);
self.player.initialize(function(err) {
if (err) {
console.error("unable to initialize player:", err.stack);
return;
}
console.info("Player initialization complete.");
self.app.use(self.player.streamMiddleware.bind(self.player));
var pend = new Pend();
for (var pluginName in plugins) {
var PluginClass = plugins[pluginName];
var plugin = new PluginClass(self);
if (plugin.initialize) pend.go(plugin.initialize.bind(plugin));
}
pend.wait(function(err) {
if (err) {
console.error("Error initializing plugin:", err.stack);
return;
}
self.startServer();
});
});
});
};
GrooveBasin.prototype.initializeDownload = function() {
var self = this;
var musicDir = self.config.musicDirectory;
self.app.use('/library', serveStatic(musicDir));
self.app.get('/library/', function(req, resp) {
downloadPath("", "library.zip", req, resp);
});
self.app.get(/^\/library\/(.*)\/$/, function(req, resp){
var reqDir = req.params[0];
var zipName = safePath(reqDir.replace(/\//g, " - ")) + ".zip";
downloadPath(reqDir, zipName, req, resp);
});
self.app.post('/download/custom', bodyParser(), function(req, resp) {
var reqKeys = req.body.key;
if (!Array.isArray(reqKeys)) {
reqKeys = [reqKeys];
}
var files = [];
for (var i = 0; i < reqKeys.length; i += 1) {
var key = reqKeys[i];
var dbFile = self.player.libraryIndex.trackTable[key];
if (dbFile) files.push(path.join(musicDir, dbFile.file));
}
var reqZipName = (req.body.zipName || "music").toString();
var zipName = safePath(reqZipName) + ".zip";
sendZipOfFiles(zipName, files, req, resp);
});
function downloadPath(dirName, zipName, req, resp) {
var files = [];
var dirEntry = self.player.dirs[dirName];
if (!dirEntry) {
resp.statusCode = 404;
resp.end("Not found");
return;
}
sendZipOfFiles(zipName, files, req, resp);
function addOneDir(dirEntry) {
var baseName, relPath;
for (baseName in dirEntry.entries) {
relPath = path.join(dirEntry.dirName, baseName);
var dbTrack = self.player.dbFilesByPath[relPath];
if (dbTrack) files.push(dbTrack.file);
}
for (baseName in dirEntry.dirEntries) {
relPath = path.join(dirEntry.dirName, baseName);
var childEntry = self.player.dirs[relPath];
if (childEntry) addOneDir(childEntry);
}
}
}
function sendZipOfFiles(zipName, files, req, resp) {
var cleanup = [];
req.on('close', cleanupEverything);
resp.setHeader("Content-Type", "application/zip");
resp.setHeader("Content-Disposition", "attachment; filename=" + zipName);
var archive = archiver('zip');
archive.on('error', function(err) {
console.log("Error while sending zip of files:", err.stack);
cleanupEverything();
});
cleanup.push(function(){
archive.destroy();
});
archive.pipe(resp);
files.forEach(function(file) {
var options = {
name: path.relative(self.config.musicDirectory, file),
};
var readStream = fs.createReadStream(file);
readStream.on('error', function(err) {
console.error("zip read stream error:", err.stack);
});
cleanup.push(function() {
readStream.destroy();
});
archive.append(readStream, options);
});
archive.finalize(function(err) {
if (err) {
console.error("Error finalizing zip:", err.stack);
cleanupEverything();
}
});
function cleanupEverything() {
cleanup.forEach(function(fn) {
try {
fn();
} catch(err) {}
});
resp.end();
}
}
};
GrooveBasin.prototype.initializeUpload = function() {
var self = this;
self.app.post('/upload', function(request, response, next) {
var form = new MultipartForm();
form.parse(request, function(err, fields, files) {
if (err) return next(err);
var keys = [];
var pend = new Pend();
for (var key in files) {
var arr = files[key];
for (var i = 0; i < arr.length; i += 1) {
var file = arr[i];
pend.go(makeImportFn(file));
}
}
pend.wait(function() {
response.json(keys);
});
function makeImportFn(file) {
return function(cb) {
self.player.importFile(file.path, file.originalFilename, function(err, dbFile) {
if (err) {
console.error("Unable to import file:", file.path, "error:", err.stack);
} else if (!dbFile) {
console.error("Unable to locate new file due to race condition");
} else {
keys.push(dbFile.key);
}
cb();
});
};
}
});
});
};
GrooveBasin.prototype.startServer = function() {
var self = this;
assert.ok(self.httpServer == null);
self.playerServer = new PlayerServer({
player: self.player,
authenticate: authenticate,
defaultPermissions: self.config.defaultPermissions,
});
self.mpdApiServer = new MpdApiServer(self.player);
self.httpServer = http.createServer(self.app);
self.wss = new WebSocketServer({
server: self.httpServer,
clientTracking: false,
});
self.wss.on('connection', function(ws) {
self.playerServer.handleNewClient(new WebSocketApiClient(ws));
});
self.httpServer.listen(self.httpPort, self.httpHost, function() {
self.emit('listening');
console.info("Listening at http://" + self.httpHost + ":" + self.httpPort + "/");
});
self.httpServer.on('close', function() {
console.info("server closed");
});
var mpdPort = self.config.mpdPort;
var mpdHost = self.config.mpdHost;
if (mpdPort == null || mpdHost == null) {
console.info("MPD Protocol disabled");
} else {
self.protocolServer = net.createServer(function(socket) {
socket.setEncoding('utf8');
var protocol = new MpdProtocol({
player: self.player,
playerServer: self.playerServer,
apiServer: self.mpdApiServer,
authenticate: authenticate,
permissions: self.config.defaultPermissions,
});
protocol.on('error', handleError);
socket.on('error', handleError);
socket.pipe(protocol).pipe(socket);
socket.on('close', cleanup);
self.mpdApiServer.handleNewClient(protocol);
function handleError(err) {
console.error("socket error:", err.stack);
socket.destroy();
cleanup();
}
function cleanup() {
self.mpdApiServer.handleClientEnd(protocol);
}
});
self.protocolServer.listen(mpdPort, mpdHost, function() {
console.info("MPD/GrooveBasin Protocol listening at " +
mpdHost + ":" + mpdPort);
});
}
function authenticate(password) {
return self.config.permissions[password];
}
};
function genPassword() {
return crypto.pseudoRandomBytes(9).toString('base64');
}

73
lib/mpd_api_server.js Normal file
View file

@ -0,0 +1,73 @@
var EventEmitter = require('events').EventEmitter;
var util = require('util');
module.exports = MpdApiServer;
// stuff that is global to all connected mpd clients
util.inherits(MpdApiServer, EventEmitter);
function MpdApiServer(player) {
var self = this;
EventEmitter.call(self);
self.gbIdToMpdId = {};
self.mpdIdToGbId = {};
self.nextMpdId = 0;
self.singleMode = false;
self.clients = [];
player.on('volumeUpdate', onVolumeUpdate);
player.on('repeatUpdate', updateOptionsSubsystem);
player.on('dynamicModeOn', updateOptionsSubsystem);
player.on('playlistUpdate', onPlaylistUpdate);
player.on('deleteDbTrack', updateDatabaseSubsystem);
player.on('addDbTrack', updateDatabaseSubsystem);
player.on('updateDbTrack', updateDatabaseSubsystem);
function onVolumeUpdate() {
self.subsystemUpdate('mixer');
}
function onPlaylistUpdate() {
// TODO make these updates more fine grained
self.subsystemUpdate('playlist');
self.subsystemUpdate('player');
}
function updateOptionsSubsystem() {
self.subsystemUpdate('options');
}
function updateDatabaseSubsystem() {
self.subsystemUpdate('database');
}
}
MpdApiServer.prototype.handleClientEnd = function(client) {
var index = this.clients.indexOf(client);
if (index !== -1) this.clients.splice(index, 1);
};
MpdApiServer.prototype.handleNewClient = function(client) {
this.clients.push(client);
};
MpdApiServer.prototype.subsystemUpdate = function(subsystem) {
this.clients.forEach(function(client) {
client.updatedSubsystems[subsystem] = true;
if (client.isIdle) client.handleIdle();
});
};
MpdApiServer.prototype.toMpdId = function(grooveBasinId) {
var mpdId = this.gbIdToMpdId[grooveBasinId];
if (!mpdId) {
mpdId = this.nextMpdId++;
this.gbIdToMpdId[grooveBasinId] = mpdId;
this.mpdIdToGbId[mpdId] = grooveBasinId;
}
return mpdId;
};
MpdApiServer.prototype.fromMpdId = function(mpdId) {
return this.mpdIdToGbId[mpdId];
};
MpdApiServer.prototype.setSingleMode = function(mode) {
this.singleMode = mode;
this.subsystemUpdate('options');
};

1605
lib/mpd_protocol.js Normal file

File diff suppressed because it is too large Load diff

1897
lib/player.js Normal file

File diff suppressed because it is too large Load diff

347
lib/player_server.js Normal file
View file

@ -0,0 +1,347 @@
var uuid = require('uuid');
var jsondiffpatch = require('jsondiffpatch');
var Player = require('./player');
module.exports = PlayerServer;
PlayerServer.plugins = [];
PlayerServer.actions = {
'addid': {
permission: 'add',
args: 'object',
fn: function(self, client, items) {
self.player.addItems(items);
},
},
'clear': {
permission: 'control',
fn: function(self) {
self.player.clearPlaylist();
},
},
'deleteTracks': {
permission: 'admin',
args: 'array',
fn: function(self, client, keys) {
for (var i = 0; i < keys.length; i += 1) {
var key = keys[i];
self.player.deleteFile(key);
}
},
},
'deleteid': {
permission: 'control',
args: 'array',
fn: function(self, client, ids) {
self.player.removePlaylistItems(ids);
},
},
'dynamicModeOn': {
permission: 'control',
args: 'boolean',
fn: function(self, client, on) {
self.player.setDynamicModeOn(on);
},
},
'dynamicModeHistorySize': {
permission: 'control',
args: 'number',
fn: function(self, client, size) {
self.player.setDynamicModeHistorySize(size);
},
},
'dynamicModeFutureSize': {
permission: 'control',
args: 'number',
fn: function(self, client, size) {
self.player.setDynamicModeFutureSize(size);
},
},
'importUrl': {
permission: 'control',
args: 'object',
fn: function(self, client, args) {
var urlString = String(args.url);
var id = args.id;
self.player.importUrl(urlString, function(err, dbFile) {
var key = null;
if (err) {
console.error("Unable to import url:", urlString, "error:", err.stack);
} else if (!dbFile) {
console.error("Unable to import file due to race condition.");
} else {
key = dbFile.key;
}
client.sendMessage('importUrl', {id: id, key: key});
});
},
},
'subscribe': {
permission: 'read',
args: 'object',
fn: function(self, client, args) {
var name = args.name;
var subscription = self.subscriptions[name];
if (!subscription) {
console.warn("Invalid subscription item:", name);
return;
}
if (args.delta) {
client.subscriptions[name] = 'delta';
if (args.version !== subscription.version) {
client.sendMessage(name, {
version: subscription.version,
reset: true,
delta: jsondiffpatch.diff(undefined, subscription.value),
});
}
} else {
client.subscriptions[name] = 'simple';
client.sendMessage(name, subscription.value);
}
},
},
'updateTags': {
permission: 'admin',
args: 'object',
fn: function(self, client, obj) {
self.player.updateTags(obj);
},
},
'unsubscribe': {
permission: 'read',
args: 'string',
fn: function(self, client, name) {
delete client.subscriptions[name];
},
},
'move': {
permission: 'control',
args: 'object',
fn: function(self, client, items) {
self.player.movePlaylistItems(items);
},
},
'password': {
permission: null,
args: 'string',
fn: function(self, client, password) {
var perms = self.authenticate(password);
var success = perms != null;
if (success) client.permissions = perms;
client.sendMessage('permissions', client.permissions);
},
},
'pause': {
permission: 'control',
fn: function(self, client) {
self.player.pause();
},
},
'play': {
permission: 'control',
fn: function(self, client) {
self.player.play();
},
},
'seek': {
permission: 'control',
args: 'object',
fn: function(self, client, args) {
self.player.seek(args.id, args.pos);
},
},
'repeat': {
permission: 'control',
args: 'number',
fn: function(self, client, mode) {
self.player.setRepeat(mode);
},
},
'setvol': {
permission: 'control',
args: 'number',
fn: function(self, client, vol) {
self.player.setVolume(vol);
},
},
'shuffle': {
permission: 'control',
fn: function(self, client) {
self.player.shufflePlaylist();
},
},
'stop': {
permission: 'control',
fn: function(self, client) {
self.player.stop();
},
},
};
function PlayerServer(options) {
this.player = options.player;
this.authenticate = options.authenticate;
this.defaultPermissions = options.defaultPermissions;
this.subscriptions = {};
this.clients = [];
this.playlistId = uuid();
this.libraryId = uuid();
this.initialize();
}
PlayerServer.prototype.initialize = function() {
var self = this;
self.player.on('currentTrack', addSubscription('currentTrack', getCurrentTrack));
self.player.on('dynamicModeOn', addSubscription('dynamicModeOn', getDynamicModeOn));
self.player.on('dynamicModeHistorySize', addSubscription('dynamicModeHistorySize', getDynamicModeHistorySize));
self.player.on('dynamicModeFutureSize', addSubscription('dynamicModeFutureSize', getDynamicModeFutureSize));
self.player.on('repeatUpdate', addSubscription('repeat', getRepeat));
self.player.on('volumeUpdate', addSubscription('volume', getVolume));
self.player.on('playlistUpdate', addSubscription('playlist', serializePlaylist));
var onLibraryUpdate = addSubscription('library', serializeLibrary);
self.player.on('addDbTrack', onLibraryUpdate);
self.player.on('updateDbTrack', onLibraryUpdate);
self.player.on('deleteDbTrack', onLibraryUpdate);
self.player.on('scanComplete', onLibraryUpdate);
self.player.on('seek', function() {
self.clients.forEach(function(client) {
client.sendMessage('seek');
});
});
setInterval(function() {
self.clients.forEach(function(client) {
client.sendMessage('time', new Date());
});
}, 30000);
function addSubscription(name, serializeFn) {
var subscription = self.subscriptions[name] = {
version: uuid(),
value: serializeFn(),
};
return function() {
var newValue = serializeFn();
var delta = jsondiffpatch.diff(subscription.value, newValue);
if (!delta) return; // no delta, nothing to send!
subscription.value = newValue;
subscription.version = uuid();
self.clients.forEach(function(client) {
var clientSubscription = client.subscriptions[name];
if (clientSubscription === 'simple') {
client.sendMessage(name, newValue);
} else if (clientSubscription === 'delta') {
client.sendMessage(name, {
version: subscription.version,
delta: delta,
});
}
});
};
}
function getVolume(client) {
return self.player.volume;
}
function getTime(client) {
return new Date();
}
function getRepeat(client) {
return self.player.repeat;
}
function getCurrentTrack() {
return {
currentItemId: self.player.currentTrack && self.player.currentTrack.id,
isPlaying: self.player.isPlaying,
trackStartDate: self.player.trackStartDate,
pausedTime: self.player.pausedTime,
};
}
function getDynamicModeOn() {
return self.player.dynamicModeOn;
}
function getDynamicModeFutureSize() {
return self.player.dynamicModeFutureSize;
}
function getDynamicModeHistorySize() {
return self.player.dynamicModeHistorySize;
}
function serializePlaylist() {
var playlist = self.player.playlist;
var o = {};
for (var id in playlist) {
var item = playlist[id];
o[id] = {
key: item.key,
sortKey: item.sortKey,
isRandom: item.isRandom,
};
}
return o;
}
function serializeLibrary() {
var table = {};
for (var key in self.player.libraryIndex.trackTable) {
var track = self.player.libraryIndex.trackTable[key];
table[key] = Player.trackWithoutIndex('read', track);
}
return table;
}
};
PlayerServer.prototype.handleNewClient = function(client) {
var self = this;
client.subscriptions = {};
client.permissions = self.defaultPermissions;
client.on('message', onMessage);
client.sendMessage('permissions', client.permissions);
client.sendMessage('time', new Date());
client.on('close', onClose);
self.clients.push(client);
PlayerServer.plugins.forEach(function(plugin) {
plugin.handleNewClient(client);
});
function onClose() {
var index = self.clients.indexOf(client);
if (index >= 0) self.clients.splice(index, 1);
}
function onMessage(name, args) {
var action = PlayerServer.actions[name];
if (!action) {
console.warn("Invalid command:", name);
client.sendMessage("error", "invalid command: " + JSON.stringify(name));
return;
}
var perm = action.permission;
if (perm != null && !client.permissions[perm]) {
var errText = "command " + JSON.stringify(name) +
" requires permission " + JSON.stringify(perm);
console.warn("permissions error:", errText);
client.sendMessage("error", errText);
return;
}
var argsType = Array.isArray(args) ? 'array' : typeof args;
if (action.args && argsType !== action.args) {
console.warn("expected arg type", action.args, args);
client.sendMessage("error", "expected " + action.args + ": " + JSON.stringify(args));
return;
}
console.info("ok command", name, args);
action.fn(self, client, args);
}
};

237
lib/plugins/lastfm.js Normal file
View file

@ -0,0 +1,237 @@
var LastFmNode = require('lastfm').LastFmNode;
var PlayerServer = require('../player_server');
module.exports = LastFm;
var DB_KEY = 'Plugin.lastfm';
function LastFm(gb) {
this.gb = gb;
this.previousNowPlaying = null;
this.lastPlayingItem = null;
this.playingStart = new Date();
this.playingTime = 0;
this.previousIsPlaying = false;
this.scrobblers = {};
this.scrobbles = [];
this.lastFm = new LastFmNode({
api_key: this.gb.config.lastFmApiKey,
secret: this.gb.config.lastFmApiSecret,
});
this.gb.player.on('playlistUpdate', checkScrobble.bind(this));
this.gb.player.on('playlistUpdate', updateNowPlaying.bind(this));
this.initActions();
}
LastFm.prototype.initialize = function(cb) {
var self = this;
self.gb.db.get(DB_KEY, function(err, value) {
if (err) {
if (err.type !== 'NotFoundError') return cb(err);
} else {
var state = JSON.parse(value);
self.scrobblers = state.scrobblers;
self.scrobbles = state.scrobbles;
}
// in case scrobbling fails and then the user presses stop, this will still
// flush the queue.
setInterval(self.flushScrobbleQueue.bind(self), 120000);
cb();
});
};
LastFm.prototype.persist = function() {
var self = this;
var state = {
scrobblers: self.scrobblers,
scrobbles: self.scrobbles,
};
self.gb.db.put(DB_KEY, JSON.stringify(state), function(err) {
if (err) {
console.error("Unable to persist lastfm state to db:", err.stack);
}
});
}
LastFm.prototype.initActions = function() {
var self = this;
PlayerServer.plugins.push({
handleNewClient: function(client) {
client.sendMessage('LastFmApiKey', self.gb.config.lastFmApiKey);
},
});
PlayerServer.actions.LastFmGetSession = {
permission: 'read',
args: 'string',
fn: function(playerServer, client, token){
self.lastFm.request("auth.getSession", {
token: token,
handlers: {
success: function(data){
delete self.scrobblers[data.session.name];
client.sendMessage('LastFmGetSessionSuccess', data);
},
error: function(error){
console.error("error from last.fm auth.getSession:", error.message);
client.sendMessage('LastFmGetSessionError', error.message);
}
}
});
}
};
PlayerServer.actions.LastFmScrobblersAdd = {
permission: 'read',
args: 'object',
fn: function(playerServer, client, params) {
var existingUser = self.scrobblers[params.username];
if (existingUser) {
console.warn("Trying to overwrite a scrobbler:", params.username);
return;
}
self.scrobblers[params.username] = params.session_key;
self.persist();
},
};
PlayerServer.actions.LastFmScrobblersRemove = {
permission: 'read',
args: 'object',
fn: function(playerServer, client, params) {
var sessionKey = self.scrobblers[params.username];
if (sessionKey !== params.session_key) {
console.warn("Invalid session key from user trying to remove scrobbler:", params.username);
return;
}
delete self.scrobblers[params.username];
self.persist();
},
};
}
LastFm.prototype.flushScrobbleQueue = function() {
var self = this;
var params;
var maxSimultaneous = 10;
var count = 0;
while ((params = self.scrobbles.shift()) != null && count++ < maxSimultaneous) {
console.info("scrobbling " + params.track + " for session " + params.sk);
params.handlers = {
error: onError,
};
self.lastFm.request('track.scrobble', params);
}
self.persist();
function onError(error){
console.error("error from last.fm track.scrobble:", error.stack);
if (!error.code || error.code === 11 || error.code === 16) {
// try again
self.scrobbles.push(params);
self.persist();
}
}
}
LastFm.prototype.queueScrobble = function(params){
console.info("queueScrobble", params);
this.scrobbles.push(params);
this.persist();
};
function checkScrobble() {
var self = this;
if (self.gb.player.isPlaying && !self.previousIsPlaying) {
self.playingStart = new Date(new Date() - self.playingTime);
self.previousIsPlaying = true;
}
self.playingTime = new Date() - self.playingStart;
var thisItem = self.gb.player.currentTrack;
if (thisItem === self.lastPlayingItem) return;
if (self.lastPlayingItem) {
var dbFile = self.gb.player.libraryIndex.trackTable[self.lastPlayingItem.key];
var minAmt = 15 * 1000;
var maxAmt = 4 * 60 * 1000;
var halfAmt = dbFile.duration / 2 * 1000;
if (self.playingTime >= minAmt && (self.playingTime >= maxAmt || self.playingTime >= halfAmt)) {
if (dbFile.artistName) {
for (var username in self.scrobblers) {
var sessionKey = self.scrobblers[username];
self.queueScrobble({
sk: sessionKey,
chosenByUser: +!self.lastPlayingItem.isRandom,
timestamp: Math.round(self.playingStart.getTime() / 1000),
album: dbFile.albumName,
track: dbFile.name,
artist: dbFile.artistName,
albumArtist: dbFile.albumArtistName,
duration: Math.round(dbFile.duration),
trackNumber: dbFile.track,
});
}
self.flushScrobbleQueue();
} else {
console.warn("Not scrobbling " + dbFile.name + " - missing artist.");
}
} else {
console.info("not scrobbling", dbFile.name, " - only listened for", self.playingTime);
}
}
self.lastPlayingItem = thisItem;
self.previousIsPlaying = self.gb.player.isPlaying;
self.playingStart = new Date();
self.playingTime = 0;
}
function updateNowPlaying() {
var self = this;
if (!self.gb.player.isPlaying) return;
var track = self.gb.player.currentTrack;
if (!track) return;
if (self.previousNowPlaying === track) return;
self.previousNowPlaying = track;
var dbFile = self.gb.player.libraryIndex.trackTable[track.key];
if (!dbFile.artistName) {
console.warn("Not updating last.fm now playing for " + dbFile.name + ": missing artist");
return;
}
for (var username in self.scrobblers) {
var sessionKey = self.scrobblers[username];
var props = {
sk: sessionKey,
track: dbFile.name,
artist: dbFile.artistName,
album: dbFile.albumName,
albumArtist: dbFile.albumArtistName,
trackNumber: dbFile.track,
duration: Math.round(dbFile.duration),
handlers: {
error: onError
}
}
console.info("updateNowPlaying", props);
self.lastFm.request("track.updateNowPlaying", props);
}
function onError(error){
console.error("unable to update last.fm now playing:", error.message);
}
}

62
lib/plugins/ytdl.js Normal file
View file

@ -0,0 +1,62 @@
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;
}
}
};

65
lib/protocol_parser.js Normal file
View file

@ -0,0 +1,65 @@
var Duplex = require('stream').Duplex;
var util = require('util');
module.exports = ProtocolParser;
util.inherits(ProtocolParser, Duplex);
function ProtocolParser(options) {
var streamOptions = extend(extend({}, options.streamOptions || {}), {decodeStrings: false});
Duplex.call(this, streamOptions);
this.player = options.player;
this.buffer = "";
this.alreadyClosed = false;
}
ProtocolParser.prototype._read = function(size) {};
ProtocolParser.prototype._write = function(chunk, encoding, callback) {
var self = this;
var lines = chunk.split("\n");
self.buffer += lines[0];
if (lines.length === 1) return callback();
handleLine(self.buffer);
var lastIndex = lines.length - 1;
for (var i = 1; i < lastIndex; i += 1) {
handleLine(lines[i]);
}
self.buffer = lines[lastIndex];
callback();
function handleLine(line) {
var jsonObject;
try {
jsonObject = JSON.parse(line);
} catch (err) {
console.warn("received invalid json:", err.message);
self.sendMessage("error", "invalid json: " + err.message);
return;
}
if (typeof jsonObject !== 'object') {
console.warn("received json not an object:", jsonObject);
self.sendMessage("error", "expected json object");
return;
}
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) {
for (var key in src) o[key] = src[key];
return o;
}

11
lib/safe_path.js Normal file
View file

@ -0,0 +1,11 @@
module.exports = safePath;
var MAX_LEN = 100;
function safePath(string) {
string = string.replace(/[<>:"\/\\|?*%]/g, "_");
string = string.substring(0, MAX_LEN);
string = string.replace(/\.$/, "_");
string = string.replace(/^\./, "_");
return string;
}

11
lib/server.js Normal file
View file

@ -0,0 +1,11 @@
if (!process.env.NODE_ENV) process.env.NODE_ENV = "dev";
var GrooveBasin = require('./groovebasin');
var gb = new GrooveBasin();
gb.on('listening', function() {
if (process.send) process.send('online');
});
process.on('message', function(message){
if (message === 'shutdown') process.exit(0);
});
gb.start();

View file

@ -0,0 +1,51 @@
var EventEmitter = require('events').EventEmitter;
var util = require('util');
module.exports = WebSocketApiClient;
util.inherits(WebSocketApiClient, EventEmitter);
function WebSocketApiClient(ws) {
EventEmitter.call(this);
this.ws = ws;
this.initialize();
}
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
}
};
WebSocketApiClient.prototype.close = function() {
this.ws.close();
};
WebSocketApiClient.prototype.initialize = function() {
var self = this;
self.ws.on('message', function(data, flags) {
if (flags.binary) {
console.warn("ignoring binary web socket message");
return;
}
var msg;
try {
msg = JSON.parse(data);
} catch (err) {
console.warn("received invalid JSON from web socket:", err.message);
return;
}
self.emit('message', msg.name, msg.args);
});
self.ws.on('error', function(err) {
console.error("web socket error:", err.stack);
});
self.ws.on('close', function() {
self.emit('close');
});
};

View file

@ -1,48 +1,54 @@
{
"name": "groovebasin",
"description": "No-nonsense music client and daemon based on mpd",
"author": "Andrew Kelley <superjoe30@gmail.com>",
"version": "0.1.1",
"licenses": [{
"type": "MIT",
"url": "https://raw.github.com/superjoe30/groovebasin/master/LICENSE"
}],
"engines": {
"node": "~0.8.2"
},
"repository": {
"type": "git",
"url": "git://github.com/superjoe30/groovebasin.git"
},
"dependencies": {
"socket.io": "~0.8.7",
"node-static": "~0.6.0",
"formidable": "~1.0.8",
"lastfm": "~0.8.0",
"mkdirp": "~0.3.0",
"node.extend": "~1.0.0",
"zipstream": "~0.2.1"
},
"devDependencies": {
"coffee-script": "~1.3.3",
"handlebars": "~1.0.4beta",
"stylus": "~0.28.1",
"node-dev": "~0.2.3"
},
"scripts": {
"start": "node lib/server.js",
"dev": "npm install . && cake build && node-dev lib/server.js"
},
"config": {
"user_id": "",
"log_level": 2,
"port": 16242,
"mpd_conf": "/etc/mpd.conf",
"state_file": ".state.json",
"development_mode": false,
"lastfm_api_key": "7d831eff492e6de5be8abb736882c44d",
"lastfm_secret": "8713e8e893c5264608e584a232dd10a0",
"dynamicmode_history_size": 10,
"dynamicmode_future_size": 10
"name": "groovebasin",
"description": "Music player server with a web-based interface inspired by Amarok 1.4",
"author": "Andrew Kelley <superjoe30@gmail.com>",
"version": "1.0.1",
"licenses": [
{
"type": "MIT",
"url": "https://raw.github.com/andrewrk/groovebasin/master/LICENSE"
}
],
"engines": {
"node": ">=0.10.20"
},
"repository": {
"type": "git",
"url": "git://github.com/andrewrk/groovebasin.git"
},
"dependencies": {
"lastfm": "~0.9.0",
"express": "^4.0.0",
"superagent": "^0.17.0",
"mkdirp": "~0.3.5",
"mv": "~2.0.0",
"pend": "~1.1.1",
"zfill": "0.0.1",
"requireindex": "^1.1.0",
"mess": "~0.1.1",
"groove": "~1.4.1",
"osenv": "0.0.3",
"level": "^0.18.0",
"findit": "~1.1.1",
"archiver": "^0.6.1",
"uuid": "~1.4.1",
"music-library-index": "^1.1.1",
"keese": "~1.0.0",
"ws": "^0.4.31",
"jsondiffpatch": "~0.1.4",
"connect-static": "^1.1.0",
"multiparty": "^3.2.4",
"ytdl": "^0.2.4",
"serve-static": "^1.0.3",
"body-parser": "^1.0.1"
},
"devDependencies": {
"stylus": "^0.42.3",
"browserify": "^3.41.0"
},
"scripts": {
"start": "node lib/server.js",
"build": "npm install && ./build",
"dev": "npm run build && npm start"
}
}

View file

@ -1,117 +0,0 @@
<!doctype html>
<html>
<head>
<title>Groove Basin</title>
<link rel="stylesheet" type="text/css" href="vendor/reset.min.css">
<link rel="stylesheet" type="text/css" href="vendor/css/dot-luv/jquery-ui-1.8.17.custom.css">
<link rel="stylesheet" type="text/css" href="vendor/fileuploader/fileuploader.css">
<link rel="stylesheet" type="text/css" href="app.css">
<link rel="shortcut icon" href="/favicon.png" type="image/png">
</head>
<body>
<div id="nowplaying" class="ui-widget-content ui-corner-all">
<ul class="playback-btns ui-widget ui-helper-clearfix">
<li class="ui-state-default ui-corner-all hoverable prev">
<span class="ui-icon ui-icon-seek-prev"></span>
</li>
<li class="ui-state-default ui-corner-all hoverable toggle">
<span class="ui-icon ui-icon-pause"></span>
</li>
<li class="ui-state-default ui-corner-all hoverable stop">
<span class="ui-icon ui-icon-stop"></span>
</li>
<li class="ui-state-default ui-corner-all hoverable next">
<span class="ui-icon ui-icon-seek-next"></span>
</li>
</ul>
<div id="vol">
<span class="ui-icon ui-icon-volume-off"></span>
<div id="vol-slider"></div>
<span class="ui-icon ui-icon-volume-on"></span>
</div>
<div id="more-playback-btns">
<input type="checkbox" id="stream-btn"><label for="stream-btn">Stream</label>
</div>
<h1 id="track-display"></h1>
<div id="track-slider"></div>
<span class="time elapsed"></span>
<span class="time left"></span>
<div style="clear: both;"></div>
</div>
<div id="left-window">
<div id="lib-tabs" class="ui-widget ui-corner-all">
<ul class="ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-corner-all">
<li class="ui-state-default ui-corner-top ui-state-active library-tab"><span>Library</span></li>
<li class="ui-state-default ui-corner-top upload-tab"><span>Upload</span></li>
<li class="ui-state-default ui-corner-top chat-tab"><span>Chat</span></li>
<li class="ui-state-default ui-corner-top settings-tab"><span>Settings</span></li>
</ul>
</div>
<div id="library-tab" class="ui-widget-content ui-corner-all">
<div class="window-header">
<input type="text" id="lib-filter" placeholder="filter">
<select id="organize">
<option selected="selected">Artist / Album / Song</option>
</select>
</div>
<div id="library">
</div>
</div>
<div id="upload-tab" class="ui-widget-content ui-corner-all" style="display: none">
<div id="upload">
<div id="upload-widget"></div>
</div>
</div>
<div id="chat-tab" class="ui-widget-content ui-corner-all" style="display: none">
online users:
<ul id="chat-user-list">
</ul>
<hr/>
chatter:
<div id="chat-list" style="overflow-y: auto;">
</div>
<hr/>
<span id="user-id" class="chat-user-self"></span>
<input type="text" id="chat-name-input" placeholder="your name" style="display: none">
<input type="text" id="chat-input" placeholder="chat">
</div>
<div id="settings-tab" class="ui-widget-content ui-corner-all" style="display: none">
<div id="settings">
</div>
</div>
</div>
<div id="playlist-window" class="ui-widget-content ui-corner-all">
<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 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">
<span class="track">&nbsp;</span>
<span class="title">Title</span>
<span class="artist">Artist</span>
<span class="album">Album</span>
<span class="time">Time</span>
</div>
<div id="playlist-items">
</div>
</div>
</div>
<div style="clear: both"></div>
<div id="mpd-error" style="display: none" class="ui-state-error ui-corner-all">
<p>
<span class="ui-icon ui-icon-alert"></span>
You have no connection to mpd.
</p>
</div>
<script type="text/javascript" src="vendor/jquery-1.7.1.min.js"></script>
<script type="text/javascript" src="vendor/jquery-ui-1.8.17.custom.min.js"></script>
<script type="text/javascript" src="vendor/socket.io/socket.io.min.js"></script>
<script type="text/javascript" src="vendor/handlebars.runtime.js"></script>
<script type="text/javascript" src="vendor/fileuploader/fileuploader.js"></script>
<script type="text/javascript" src="vendor/soundmanager2/soundmanager2-nodebug-jsmin.js"></script>
<script type="text/javascript" src="app.js"></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -1,565 +0,0 @@
/*
* jQuery UI CSS Framework 1.8.17
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Theming/API
*/
/* Layout helpers
----------------------------------*/
.ui-helper-hidden { display: none; }
.ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); }
.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
.ui-helper-clearfix:before, .ui-helper-clearfix:after { content: ""; display: table; }
.ui-helper-clearfix:after { clear: both; }
.ui-helper-clearfix { zoom: 1; }
.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
/* Interaction Cues
----------------------------------*/
.ui-state-disabled { cursor: default !important; }
/* Icons
----------------------------------*/
/* states and images */
.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
/* Misc visuals
----------------------------------*/
/* Overlays */
.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
/*
* jQuery UI CSS Framework 1.8.17
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Theming/API
*
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Arial,%20sans-serif&fwDefault=bold&fsDefault=1.3em&cornerRadius=4px&bgColorHeader=0b3e6f&bgTextureHeader=08_diagonals_thick.png&bgImgOpacityHeader=15&borderColorHeader=0b3e6f&fcHeader=f6f6f6&iconColorHeader=98d2fb&bgColorContent=111111&bgTextureContent=12_gloss_wave.png&bgImgOpacityContent=20&borderColorContent=000000&fcContent=d9d9d9&iconColorContent=9ccdfc&bgColorDefault=333333&bgTextureDefault=09_dots_small.png&bgImgOpacityDefault=20&borderColorDefault=333333&fcDefault=ffffff&iconColorDefault=9ccdfc&bgColorHover=00498f&bgTextureHover=09_dots_small.png&bgImgOpacityHover=40&borderColorHover=222222&fcHover=ffffff&iconColorHover=ffffff&bgColorActive=292929&bgTextureActive=01_flat.png&bgImgOpacityActive=40&borderColorActive=096ac8&fcActive=75abff&iconColorActive=00498f&bgColorHighlight=0b58a2&bgTextureHighlight=10_dots_medium.png&bgImgOpacityHighlight=30&borderColorHighlight=052f57&fcHighlight=ffffff&iconColorHighlight=ffffff&bgColorError=a32d00&bgTextureError=09_dots_small.png&bgImgOpacityError=30&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffffff&bgColorOverlay=aaaaaa&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
*/
/* Component containers
----------------------------------*/
.ui-widget { font-family: Arial, sans-serif; font-size: 1.3em; }
.ui-widget .ui-widget { font-size: 1em; }
.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Arial, sans-serif; font-size: 1em; }
.ui-widget-content { border: 1px solid #000000; background: #111111 url(images/ui-bg_gloss-wave_20_111111_500x100.png) 50% top repeat-x; color: #d9d9d9; }
.ui-widget-content a { color: #d9d9d9; }
.ui-widget-header { border: 1px solid #0b3e6f; background: #0b3e6f url(images/ui-bg_diagonals-thick_15_0b3e6f_40x40.png) 50% 50% repeat; color: #f6f6f6; font-weight: bold; }
.ui-widget-header a { color: #f6f6f6; }
/* Interaction states
----------------------------------*/
.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #333333; background: #333333 url(images/ui-bg_dots-small_20_333333_2x2.png) 50% 50% repeat; font-weight: bold; color: #ffffff; }
.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #ffffff; text-decoration: none; }
.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #222222; background: #00498f url(images/ui-bg_dots-small_40_00498f_2x2.png) 50% 50% repeat; font-weight: bold; color: #ffffff; }
.ui-state-hover a, .ui-state-hover a:hover { color: #ffffff; text-decoration: none; }
.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #096ac8; background: #292929 url(images/ui-bg_flat_40_292929_40x100.png) 50% 50% repeat-x; font-weight: bold; color: #75abff; }
.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #75abff; text-decoration: none; }
.ui-widget :active { outline: none; }
/* Interaction Cues
----------------------------------*/
.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #052f57; background: #0b58a2 url(images/ui-bg_dots-medium_30_0b58a2_4x4.png) 50% 50% repeat; color: #ffffff; }
.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #ffffff; }
.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #a32d00 url(images/ui-bg_dots-small_30_a32d00_2x2.png) 50% 50% repeat; color: #ffffff; }
.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #ffffff; }
.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #ffffff; }
.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; }
.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
/* Icons
----------------------------------*/
/* states and images */
.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_9ccdfc_256x240.png); }
.ui-widget-content .ui-icon {background-image: url(images/ui-icons_9ccdfc_256x240.png); }
.ui-widget-header .ui-icon {background-image: url(images/ui-icons_98d2fb_256x240.png); }
.ui-state-default .ui-icon { background-image: url(images/ui-icons_9ccdfc_256x240.png); }
.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); }
.ui-state-active .ui-icon {background-image: url(images/ui-icons_00498f_256x240.png); }
.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); }
.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); }
/* positioning */
.ui-icon-carat-1-n { background-position: 0 0; }
.ui-icon-carat-1-ne { background-position: -16px 0; }
.ui-icon-carat-1-e { background-position: -32px 0; }
.ui-icon-carat-1-se { background-position: -48px 0; }
.ui-icon-carat-1-s { background-position: -64px 0; }
.ui-icon-carat-1-sw { background-position: -80px 0; }
.ui-icon-carat-1-w { background-position: -96px 0; }
.ui-icon-carat-1-nw { background-position: -112px 0; }
.ui-icon-carat-2-n-s { background-position: -128px 0; }
.ui-icon-carat-2-e-w { background-position: -144px 0; }
.ui-icon-triangle-1-n { background-position: 0 -16px; }
.ui-icon-triangle-1-ne { background-position: -16px -16px; }
.ui-icon-triangle-1-e { background-position: -32px -16px; }
.ui-icon-triangle-1-se { background-position: -48px -16px; }
.ui-icon-triangle-1-s { background-position: -64px -16px; }
.ui-icon-triangle-1-sw { background-position: -80px -16px; }
.ui-icon-triangle-1-w { background-position: -96px -16px; }
.ui-icon-triangle-1-nw { background-position: -112px -16px; }
.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
.ui-icon-arrow-1-n { background-position: 0 -32px; }
.ui-icon-arrow-1-ne { background-position: -16px -32px; }
.ui-icon-arrow-1-e { background-position: -32px -32px; }
.ui-icon-arrow-1-se { background-position: -48px -32px; }
.ui-icon-arrow-1-s { background-position: -64px -32px; }
.ui-icon-arrow-1-sw { background-position: -80px -32px; }
.ui-icon-arrow-1-w { background-position: -96px -32px; }
.ui-icon-arrow-1-nw { background-position: -112px -32px; }
.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
.ui-icon-arrow-4 { background-position: 0 -80px; }
.ui-icon-arrow-4-diag { background-position: -16px -80px; }
.ui-icon-extlink { background-position: -32px -80px; }
.ui-icon-newwin { background-position: -48px -80px; }
.ui-icon-refresh { background-position: -64px -80px; }
.ui-icon-shuffle { background-position: -80px -80px; }
.ui-icon-transfer-e-w { background-position: -96px -80px; }
.ui-icon-transferthick-e-w { background-position: -112px -80px; }
.ui-icon-folder-collapsed { background-position: 0 -96px; }
.ui-icon-folder-open { background-position: -16px -96px; }
.ui-icon-document { background-position: -32px -96px; }
.ui-icon-document-b { background-position: -48px -96px; }
.ui-icon-note { background-position: -64px -96px; }
.ui-icon-mail-closed { background-position: -80px -96px; }
.ui-icon-mail-open { background-position: -96px -96px; }
.ui-icon-suitcase { background-position: -112px -96px; }
.ui-icon-comment { background-position: -128px -96px; }
.ui-icon-person { background-position: -144px -96px; }
.ui-icon-print { background-position: -160px -96px; }
.ui-icon-trash { background-position: -176px -96px; }
.ui-icon-locked { background-position: -192px -96px; }
.ui-icon-unlocked { background-position: -208px -96px; }
.ui-icon-bookmark { background-position: -224px -96px; }
.ui-icon-tag { background-position: -240px -96px; }
.ui-icon-home { background-position: 0 -112px; }
.ui-icon-flag { background-position: -16px -112px; }
.ui-icon-calendar { background-position: -32px -112px; }
.ui-icon-cart { background-position: -48px -112px; }
.ui-icon-pencil { background-position: -64px -112px; }
.ui-icon-clock { background-position: -80px -112px; }
.ui-icon-disk { background-position: -96px -112px; }
.ui-icon-calculator { background-position: -112px -112px; }
.ui-icon-zoomin { background-position: -128px -112px; }
.ui-icon-zoomout { background-position: -144px -112px; }
.ui-icon-search { background-position: -160px -112px; }
.ui-icon-wrench { background-position: -176px -112px; }
.ui-icon-gear { background-position: -192px -112px; }
.ui-icon-heart { background-position: -208px -112px; }
.ui-icon-star { background-position: -224px -112px; }
.ui-icon-link { background-position: -240px -112px; }
.ui-icon-cancel { background-position: 0 -128px; }
.ui-icon-plus { background-position: -16px -128px; }
.ui-icon-plusthick { background-position: -32px -128px; }
.ui-icon-minus { background-position: -48px -128px; }
.ui-icon-minusthick { background-position: -64px -128px; }
.ui-icon-close { background-position: -80px -128px; }
.ui-icon-closethick { background-position: -96px -128px; }
.ui-icon-key { background-position: -112px -128px; }
.ui-icon-lightbulb { background-position: -128px -128px; }
.ui-icon-scissors { background-position: -144px -128px; }
.ui-icon-clipboard { background-position: -160px -128px; }
.ui-icon-copy { background-position: -176px -128px; }
.ui-icon-contact { background-position: -192px -128px; }
.ui-icon-image { background-position: -208px -128px; }
.ui-icon-video { background-position: -224px -128px; }
.ui-icon-script { background-position: -240px -128px; }
.ui-icon-alert { background-position: 0 -144px; }
.ui-icon-info { background-position: -16px -144px; }
.ui-icon-notice { background-position: -32px -144px; }
.ui-icon-help { background-position: -48px -144px; }
.ui-icon-check { background-position: -64px -144px; }
.ui-icon-bullet { background-position: -80px -144px; }
.ui-icon-radio-off { background-position: -96px -144px; }
.ui-icon-radio-on { background-position: -112px -144px; }
.ui-icon-pin-w { background-position: -128px -144px; }
.ui-icon-pin-s { background-position: -144px -144px; }
.ui-icon-play { background-position: 0 -160px; }
.ui-icon-pause { background-position: -16px -160px; }
.ui-icon-seek-next { background-position: -32px -160px; }
.ui-icon-seek-prev { background-position: -48px -160px; }
.ui-icon-seek-end { background-position: -64px -160px; }
.ui-icon-seek-start { background-position: -80px -160px; }
/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
.ui-icon-seek-first { background-position: -80px -160px; }
.ui-icon-stop { background-position: -96px -160px; }
.ui-icon-eject { background-position: -112px -160px; }
.ui-icon-volume-off { background-position: -128px -160px; }
.ui-icon-volume-on { background-position: -144px -160px; }
.ui-icon-power { background-position: 0 -176px; }
.ui-icon-signal-diag { background-position: -16px -176px; }
.ui-icon-signal { background-position: -32px -176px; }
.ui-icon-battery-0 { background-position: -48px -176px; }
.ui-icon-battery-1 { background-position: -64px -176px; }
.ui-icon-battery-2 { background-position: -80px -176px; }
.ui-icon-battery-3 { background-position: -96px -176px; }
.ui-icon-circle-plus { background-position: 0 -192px; }
.ui-icon-circle-minus { background-position: -16px -192px; }
.ui-icon-circle-close { background-position: -32px -192px; }
.ui-icon-circle-triangle-e { background-position: -48px -192px; }
.ui-icon-circle-triangle-s { background-position: -64px -192px; }
.ui-icon-circle-triangle-w { background-position: -80px -192px; }
.ui-icon-circle-triangle-n { background-position: -96px -192px; }
.ui-icon-circle-arrow-e { background-position: -112px -192px; }
.ui-icon-circle-arrow-s { background-position: -128px -192px; }
.ui-icon-circle-arrow-w { background-position: -144px -192px; }
.ui-icon-circle-arrow-n { background-position: -160px -192px; }
.ui-icon-circle-zoomin { background-position: -176px -192px; }
.ui-icon-circle-zoomout { background-position: -192px -192px; }
.ui-icon-circle-check { background-position: -208px -192px; }
.ui-icon-circlesmall-plus { background-position: 0 -208px; }
.ui-icon-circlesmall-minus { background-position: -16px -208px; }
.ui-icon-circlesmall-close { background-position: -32px -208px; }
.ui-icon-squaresmall-plus { background-position: -48px -208px; }
.ui-icon-squaresmall-minus { background-position: -64px -208px; }
.ui-icon-squaresmall-close { background-position: -80px -208px; }
.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
/* Misc visuals
----------------------------------*/
/* Corner radius */
.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -khtml-border-top-left-radius: 4px; border-top-left-radius: 4px; }
.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; -khtml-border-top-right-radius: 4px; border-top-right-radius: 4px; }
.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; -khtml-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; }
.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; -khtml-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; }
/* Overlays */
.ui-widget-overlay { background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); }
.ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -khtml-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }/*
* jQuery UI Resizable 1.8.17
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Resizable#theming
*/
.ui-resizable { position: relative;}
.ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block; }
.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; }
.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; }
.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; }
.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; }
.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/*
* jQuery UI Selectable 1.8.17
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Selectable#theming
*/
.ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; }
/*
* jQuery UI Accordion 1.8.17
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Accordion#theming
*/
/* IE/Win - Fix animation bug - #4615 */
.ui-accordion { width: 100%; }
.ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; }
.ui-accordion .ui-accordion-li-fix { display: inline; }
.ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; }
.ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em .7em; }
.ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; }
.ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; }
.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; }
.ui-accordion .ui-accordion-content-active { display: block; }
/*
* jQuery UI Autocomplete 1.8.17
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Autocomplete#theming
*/
.ui-autocomplete { position: absolute; cursor: default; }
/* workarounds */
* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
/*
* jQuery UI Menu 1.8.17
*
* Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Menu#theming
*/
.ui-menu {
list-style:none;
padding: 2px;
margin: 0;
display:block;
float: left;
}
.ui-menu .ui-menu {
margin-top: -3px;
}
.ui-menu .ui-menu-item {
margin:0;
padding: 0;
zoom: 1;
float: left;
clear: left;
width: 100%;
}
.ui-menu .ui-menu-item a {
text-decoration:none;
display:block;
padding:.2em .4em;
line-height:1.5;
zoom:1;
}
.ui-menu .ui-menu-item a.ui-state-hover,
.ui-menu .ui-menu-item a.ui-state-active {
font-weight: normal;
margin: -1px;
}
/*
* jQuery UI Button 1.8.17
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Button#theming
*/
.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */
.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */
button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */
.ui-button-icons-only { width: 3.4em; }
button.ui-button-icons-only { width: 3.7em; }
/*button text element */
.ui-button .ui-button-text { display: block; line-height: 1.4; }
.ui-button-text-only .ui-button-text { padding: .4em 1em; }
.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; }
.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; }
.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; }
.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; }
/* no icon support for input elements, provide padding by default */
input.ui-button { padding: .4em 1em; }
/*button icon element(s) */
.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; }
.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; }
.ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; }
.ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
/*button sets*/
.ui-buttonset { margin-right: 7px; }
.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; }
/* workarounds */
button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
/*
* jQuery UI Dialog 1.8.17
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Dialog#theming
*/
.ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; }
.ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative; }
.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; }
.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; }
.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; }
.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; }
.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
.ui-draggable .ui-dialog-titlebar { cursor: move; }
/*
* jQuery UI Slider 1.8.17
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Slider#theming
*/
.ui-slider { position: relative; text-align: left; }
.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; }
.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; }
.ui-slider-horizontal { height: .8em; }
.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; }
.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; }
.ui-slider-horizontal .ui-slider-range-min { left: 0; }
.ui-slider-horizontal .ui-slider-range-max { right: 0; }
.ui-slider-vertical { width: .8em; height: 100px; }
.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; }
.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; }
.ui-slider-vertical .ui-slider-range-min { bottom: 0; }
.ui-slider-vertical .ui-slider-range-max { top: 0; }/*
* jQuery UI Tabs 1.8.17
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Tabs#theming
*/
.ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
.ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; }
.ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; }
.ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; }
.ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; }
.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; }
.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */
.ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; }
.ui-tabs .ui-tabs-hide { display: none !important; }
/*
* jQuery UI Datepicker 1.8.17
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Datepicker#theming
*/
.ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; }
.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; }
.ui-datepicker .ui-datepicker-prev { left:2px; }
.ui-datepicker .ui-datepicker-next { right:2px; }
.ui-datepicker .ui-datepicker-prev-hover { left:1px; }
.ui-datepicker .ui-datepicker-next-hover { right:1px; }
.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; }
.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; }
.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
.ui-datepicker select.ui-datepicker-month,
.ui-datepicker select.ui-datepicker-year { width: 49%;}
.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; }
.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; }
.ui-datepicker td { border: 0; padding: 1px; }
.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; }
.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
/* with multiple calendars */
.ui-datepicker.ui-datepicker-multi { width:auto; }
.ui-datepicker-multi .ui-datepicker-group { float:left; }
.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; }
.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; }
.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; }
.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; }
.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
.ui-datepicker-row-break { clear:both; width:100%; font-size:0em; }
/* RTL support */
.ui-datepicker-rtl { direction: rtl; }
.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
.ui-datepicker-rtl .ui-datepicker-group { float:right; }
.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
.ui-datepicker-cover {
display: none; /*sorry for IE5*/
display/**/: block; /*sorry for IE5*/
position: absolute; /*must have*/
z-index: -1; /*must have*/
filter: mask(); /*must have*/
top: -4px; /*must have*/
left: -4px; /*must have*/
width: 200px; /*must have*/
height: 200px; /*must have*/
}/*
* jQuery UI Progressbar 1.8.17
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Progressbar#theming
*/
.ui-progressbar { height:2em; text-align: left; overflow: hidden; }
.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; }

View file

@ -1,31 +0,0 @@
.qq-uploader { position:relative; width: 100%;}
.qq-upload-button {
display:block; /* or inline-block */
width: 105px; padding: 7px 0; text-align:center;
background:#880000; border-bottom:1px solid #ddd;color:#fff;
}
.qq-upload-button-hover {background:#cc0000;}
.qq-upload-button-focus {outline:1px dotted black;}
.qq-upload-drop-area {
position:absolute; top:0; left:0; width:100%; height:100%; min-height: 70px; z-index:2;
background:#FF9797; text-align:center;
}
.qq-upload-drop-area span {
display:block; position:absolute; top: 50%; width:100%; margin-top:-8px; font-size:16px;
}
.qq-upload-drop-area-active {background:#FF7171;}
.qq-upload-list {margin:15px 35px; padding:0; list-style:disc;}
.qq-upload-list li { margin:0; padding:0; line-height:15px; font-size:12px;}
.qq-upload-file, .qq-upload-spinner, .qq-upload-size, .qq-upload-cancel, .qq-upload-failed-text {
margin-right: 7px;
}
.qq-upload-file {}
.qq-upload-spinner {display:inline-block; background: url("loading.gif"); width:15px; height:15px; vertical-align:text-bottom;}
.qq-upload-size,.qq-upload-cancel {font-size:11px;}
.qq-upload-failed-text {display:none;}
.qq-upload-fail .qq-upload-failed-text {display:inline;}

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 B

View file

@ -1,223 +0,0 @@
// lib/handlebars/base.js
var Handlebars = {};
Handlebars.VERSION = "1.0.beta.6";
Handlebars.helpers = {};
Handlebars.partials = {};
Handlebars.registerHelper = function(name, fn, inverse) {
if(inverse) { fn.not = inverse; }
this.helpers[name] = fn;
};
Handlebars.registerPartial = function(name, str) {
this.partials[name] = str;
};
Handlebars.registerHelper('helperMissing', function(arg) {
if(arguments.length === 2) {
return undefined;
} else {
throw new Error("Could not find property '" + arg + "'");
}
});
var toString = Object.prototype.toString, functionType = "[object Function]";
Handlebars.registerHelper('blockHelperMissing', function(context, options) {
var inverse = options.inverse || function() {}, fn = options.fn;
var ret = "";
var type = toString.call(context);
if(type === functionType) { context = context.call(this); }
if(context === true) {
return fn(this);
} else if(context === false || context == null) {
return inverse(this);
} else if(type === "[object Array]") {
if(context.length > 0) {
for(var i=0, j=context.length; i<j; i++) {
ret = ret + fn(context[i]);
}
} else {
ret = inverse(this);
}
return ret;
} else {
return fn(context);
}
});
Handlebars.registerHelper('each', function(context, options) {
var fn = options.fn, inverse = options.inverse;
var ret = "";
if(context && context.length > 0) {
for(var i=0, j=context.length; i<j; i++) {
ret = ret + fn(context[i]);
}
} else {
ret = inverse(this);
}
return ret;
});
Handlebars.registerHelper('if', function(context, options) {
var type = toString.call(context);
if(type === functionType) { context = context.call(this); }
if(!context || Handlebars.Utils.isEmpty(context)) {
return options.inverse(this);
} else {
return options.fn(this);
}
});
Handlebars.registerHelper('unless', function(context, options) {
var fn = options.fn, inverse = options.inverse;
options.fn = inverse;
options.inverse = fn;
return Handlebars.helpers['if'].call(this, context, options);
});
Handlebars.registerHelper('with', function(context, options) {
return options.fn(context);
});
Handlebars.registerHelper('log', function(context) {
Handlebars.log(context);
});
;
// lib/handlebars/utils.js
Handlebars.Exception = function(message) {
var tmp = Error.prototype.constructor.apply(this, arguments);
for (var p in tmp) {
if (tmp.hasOwnProperty(p)) { this[p] = tmp[p]; }
}
this.message = tmp.message;
};
Handlebars.Exception.prototype = new Error;
// Build out our basic SafeString type
Handlebars.SafeString = function(string) {
this.string = string;
};
Handlebars.SafeString.prototype.toString = function() {
return this.string.toString();
};
(function() {
var escape = {
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
"`": "&#x60;"
};
var badChars = /&(?!\w+;)|[<>"'`]/g;
var possible = /[&<>"'`]/;
var escapeChar = function(chr) {
return escape[chr] || "&amp;";
};
Handlebars.Utils = {
escapeExpression: function(string) {
// don't escape SafeStrings, since they're already safe
if (string instanceof Handlebars.SafeString) {
return string.toString();
} else if (string == null || string === false) {
return "";
}
if(!possible.test(string)) { return string; }
return string.replace(badChars, escapeChar);
},
isEmpty: function(value) {
if (typeof value === "undefined") {
return true;
} else if (value === null) {
return true;
} else if (value === false) {
return true;
} else if(Object.prototype.toString.call(value) === "[object Array]" && value.length === 0) {
return true;
} else {
return false;
}
}
};
})();;
// lib/handlebars/runtime.js
Handlebars.VM = {
template: function(templateSpec) {
// Just add water
var container = {
escapeExpression: Handlebars.Utils.escapeExpression,
invokePartial: Handlebars.VM.invokePartial,
programs: [],
program: function(i, fn, data) {
var programWrapper = this.programs[i];
if(data) {
return Handlebars.VM.program(fn, data);
} else if(programWrapper) {
return programWrapper;
} else {
programWrapper = this.programs[i] = Handlebars.VM.program(fn);
return programWrapper;
}
},
programWithDepth: Handlebars.VM.programWithDepth,
noop: Handlebars.VM.noop
};
return function(context, options) {
options = options || {};
return templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data);
};
},
programWithDepth: function(fn, data, $depth) {
var args = Array.prototype.slice.call(arguments, 2);
return function(context, options) {
options = options || {};
return fn.apply(this, [context, options.data || data].concat(args));
};
},
program: function(fn, data) {
return function(context, options) {
options = options || {};
return fn(context, options.data || data);
};
},
noop: function() { return ""; },
invokePartial: function(partial, name, context, helpers, partials, data) {
options = { helpers: helpers, partials: partials, data: data };
if(partial === undefined) {
throw new Handlebars.Exception("The partial " + name + " could not be found");
} else if(partial instanceof Function) {
return partial(context, options);
} else if (!Handlebars.compile) {
throw new Handlebars.Exception("The partial " + name + " could not be compiled when running in runtime-only mode");
} else {
partials[name] = Handlebars.compile(partial);
return partials[name](context, options);
}
}
};
Handlebars.template = Handlebars.VM.template;
;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

3750
public/vendor/socket.io/socket.io.js generated vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,80 +0,0 @@
/** @license
*
* SoundManager 2: JavaScript Sound for the Web
* ----------------------------------------------
* http://schillmania.com/projects/soundmanager2/
*
* Copyright (c) 2007, Scott Schiller. All rights reserved.
* Code provided under the BSD License:
* http://schillmania.com/projects/soundmanager2/license.txt
*
* V2.97a.20120624
*/
(function(ea){function Q(Q,da){function R(a){return c.preferFlash&&t&&!c.ignoreFlash&&"undefined"!==typeof c.flash[a]&&c.flash[a]}function m(a){return function(c){var d=this._t;return!d||!d._a?null:a.call(this,c)}}this.setupOptions={url:Q||null,flashVersion:8,debugMode:!0,debugFlash:!1,useConsole:!0,consoleOnly:!0,waitForWindowLoad:!1,bgColor:"#ffffff",useHighPerformance:!1,flashPollingInterval:null,html5PollingInterval:null,flashLoadTimeout:1E3,wmode:null,allowScriptAccess:"always",useFlashBlock:!1,
useHTML5Audio:!0,html5Test:/^(probably|maybe)$/i,preferFlash:!0,noSWFCache:!1};this.defaultOptions={autoLoad:!1,autoPlay:!1,from:null,loops:1,onid3:null,onload:null,whileloading:null,onplay:null,onpause:null,onresume:null,whileplaying:null,onposition:null,onstop:null,onfailure:null,onfinish:null,multiShot:!0,multiShotEvents:!1,position:null,pan:0,stream:!0,to:null,type:null,usePolicyFile:!1,volume:100};this.flash9Options={isMovieStar:null,usePeakData:!1,useWaveformData:!1,useEQData:!1,onbufferchange:null,
ondataerror:null};this.movieStarOptions={bufferTime:3,serverURL:null,onconnect:null,duration:null};this.audioFormats={mp3:{type:['audio/mpeg; codecs="mp3"',"audio/mpeg","audio/mp3","audio/MPA","audio/mpa-robust"],required:!0},mp4:{related:["aac","m4a"],type:['audio/mp4; codecs="mp4a.40.2"',"audio/aac","audio/x-m4a","audio/MP4A-LATM","audio/mpeg4-generic"],required:!1},ogg:{type:["audio/ogg; codecs=vorbis"],required:!1},wav:{type:['audio/wav; codecs="1"',"audio/wav","audio/wave","audio/x-wav"],required:!1}};
this.movieID="sm2-container";this.id=da||"sm2movie";this.debugID="soundmanager-debug";this.debugURLParam=/([#?&])debug=1/i;this.versionNumber="V2.97a.20120624";this.altURL=this.movieURL=this.version=null;this.enabled=this.swfLoaded=!1;this.oMC=null;this.sounds={};this.soundIDs=[];this.didFlashBlock=this.muted=!1;this.filePattern=null;this.filePatterns={flash8:/\.mp3(\?.*)?$/i,flash9:/\.mp3(\?.*)?$/i};this.features={buffering:!1,peakData:!1,waveformData:!1,eqData:!1,movieStar:!1};this.sandbox={};var fa;
try{fa="undefined"!==typeof Audio&&"undefined"!==typeof(new Audio).canPlayType}catch(Za){fa=!1}this.hasHTML5=fa;this.html5={usingFlash:null};this.flash={};this.ignoreFlash=this.html5Only=!1;var Ca,c=this,i=null,S,q=navigator.userAgent,h=ea,ga=h.location.href.toString(),l=document,ha,Da,ia,j,w=[],J=!1,K=!1,k=!1,s=!1,ja=!1,L,r,ka,T,la,B,C,D,Ea,ma,U,V,E,na,oa,pa,W,F,Fa,qa,Ga,X,Ha,M=null,ra=null,u,sa,G,Y,Z,H,p,N=!1,ta=!1,Ia,Ja,Ka,$=0,O=null,aa,n=null,La,ba,P,x,ua,va,Ma,o,Wa=Array.prototype.slice,z=!1,
t,wa,Na,v,Oa,xa=q.match(/(ipad|iphone|ipod)/i),y=q.match(/msie/i),Xa=q.match(/webkit/i),ya=q.match(/safari/i)&&!q.match(/chrome/i),Pa=q.match(/opera/i),za=q.match(/(mobile|pre\/|xoom)/i)||xa,Qa=!ga.match(/usehtml5audio/i)&&!ga.match(/sm2\-ignorebadua/i)&&ya&&!q.match(/silk/i)&&q.match(/OS X 10_6_([3-7])/i),Aa="undefined"!==typeof l.hasFocus?l.hasFocus():null,ca=ya&&("undefined"===typeof l.hasFocus||!l.hasFocus()),Ra=!ca,Sa=/(mp3|mp4|mpa|m4a)/i,Ba=l.location?l.location.protocol.match(/http/i):null,
Ta=!Ba?"http://":"",Ua=/^\s*audio\/(?:x-)?(?:mpeg4|aac|flv|mov|mp4||m4v|m4a|mp4v|3gp|3g2)\s*(?:$|;)/i,Va="mpeg4,aac,flv,mov,mp4,m4v,f4v,m4a,mp4v,3gp,3g2".split(","),Ya=RegExp("\\.("+Va.join("|")+")(\\?.*)?$","i");this.mimePattern=/^\s*audio\/(?:x-)?(?:mp(?:eg|3))\s*(?:$|;)/i;this.useAltURL=!Ba;this._global_a=null;if(za&&(c.useHTML5Audio=!0,c.preferFlash=!1,xa))z=c.ignoreFlash=!0;this.setup=function(a){"undefined"!==typeof a&&k&&n&&c.ok()&&("undefined"!==typeof a.flashVersion||"undefined"!==typeof a.url)&&
H(u("setupLate"));ka(a);return c};this.supported=this.ok=function(){return n?k&&!s:c.useHTML5Audio&&c.hasHTML5};this.getMovie=function(a){return S(a)||l[a]||h[a]};this.createSound=function(a,e){function d(){b=Y(b);c.sounds[f.id]=new Ca(f);c.soundIDs.push(f.id);return c.sounds[f.id]}var b=null,g=null,f=null;if(!k||!c.ok())return H(void 0),!1;"undefined"!==typeof e&&(a={id:a,url:e});b=r(a);b.url=aa(b.url);f=b;if(p(f.id,!0))return c.sounds[f.id];if(ba(f))g=d(),g._setup_html5(f);else{if(8<j&&null===f.isMovieStar)f.isMovieStar=
!(!f.serverURL&&!(f.type&&f.type.match(Ua)||f.url.match(Ya)));f=Z(f,void 0);g=d();if(8===j)i._createSound(f.id,f.loops||1,f.usePolicyFile);else if(i._createSound(f.id,f.url,f.usePeakData,f.useWaveformData,f.useEQData,f.isMovieStar,f.isMovieStar?f.bufferTime:!1,f.loops||1,f.serverURL,f.duration||null,f.autoPlay,!0,f.autoLoad,f.usePolicyFile),!f.serverURL)g.connected=!0,f.onconnect&&f.onconnect.apply(g);!f.serverURL&&(f.autoLoad||f.autoPlay)&&g.load(f)}!f.serverURL&&f.autoPlay&&g.play();return g};this.destroySound=
function(a,e){if(!p(a))return!1;var d=c.sounds[a],b;d._iO={};d.stop();d.unload();for(b=0;b<c.soundIDs.length;b++)if(c.soundIDs[b]===a){c.soundIDs.splice(b,1);break}e||d.destruct(!0);delete c.sounds[a];return!0};this.load=function(a,e){return!p(a)?!1:c.sounds[a].load(e)};this.unload=function(a){return!p(a)?!1:c.sounds[a].unload()};this.onposition=this.onPosition=function(a,e,d,b){return!p(a)?!1:c.sounds[a].onposition(e,d,b)};this.clearOnPosition=function(a,e,d){return!p(a)?!1:c.sounds[a].clearOnPosition(e,
d)};this.start=this.play=function(a,e){var d=!1;if(!k||!c.ok())return H("soundManager.play(): "+u(!k?"notReady":"notOK")),d;if(!p(a)){e instanceof Object||(e={url:e});if(e&&e.url)e.id=a,d=c.createSound(e).play();return d}return c.sounds[a].play(e)};this.setPosition=function(a,e){return!p(a)?!1:c.sounds[a].setPosition(e)};this.stop=function(a){return!p(a)?!1:c.sounds[a].stop()};this.stopAll=function(){for(var a in c.sounds)c.sounds.hasOwnProperty(a)&&c.sounds[a].stop()};this.pause=function(a){return!p(a)?
!1:c.sounds[a].pause()};this.pauseAll=function(){var a;for(a=c.soundIDs.length-1;0<=a;a--)c.sounds[c.soundIDs[a]].pause()};this.resume=function(a){return!p(a)?!1:c.sounds[a].resume()};this.resumeAll=function(){var a;for(a=c.soundIDs.length-1;0<=a;a--)c.sounds[c.soundIDs[a]].resume()};this.togglePause=function(a){return!p(a)?!1:c.sounds[a].togglePause()};this.setPan=function(a,e){return!p(a)?!1:c.sounds[a].setPan(e)};this.setVolume=function(a,e){return!p(a)?!1:c.sounds[a].setVolume(e)};this.mute=function(a){var e=
0;"string"!==typeof a&&(a=null);if(a)return!p(a)?!1:c.sounds[a].mute();for(e=c.soundIDs.length-1;0<=e;e--)c.sounds[c.soundIDs[e]].mute();return c.muted=!0};this.muteAll=function(){c.mute()};this.unmute=function(a){"string"!==typeof a&&(a=null);if(a)return!p(a)?!1:c.sounds[a].unmute();for(a=c.soundIDs.length-1;0<=a;a--)c.sounds[c.soundIDs[a]].unmute();c.muted=!1;return!0};this.unmuteAll=function(){c.unmute()};this.toggleMute=function(a){return!p(a)?!1:c.sounds[a].toggleMute()};this.getMemoryUse=function(){var a=
0;i&&8!==j&&(a=parseInt(i._getMemoryUse(),10));return a};this.disable=function(a){var e;"undefined"===typeof a&&(a=!1);if(s)return!1;s=!0;for(e=c.soundIDs.length-1;0<=e;e--)Ga(c.sounds[c.soundIDs[e]]);L(a);o.remove(h,"load",C);return!0};this.canPlayMIME=function(a){var e;c.hasHTML5&&(e=P({type:a}));!e&&n&&(e=a&&c.ok()?!!(8<j&&a.match(Ua)||a.match(c.mimePattern)):null);return e};this.canPlayURL=function(a){var e;c.hasHTML5&&(e=P({url:a}));!e&&n&&(e=a&&c.ok()?!!a.match(c.filePattern):null);return e};
this.canPlayLink=function(a){return"undefined"!==typeof a.type&&a.type&&c.canPlayMIME(a.type)?!0:c.canPlayURL(a.href)};this.getSoundById=function(a){if(!a)throw Error("soundManager.getSoundById(): sID is null/undefined");return c.sounds[a]};this.onready=function(a,c){var d=!1;if("function"===typeof a)c||(c=h),la("onready",a,c),B();else throw u("needFunction","onready");return!0};this.ontimeout=function(a,c){var d=!1;if("function"===typeof a)c||(c=h),la("ontimeout",a,c),B({type:"ontimeout"});else throw u("needFunction",
"ontimeout");return!0};this._wD=this._writeDebug=function(){return!0};this._debug=function(){};this.reboot=function(){var a,e;for(a=c.soundIDs.length-1;0<=a;a--)c.sounds[c.soundIDs[a]].destruct();if(i)try{if(y)ra=i.innerHTML;M=i.parentNode.removeChild(i)}catch(d){}ra=M=n=null;c.enabled=oa=k=N=ta=J=K=s=c.swfLoaded=!1;c.soundIDs=[];c.sounds={};i=null;for(a in w)if(w.hasOwnProperty(a))for(e=w[a].length-1;0<=e;e--)w[a][e].fired=!1;h.setTimeout(c.beginDelayedInit,20)};this.getMoviePercent=function(){return i&&
"undefined"!==typeof i.PercentLoaded?i.PercentLoaded():null};this.beginDelayedInit=function(){ja=!0;E();setTimeout(function(){if(ta)return!1;W();V();return ta=!0},20);D()};this.destruct=function(){c.disable(!0)};Ca=function(a){var e,d,b=this,g,f,A,I,h,l,m=!1,k=[],o=0,q,s,n=null;e=null;d=null;this.sID=this.id=a.id;this.url=a.url;this._iO=this.instanceOptions=this.options=r(a);this.pan=this.options.pan;this.volume=this.options.volume;this.isHTML5=!1;this._a=null;this.id3={};this._debug=function(){};
this.load=function(a){var c=null;if("undefined"!==typeof a)b._iO=r(a,b.options),b.instanceOptions=b._iO;else if(a=b.options,b._iO=a,b.instanceOptions=b._iO,n&&n!==b.url)b._iO.url=b.url,b.url=null;if(!b._iO.url)b._iO.url=b.url;b._iO.url=aa(b._iO.url);if(b._iO.url===b.url&&0!==b.readyState&&2!==b.readyState)return 3===b.readyState&&b._iO.onload&&b._iO.onload.apply(b,[!!b.duration]),b;a=b._iO;n=b.url;b.loaded=!1;b.readyState=1;b.playState=0;b.id3={};if(ba(a)){if(c=b._setup_html5(a),!c._called_load){b._html5_canplay=
!1;if(b._a.src!==a.url)b._a.src=a.url,b.setPosition(0);b._a.autobuffer="auto";b._a.preload="auto";c._called_load=!0;a.autoPlay&&b.play()}}else try{b.isHTML5=!1,b._iO=Z(Y(a)),a=b._iO,8===j?i._load(b.id,a.url,a.stream,a.autoPlay,a.whileloading?1:0,a.loops||1,a.usePolicyFile):i._load(b.id,a.url,!!a.stream,!!a.autoPlay,a.loops||1,!!a.autoLoad,a.usePolicyFile)}catch(e){F({type:"SMSOUND_LOAD_JS_EXCEPTION",fatal:!0})}return b};this.unload=function(){if(0!==b.readyState){if(b.isHTML5){if(I(),b._a)b._a.pause(),
ua(b._a,"about:blank"),b.url="about:blank"}else 8===j?i._unload(b.id,"about:blank"):i._unload(b.id);g()}return b};this.destruct=function(a){if(b.isHTML5){if(I(),b._a)b._a.pause(),ua(b._a),z||A(),b._a._t=null,b._a=null}else b._iO.onfailure=null,i._destroySound(b.id);a||c.destroySound(b.id,!0)};this.start=this.play=function(a,c){var e,d;d=!0;d=null;c="undefined"===typeof c?!0:c;a||(a={});b._iO=r(a,b._iO);b._iO=r(b._iO,b.options);b._iO.url=aa(b._iO.url);b.instanceOptions=b._iO;if(b._iO.serverURL&&!b.connected)return b.getAutoPlay()||
b.setAutoPlay(!0),b;ba(b._iO)&&(b._setup_html5(b._iO),h());if(1===b.playState&&!b.paused)(e=b._iO.multiShot)||(d=b);if(null!==d)return d;if(!b.loaded)if(0===b.readyState){if(!b.isHTML5)b._iO.autoPlay=!0;b.load(b._iO)}else 2===b.readyState&&(d=b);if(null!==d)return d;if(!b.isHTML5&&9===j&&0<b.position&&b.position===b.duration)a.position=0;if(b.paused&&b.position&&0<b.position)b.resume();else{b._iO=r(a,b._iO);if(null!==b._iO.from&&null!==b._iO.to&&0===b.instanceCount&&0===b.playState&&!b._iO.serverURL){e=
function(){b._iO=r(a,b._iO);b.play(b._iO)};if(b.isHTML5&&!b._html5_canplay)b.load({_oncanplay:e}),d=!1;else if(!b.isHTML5&&!b.loaded&&(!b.readyState||2!==b.readyState))b.load({onload:e}),d=!1;if(null!==d)return d;b._iO=s()}(!b.instanceCount||b._iO.multiShotEvents||!b.isHTML5&&8<j&&!b.getAutoPlay())&&b.instanceCount++;b._iO.onposition&&0===b.playState&&l(b);b.playState=1;b.paused=!1;b.position="undefined"!==typeof b._iO.position&&!isNaN(b._iO.position)?b._iO.position:0;if(!b.isHTML5)b._iO=Z(Y(b._iO));
b._iO.onplay&&c&&(b._iO.onplay.apply(b),m=!0);b.setVolume(b._iO.volume,!0);b.setPan(b._iO.pan,!0);b.isHTML5?(h(),d=b._setup_html5(),b.setPosition(b._iO.position),d.play()):(d=i._start(b.id,b._iO.loops||1,9===j?b._iO.position:b._iO.position/1E3,b._iO.multiShot),9===j&&!d&&b._iO.onplayerror&&b._iO.onplayerror.apply(b))}return b};this.stop=function(a){var c=b._iO;if(1===b.playState){b._onbufferchange(0);b._resetOnPosition(0);b.paused=!1;if(!b.isHTML5)b.playState=0;q();c.to&&b.clearOnPosition(c.to);if(b.isHTML5){if(b._a)a=
b.position,b.setPosition(0),b.position=a,b._a.pause(),b.playState=0,b._onTimer(),I()}else i._stop(b.id,a),c.serverURL&&b.unload();b.instanceCount=0;b._iO={};c.onstop&&c.onstop.apply(b)}return b};this.setAutoPlay=function(a){b._iO.autoPlay=a;b.isHTML5||(i._setAutoPlay(b.id,a),a&&!b.instanceCount&&1===b.readyState&&b.instanceCount++)};this.getAutoPlay=function(){return b._iO.autoPlay};this.setPosition=function(a){"undefined"===typeof a&&(a=0);var c=b.isHTML5?Math.max(a,0):Math.min(b.duration||b._iO.duration,
Math.max(a,0));b.position=c;a=b.position/1E3;b._resetOnPosition(b.position);b._iO.position=c;if(b.isHTML5){if(b._a&&b._html5_canplay&&b._a.currentTime!==a)try{b._a.currentTime=a,(0===b.playState||b.paused)&&b._a.pause()}catch(e){}}else a=9===j?b.position:a,b.readyState&&2!==b.readyState&&i._setPosition(b.id,a,b.paused||!b.playState,b._iO.multiShot);b.isHTML5&&b.paused&&b._onTimer(!0);return b};this.pause=function(a){if(b.paused||0===b.playState&&1!==b.readyState)return b;b.paused=!0;b.isHTML5?(b._setup_html5().pause(),
I()):(a||"undefined"===typeof a)&&i._pause(b.id,b._iO.multiShot);b._iO.onpause&&b._iO.onpause.apply(b);return b};this.resume=function(){var a=b._iO;if(!b.paused)return b;b.paused=!1;b.playState=1;b.isHTML5?(b._setup_html5().play(),h()):(a.isMovieStar&&!a.serverURL&&b.setPosition(b.position),i._pause(b.id,a.multiShot));!m&&a.onplay?(a.onplay.apply(b),m=!0):a.onresume&&a.onresume.apply(b);return b};this.togglePause=function(){if(0===b.playState)return b.play({position:9===j&&!b.isHTML5?b.position:b.position/
1E3}),b;b.paused?b.resume():b.pause();return b};this.setPan=function(a,c){"undefined"===typeof a&&(a=0);"undefined"===typeof c&&(c=!1);b.isHTML5||i._setPan(b.id,a);b._iO.pan=a;if(!c)b.pan=a,b.options.pan=a;return b};this.setVolume=function(a,e){"undefined"===typeof a&&(a=100);"undefined"===typeof e&&(e=!1);if(b.isHTML5){if(b._a)b._a.volume=Math.max(0,Math.min(1,a/100))}else i._setVolume(b.id,c.muted&&!b.muted||b.muted?0:a);b._iO.volume=a;if(!e)b.volume=a,b.options.volume=a;return b};this.mute=function(){b.muted=
!0;if(b.isHTML5){if(b._a)b._a.muted=!0}else i._setVolume(b.id,0);return b};this.unmute=function(){b.muted=!1;var a="undefined"!==typeof b._iO.volume;if(b.isHTML5){if(b._a)b._a.muted=!1}else i._setVolume(b.id,a?b._iO.volume:b.options.volume);return b};this.toggleMute=function(){return b.muted?b.unmute():b.mute()};this.onposition=this.onPosition=function(a,c,e){k.push({position:parseInt(a,10),method:c,scope:"undefined"!==typeof e?e:b,fired:!1});return b};this.clearOnPosition=function(b,a){var c,b=parseInt(b,
10);if(isNaN(b))return!1;for(c=0;c<k.length;c++)if(b===k[c].position&&(!a||a===k[c].method))k[c].fired&&o--,k.splice(c,1)};this._processOnPosition=function(){var a,c;a=k.length;if(!a||!b.playState||o>=a)return!1;for(a-=1;0<=a;a--)if(c=k[a],!c.fired&&b.position>=c.position)c.fired=!0,o++,c.method.apply(c.scope,[c.position]);return!0};this._resetOnPosition=function(b){var a,c;a=k.length;if(!a)return!1;for(a-=1;0<=a;a--)if(c=k[a],c.fired&&b<=c.position)c.fired=!1,o--;return!0};s=function(){var a=b._iO,
c=a.from,e=a.to,d,f;f=function(){b.clearOnPosition(e,f);b.stop()};d=function(){if(null!==e&&!isNaN(e))b.onPosition(e,f)};if(null!==c&&!isNaN(c))a.position=c,a.multiShot=!1,d();return a};l=function(){var a,c=b._iO.onposition;if(c)for(a in c)if(c.hasOwnProperty(a))b.onPosition(parseInt(a,10),c[a])};q=function(){var a,c=b._iO.onposition;if(c)for(a in c)c.hasOwnProperty(a)&&b.clearOnPosition(parseInt(a,10))};h=function(){b.isHTML5&&Ia(b)};I=function(){b.isHTML5&&Ja(b)};g=function(a){a||(k=[],o=0);m=!1;
b._hasTimer=null;b._a=null;b._html5_canplay=!1;b.bytesLoaded=null;b.bytesTotal=null;b.duration=b._iO&&b._iO.duration?b._iO.duration:null;b.durationEstimate=null;b.buffered=[];b.eqData=[];b.eqData.left=[];b.eqData.right=[];b.failures=0;b.isBuffering=!1;b.instanceOptions={};b.instanceCount=0;b.loaded=!1;b.metadata={};b.readyState=0;b.muted=!1;b.paused=!1;b.peakData={left:0,right:0};b.waveformData={left:[],right:[]};b.playState=0;b.position=null;b.id3={}};g();this._onTimer=function(a){var c,f=!1,g={};
if(b._hasTimer||a){if(b._a&&(a||(0<b.playState||1===b.readyState)&&!b.paused)){c=b._get_html5_duration();if(c!==e)e=c,b.duration=c,f=!0;b.durationEstimate=b.duration;c=1E3*b._a.currentTime||0;c!==d&&(d=c,f=!0);(f||a)&&b._whileplaying(c,g,g,g,g)}return f}};this._get_html5_duration=function(){var a=b._iO,c=b._a?1E3*b._a.duration:a?a.duration:void 0;return c&&!isNaN(c)&&Infinity!==c?c:a?a.duration:null};this._apply_loop=function(b,a){b.loop=1<a?"loop":""};this._setup_html5=function(a){var a=r(b._iO,
a),e=decodeURI,d=z?c._global_a:b._a,i=e(a.url),h=d&&d._t?d._t.instanceOptions:null,A;if(d){if(d._t){if(!z&&i===e(n))A=d;else if(z&&h.url===a.url&&(!n||n===h.url))A=d;if(A)return b._apply_loop(d,a.loops),A}z&&d._t&&d._t.playState&&a.url!==h.url&&d._t.stop();g(h&&h.url?a.url===h.url:n?n===a.url:!1);d.src=a.url;n=b.url=a.url;d._called_load=!1}else if(b._a=a.autoLoad||a.autoPlay?new Audio(a.url):Pa?new Audio(null):new Audio,d=b._a,d._called_load=!1,z)c._global_a=d;b.isHTML5=!0;b._a=d;d._t=b;f();b._apply_loop(d,
a.loops);a.autoLoad||a.autoPlay?b.load():(d.autobuffer=!1,d.preload="auto");return d};f=function(){if(b._a._added_events)return!1;var a;b._a._added_events=!0;for(a in v)v.hasOwnProperty(a)&&b._a&&b._a.addEventListener(a,v[a],!1);return!0};A=function(){var a;b._a._added_events=!1;for(a in v)v.hasOwnProperty(a)&&b._a&&b._a.removeEventListener(a,v[a],!1)};this._onload=function(a){a=!!a||!b.isHTML5&&8===j&&b.duration;b.loaded=a;b.readyState=a?3:2;b._onbufferchange(0);b._iO.onload&&b._iO.onload.apply(b,
[a]);return!0};this._onbufferchange=function(a){if(0===b.playState||a&&b.isBuffering||!a&&!b.isBuffering)return!1;b.isBuffering=1===a;b._iO.onbufferchange&&b._iO.onbufferchange.apply(b);return!0};this._onsuspend=function(){b._iO.onsuspend&&b._iO.onsuspend.apply(b);return!0};this._onfailure=function(a,c,e){b.failures++;if(b._iO.onfailure&&1===b.failures)b._iO.onfailure(b,a,c,e)};this._onfinish=function(){var a=b._iO.onfinish;b._onbufferchange(0);b._resetOnPosition(0);if(b.instanceCount){b.instanceCount--;
if(!b.instanceCount&&(q(),b.playState=0,b.paused=!1,b.instanceCount=0,b.instanceOptions={},b._iO={},I(),b.isHTML5))b.position=0;(!b.instanceCount||b._iO.multiShotEvents)&&a&&a.apply(b)}};this._whileloading=function(a,c,e,d){var f=b._iO;b.bytesLoaded=a;b.bytesTotal=c;b.duration=Math.floor(e);b.bufferLength=d;if(f.isMovieStar)b.durationEstimate=b.duration;else if(b.durationEstimate=f.duration?b.duration>f.duration?b.duration:f.duration:parseInt(b.bytesTotal/b.bytesLoaded*b.duration,10),"undefined"===
typeof b.durationEstimate)b.durationEstimate=b.duration;if(!b.isHTML5)b.buffered=[{start:0,end:b.duration}];(3!==b.readyState||b.isHTML5)&&f.whileloading&&f.whileloading.apply(b)};this._whileplaying=function(a,c,e,d,f){var g=b._iO;if(isNaN(a)||null===a)return!1;b.position=Math.max(0,a);b._processOnPosition();if(!b.isHTML5&&8<j){if(g.usePeakData&&"undefined"!==typeof c&&c)b.peakData={left:c.leftPeak,right:c.rightPeak};if(g.useWaveformData&&"undefined"!==typeof e&&e)b.waveformData={left:e.split(","),
right:d.split(",")};if(g.useEQData&&"undefined"!==typeof f&&f&&f.leftEQ&&(a=f.leftEQ.split(","),b.eqData=a,b.eqData.left=a,"undefined"!==typeof f.rightEQ&&f.rightEQ))b.eqData.right=f.rightEQ.split(",")}1===b.playState&&(!b.isHTML5&&8===j&&!b.position&&b.isBuffering&&b._onbufferchange(0),g.whileplaying&&g.whileplaying.apply(b));return!0};this._oncaptiondata=function(a){b.captiondata=a;b._iO.oncaptiondata&&b._iO.oncaptiondata.apply(b)};this._onmetadata=function(a,c){var e={},d,f;for(d=0,f=a.length;d<
f;d++)e[a[d]]=c[d];b.metadata=e;b._iO.onmetadata&&b._iO.onmetadata.apply(b)};this._onid3=function(a,c){var e=[],d,f;for(d=0,f=a.length;d<f;d++)e[a[d]]=c[d];b.id3=r(b.id3,e);b._iO.onid3&&b._iO.onid3.apply(b)};this._onconnect=function(a){a=1===a;if(b.connected=a)b.failures=0,p(b.id)&&(b.getAutoPlay()?b.play(void 0,b.getAutoPlay()):b._iO.autoLoad&&b.load()),b._iO.onconnect&&b._iO.onconnect.apply(b,[a])};this._ondataerror=function(){0<b.playState&&b._iO.ondataerror&&b._iO.ondataerror.apply(b)}};pa=function(){return l.body||
l._docElement||l.getElementsByTagName("div")[0]};S=function(a){return l.getElementById(a)};r=function(a,e){var d=a||{},b,g;b="undefined"===typeof e?c.defaultOptions:e;for(g in b)b.hasOwnProperty(g)&&"undefined"===typeof d[g]&&(d[g]="object"!==typeof b[g]||null===b[g]?b[g]:r(d[g],b[g]));return d};T={onready:1,ontimeout:1,defaultOptions:1,flash9Options:1,movieStarOptions:1};ka=function(a,e){var d,b=!0,g="undefined"!==typeof e,f=c.setupOptions;for(d in a)if(a.hasOwnProperty(d))if("object"!==typeof a[d]||
null===a[d]||a[d]instanceof Array)g&&"undefined"!==typeof T[e]?c[e][d]=a[d]:"undefined"!==typeof f[d]?(c.setupOptions[d]=a[d],c[d]=a[d]):"undefined"===typeof T[d]?(H(u("undefined"===typeof c[d]?"setupUndef":"setupError",d),2),b=!1):c[d]instanceof Function?c[d].apply(c,a[d]instanceof Array?a[d]:[a[d]]):c[d]=a[d];else if("undefined"===typeof T[d])H(u("undefined"===typeof c[d]?"setupUndef":"setupError",d),2),b=!1;else return ka(a[d],d);return b};o=function(){function a(a){var a=Wa.call(a),b=a.length;
d?(a[1]="on"+a[1],3<b&&a.pop()):3===b&&a.push(!1);return a}function c(a,e){var h=a.shift(),i=[b[e]];if(d)h[i](a[0],a[1]);else h[i].apply(h,a)}var d=h.attachEvent,b={add:d?"attachEvent":"addEventListener",remove:d?"detachEvent":"removeEventListener"};return{add:function(){c(a(arguments),"add")},remove:function(){c(a(arguments),"remove")}}}();v={abort:m(function(){}),canplay:m(function(){var a=this._t,c;if(a._html5_canplay)return!0;a._html5_canplay=!0;a._onbufferchange(0);c="undefined"!==typeof a._iO.position&&
!isNaN(a._iO.position)?a._iO.position/1E3:null;if(a.position&&this.currentTime!==c)try{this.currentTime=c}catch(d){}a._iO._oncanplay&&a._iO._oncanplay()}),canplaythrough:m(function(){var a=this._t;a.loaded||(a._onbufferchange(0),a._whileloading(a.bytesLoaded,a.bytesTotal,a._get_html5_duration()),a._onload(!0))}),ended:m(function(){this._t._onfinish()}),error:m(function(){this._t._onload(!1)}),loadeddata:m(function(){var a=this._t;if(!a._loaded&&!ya)a.duration=a._get_html5_duration()}),loadedmetadata:m(function(){}),
loadstart:m(function(){this._t._onbufferchange(1)}),play:m(function(){this._t._onbufferchange(0)}),playing:m(function(){this._t._onbufferchange(0)}),progress:m(function(a){var c=this._t,d,b,g=0,g=a.target.buffered;d=a.loaded||0;var f=a.total||1;c.buffered=[];if(g&&g.length){for(d=0,b=g.length;d<b;d++)c.buffered.push({start:g.start(d),end:g.end(d)});g=g.end(0)-g.start(0);d=g/a.target.duration}isNaN(d)||(c._onbufferchange(0),c._whileloading(d,f,c._get_html5_duration()),d&&f&&d===f&&v.canplaythrough.call(this,
a))}),ratechange:m(function(){}),suspend:m(function(a){var c=this._t;v.progress.call(this,a);c._onsuspend()}),stalled:m(function(){}),timeupdate:m(function(){this._t._onTimer()}),waiting:m(function(){this._t._onbufferchange(1)})};ba=function(a){return a.serverURL||a.type&&R(a.type)?!1:a.type?P({type:a.type}):P({url:a.url})||c.html5Only};ua=function(a,c){if(a)a.src=c};P=function(a){if(!c.useHTML5Audio||!c.hasHTML5)return!1;var e=a.url||null,a=a.type||null,d=c.audioFormats,b;if(a&&"undefined"!==typeof c.html5[a])return c.html5[a]&&
!R(a);if(!x){x=[];for(b in d)d.hasOwnProperty(b)&&(x.push(b),d[b].related&&(x=x.concat(d[b].related)));x=RegExp("\\.("+x.join("|")+")(\\?.*)?$","i")}b=e?e.toLowerCase().match(x):null;!b||!b.length?a&&(e=a.indexOf(";"),b=(-1!==e?a.substr(0,e):a).substr(6)):b=b[1];b&&"undefined"!==typeof c.html5[b]?e=c.html5[b]&&!R(b):(a="audio/"+b,e=c.html5.canPlayType({type:a}),e=(c.html5[b]=e)&&c.html5[a]&&!R(a));return e};Ma=function(){function a(a){var b,d,f=b=!1;if(!e||"function"!==typeof e.canPlayType)return b;
if(a instanceof Array){for(b=0,d=a.length;b<d&&!f;b++)if(c.html5[a[b]]||e.canPlayType(a[b]).match(c.html5Test))f=!0,c.html5[a[b]]=!0,c.flash[a[b]]=!!a[b].match(Sa);b=f}else a=e&&"function"===typeof e.canPlayType?e.canPlayType(a):!1,b=!(!a||!a.match(c.html5Test));return b}if(!c.useHTML5Audio||"undefined"===typeof Audio)return!1;var e="undefined"!==typeof Audio?Pa?new Audio(null):new Audio:null,d,b,g={},f;f=c.audioFormats;for(d in f)if(f.hasOwnProperty(d)&&(b="audio/"+d,g[d]=a(f[d].type),g[b]=g[d],
d.match(Sa)?(c.flash[d]=!0,c.flash[b]=!0):(c.flash[d]=!1,c.flash[b]=!1),f[d]&&f[d].related))for(b=f[d].related.length-1;0<=b;b--)g["audio/"+f[d].related[b]]=g[d],c.html5[f[d].related[b]]=g[d],c.flash[f[d].related[b]]=g[d];g.canPlayType=e?a:null;c.html5=r(c.html5,g);return!0};u=function(){};Y=function(a){if(8===j&&1<a.loops&&a.stream)a.stream=!1;return a};Z=function(a){if(a&&!a.usePolicyFile&&(a.onid3||a.usePeakData||a.useWaveformData||a.useEQData))a.usePolicyFile=!0;return a};H=function(){};ha=function(){return!1};
Ga=function(a){for(var c in a)a.hasOwnProperty(c)&&"function"===typeof a[c]&&(a[c]=ha)};X=function(a){"undefined"===typeof a&&(a=!1);(s||a)&&c.disable(a)};Ha=function(a){var e=null;if(a)if(a.match(/\.swf(\?.*)?$/i)){if(e=a.substr(a.toLowerCase().lastIndexOf(".swf?")+4))return a}else a.lastIndexOf("/")!==a.length-1&&(a+="/");a=(a&&-1!==a.lastIndexOf("/")?a.substr(0,a.lastIndexOf("/")+1):"./")+c.movieURL;c.noSWFCache&&(a+="?ts="+(new Date).getTime());return a};ma=function(){j=parseInt(c.flashVersion,
10);if(8!==j&&9!==j)c.flashVersion=j=8;var a=c.debugMode||c.debugFlash?"_debug.swf":".swf";if(c.useHTML5Audio&&!c.html5Only&&c.audioFormats.mp4.required&&9>j)c.flashVersion=j=9;c.version=c.versionNumber+(c.html5Only?" (HTML5-only mode)":9===j?" (AS3/Flash 9)":" (AS2/Flash 8)");8<j?(c.defaultOptions=r(c.defaultOptions,c.flash9Options),c.features.buffering=!0,c.defaultOptions=r(c.defaultOptions,c.movieStarOptions),c.filePatterns.flash9=RegExp("\\.(mp3|"+Va.join("|")+")(\\?.*)?$","i"),c.features.movieStar=
!0):c.features.movieStar=!1;c.filePattern=c.filePatterns[8!==j?"flash9":"flash8"];c.movieURL=(8===j?"soundmanager2.swf":"soundmanager2_flash9.swf").replace(".swf",a);c.features.peakData=c.features.waveformData=c.features.eqData=8<j};Fa=function(a,c){if(!i)return!1;i._setPolling(a,c)};qa=function(){if(c.debugURLParam.test(ga))c.debugMode=!0};p=this.getSoundById;G=function(){var a=[];c.debugMode&&a.push("sm2_debug");c.debugFlash&&a.push("flash_debug");c.useHighPerformance&&a.push("high_performance");
return a.join(" ")};sa=function(){u("fbHandler");var a=c.getMoviePercent(),e={type:"FLASHBLOCK"};if(c.html5Only)return!1;if(c.ok()){if(c.oMC)c.oMC.className=[G(),"movieContainer","swf_loaded"+(c.didFlashBlock?" swf_unblocked":"")].join(" ")}else{if(n)c.oMC.className=G()+" movieContainer "+(null===a?"swf_timedout":"swf_error");c.didFlashBlock=!0;B({type:"ontimeout",ignoreInit:!0,error:e});F(e)}};la=function(a,c,d){"undefined"===typeof w[a]&&(w[a]=[]);w[a].push({method:c,scope:d||null,fired:!1})};B=
function(a){a||(a={type:c.ok()?"onready":"ontimeout"});if(!k&&a&&!a.ignoreInit||"ontimeout"===a.type&&(c.ok()||s&&!a.ignoreInit))return!1;var e={success:a&&a.ignoreInit?c.ok():!s},d=a&&a.type?w[a.type]||[]:[],b=[],g,e=[e],f=n&&c.useFlashBlock&&!c.ok();if(a.error)e[0].error=a.error;for(a=0,g=d.length;a<g;a++)!0!==d[a].fired&&b.push(d[a]);if(b.length)for(a=0,g=b.length;a<g;a++)if(b[a].scope?b[a].method.apply(b[a].scope,e):b[a].method.apply(this,e),!f)b[a].fired=!0;return!0};C=function(){h.setTimeout(function(){c.useFlashBlock&&
sa();B();"function"===typeof c.onload&&c.onload.apply(h);c.waitForWindowLoad&&o.add(h,"load",C)},1)};wa=function(){if("undefined"!==typeof t)return t;var a=!1,c=navigator,d=c.plugins,b,g=h.ActiveXObject;if(d&&d.length)(c=c.mimeTypes)&&c["application/x-shockwave-flash"]&&c["application/x-shockwave-flash"].enabledPlugin&&c["application/x-shockwave-flash"].enabledPlugin.description&&(a=!0);else if("undefined"!==typeof g){try{b=new g("ShockwaveFlash.ShockwaveFlash")}catch(f){}a=!!b}return t=a};La=function(){var a,
e,d=c.audioFormats;if(xa&&q.match(/os (1|2|3_0|3_1)/i)){if(c.hasHTML5=!1,c.html5Only=!0,c.oMC)c.oMC.style.display="none"}else if(c.useHTML5Audio)c.hasHTML5=!c.html5||!c.html5.canPlayType?!1:!0;if(c.useHTML5Audio&&c.hasHTML5)for(e in d)if(d.hasOwnProperty(e)&&(d[e].required&&!c.html5.canPlayType(d[e].type)||c.preferFlash&&(c.flash[e]||c.flash[d[e].type])))a=!0;c.ignoreFlash&&(a=!1);c.html5Only=c.hasHTML5&&c.useHTML5Audio&&!a;return!c.html5Only};aa=function(a){var e,d,b=0;if(a instanceof Array){for(e=
0,d=a.length;e<d;e++)if(a[e]instanceof Object){if(c.canPlayMIME(a[e].type)){b=e;break}}else if(c.canPlayURL(a[e])){b=e;break}if(a[b].url)a[b]=a[b].url;a=a[b]}return a};Ia=function(a){if(!a._hasTimer)a._hasTimer=!0,!za&&c.html5PollingInterval&&(null===O&&0===$&&(O=h.setInterval(Ka,c.html5PollingInterval)),$++)};Ja=function(a){if(a._hasTimer)a._hasTimer=!1,!za&&c.html5PollingInterval&&$--};Ka=function(){var a;if(null!==O&&!$)return h.clearInterval(O),O=null,!1;for(a=c.soundIDs.length-1;0<=a;a--)c.sounds[c.soundIDs[a]].isHTML5&&
c.sounds[c.soundIDs[a]]._hasTimer&&c.sounds[c.soundIDs[a]]._onTimer()};F=function(a){a="undefined"!==typeof a?a:{};"function"===typeof c.onerror&&c.onerror.apply(h,[{type:"undefined"!==typeof a.type?a.type:null}]);"undefined"!==typeof a.fatal&&a.fatal&&c.disable()};Na=function(){if(!Qa||!wa())return!1;var a=c.audioFormats,e,d;for(d in a)if(a.hasOwnProperty(d)&&("mp3"===d||"mp4"===d))if(c.html5[d]=!1,a[d]&&a[d].related)for(e=a[d].related.length-1;0<=e;e--)c.html5[a[d].related[e]]=!1};this._setSandboxType=
function(){};this._externalInterfaceOK=function(){if(c.swfLoaded)return!1;(new Date).getTime();c.swfLoaded=!0;ca=!1;Qa&&Na();setTimeout(ia,y?100:1)};W=function(a,e){function d(a,b){return'<param name="'+a+'" value="'+b+'" />'}if(J&&K)return!1;if(c.html5Only)return ma(),c.oMC=S(c.movieID),ia(),K=J=!0,!1;var b=e||c.url,g=c.altURL||b,f;f=pa();var h,i,j=G(),k,m=null,m=(m=l.getElementsByTagName("html")[0])&&m.dir&&m.dir.match(/rtl/i),a="undefined"===typeof a?c.id:a;ma();c.url=Ha(Ba?b:g);e=c.url;c.wmode=
!c.wmode&&c.useHighPerformance?"transparent":c.wmode;if(null!==c.wmode&&(q.match(/msie 8/i)||!y&&!c.useHighPerformance)&&navigator.platform.match(/win32|win64/i))c.wmode=null;f={name:a,id:a,src:e,quality:"high",allowScriptAccess:c.allowScriptAccess,bgcolor:c.bgColor,pluginspage:Ta+"www.macromedia.com/go/getflashplayer",title:"JS/Flash audio component (SoundManager 2)",type:"application/x-shockwave-flash",wmode:c.wmode,hasPriority:"true"};if(c.debugFlash)f.FlashVars="debug=1";c.wmode||delete f.wmode;
if(y)b=l.createElement("div"),i=['<object id="'+a+'" data="'+e+'" type="'+f.type+'" title="'+f.title+'" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="'+Ta+'download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0">',d("movie",e),d("AllowScriptAccess",c.allowScriptAccess),d("quality",f.quality),c.wmode?d("wmode",c.wmode):"",d("bgcolor",c.bgColor),d("hasPriority","true"),c.debugFlash?d("FlashVars",f.FlashVars):"","</object>"].join("");else for(h in b=l.createElement("embed"),
f)f.hasOwnProperty(h)&&b.setAttribute(h,f[h]);qa();j=G();if(f=pa())if(c.oMC=S(c.movieID)||l.createElement("div"),c.oMC.id){k=c.oMC.className;c.oMC.className=(k?k+" ":"movieContainer")+(j?" "+j:"");c.oMC.appendChild(b);if(y)h=c.oMC.appendChild(l.createElement("div")),h.className="sm2-object-box",h.innerHTML=i;K=!0}else{c.oMC.id=c.movieID;c.oMC.className="movieContainer "+j;h=j=null;if(!c.useFlashBlock)if(c.useHighPerformance)j={position:"fixed",width:"8px",height:"8px",bottom:"0px",left:"0px",overflow:"hidden"};
else if(j={position:"absolute",width:"6px",height:"6px",top:"-9999px",left:"-9999px"},m)j.left=Math.abs(parseInt(j.left,10))+"px";if(Xa)c.oMC.style.zIndex=1E4;if(!c.debugFlash)for(k in j)j.hasOwnProperty(k)&&(c.oMC.style[k]=j[k]);try{y||c.oMC.appendChild(b);f.appendChild(c.oMC);if(y)h=c.oMC.appendChild(l.createElement("div")),h.className="sm2-object-box",h.innerHTML=i;K=!0}catch(n){throw Error(u("domError")+" \n"+n.toString());}}return J=!0};V=function(){if(c.html5Only)return W(),!1;if(i)return!1;
i=c.getMovie(c.id);if(!i)M?(y?c.oMC.innerHTML=ra:c.oMC.appendChild(M),M=null,J=!0):W(c.id,c.url),i=c.getMovie(c.id);"function"===typeof c.oninitmovie&&setTimeout(c.oninitmovie,1);return!0};D=function(){setTimeout(Ea,1E3)};Ea=function(){var a,e=!1;if(N)return!1;N=!0;o.remove(h,"load",D);if(ca&&!Aa)return!1;k||(a=c.getMoviePercent(),0<a&&100>a&&(e=!0));setTimeout(function(){a=c.getMoviePercent();if(e)return N=!1,h.setTimeout(D,1),!1;!k&&Ra&&(null===a?c.useFlashBlock||0===c.flashLoadTimeout?c.useFlashBlock&&
sa():X(!0):0!==c.flashLoadTimeout&&X(!0))},c.flashLoadTimeout)};U=function(){if(Aa||!ca)return o.remove(h,"focus",U),!0;Aa=Ra=!0;N=!1;D();o.remove(h,"focus",U);return!0};Oa=function(){var a,e=[];if(c.useHTML5Audio&&c.hasHTML5)for(a in c.audioFormats)c.audioFormats.hasOwnProperty(a)&&e.push(a+": "+c.html5[a]+(!c.html5[a]&&t&&c.flash[a]?" (using flash)":c.preferFlash&&c.flash[a]&&t?" (preferring flash)":!c.html5[a]?" ("+(c.audioFormats[a].required?"required, ":"")+"and no flash support)":""))};L=function(a){if(k)return!1;
if(c.html5Only)return k=!0,C(),!0;var e=!0,d;if(!c.useFlashBlock||!c.flashLoadTimeout||c.getMoviePercent())k=!0,s&&(d={type:!t&&n?"NO_FLASH":"INIT_TIMEOUT"});if(s||a){if(c.useFlashBlock&&c.oMC)c.oMC.className=G()+" "+(null===c.getMoviePercent()?"swf_timedout":"swf_error");B({type:"ontimeout",error:d,ignoreInit:!0});F(d);e=!1}s||(c.waitForWindowLoad&&!ja?o.add(h,"load",C):C());return e};Da=function(){var a,e=c.setupOptions;for(a in e)e.hasOwnProperty(a)&&("undefined"===typeof c[a]?c[a]=e[a]:c[a]!==
e[a]&&(c.setupOptions[a]=c[a]))};ia=function(){if(k)return!1;if(c.html5Only){if(!k)o.remove(h,"load",c.beginDelayedInit),c.enabled=!0,L();return!0}V();try{i._externalInterfaceTest(!1),Fa(!0,c.flashPollingInterval||(c.useHighPerformance?10:50)),c.debugMode||i._disableDebug(),c.enabled=!0,c.html5Only||o.add(h,"unload",ha)}catch(a){return F({type:"JS_TO_FLASH_EXCEPTION",fatal:!0}),X(!0),L(),!1}L();o.remove(h,"load",c.beginDelayedInit);return!0};E=function(){if(oa)return!1;oa=!0;Da();qa();!t&&c.hasHTML5&&
c.setup({useHTML5Audio:!0,preferFlash:!1});Ma();c.html5.usingFlash=La();n=c.html5.usingFlash;Oa();!t&&n&&c.setup({flashLoadTimeout:1});l.removeEventListener&&l.removeEventListener("DOMContentLoaded",E,!1);V();return!0};va=function(){"complete"===l.readyState&&(E(),l.detachEvent("onreadystatechange",va));return!0};na=function(){ja=!0;o.remove(h,"load",na)};wa();o.add(h,"focus",U);o.add(h,"load",D);o.add(h,"load",na);l.addEventListener?l.addEventListener("DOMContentLoaded",E,!1):l.attachEvent?l.attachEvent("onreadystatechange",
va):F({type:"NO_DOM2_EVENTS",fatal:!0});"complete"===l.readyState&&setTimeout(E,100)}var da=null;if("undefined"===typeof SM2_DEFER||!SM2_DEFER)da=new Q;ea.SoundManager=Q;ea.soundManager=da})(window);

216
readme.md
View file

@ -1,216 +0,0 @@
# Groove Basin
No-nonsense music client and server for your home or office.
Run it on a server connected to your main speakers. Guests can connect with
their laptops, tablets, and phones, and play and share music.
Depends on [mpd](http://musicpd.org) version 0.17+ for the backend. Some might
call this project an mpd client. (Note, version 0.17 is only available from
source as of writing this; see below instructions regarding mpd installation.)
Live demo: [groovebasin.com](http://groovebasin.com/)
## Features
* Lightning-fast, responsive UI. You can hardly tell that the music server is
on another computer.
* Dynamic playlist mode which automatically queues random songs, favoring
songs that have not been played recently.
* Drag and drop upload. Drag and drop playlist editing. Rich keyboard
shortcuts.
* Streaming support. You can listen to your music library - or share it with
your friends - even when you are not physically near your home speakers.
* Last.fm scrobbling.
## Get Started
Make sure you have [Node](http://nodejs.org) and [npm](http://npmjs.org)
installed and [mpd](http://musicpd.org) version 0.17+ (see below) running,
then:
```
$ npm install groovebasin
$ npm start groovebasin
```
At this point, Groove Basin will issue warnings telling you what to do next.
## Screenshots
![Search + drag/drop support](http://www.superjoesoftware.com/temp/groove-basin-0.0.4.png)
![Multi-select and context menu](http://www.superjoesoftware.com/temp/groove-basin-0.0.4-lib-menu.png)
![Keyboard shortcuts](http://www.superjoesoftware.com/temp/groove-basin-0.0.4-shortcuts.png)
![Last.fm Scrobbling](http://www.superjoesoftware.com/temp/groove-basin-0.0.4-lastfm.png)
## Mpd
Groove Basin depends on [mpd](http://musicpd.org) version 0.17+.
To compile from source, start here
```
$ git clone git://git.musicpd.org/master/mpd.git
```
and follow mpd's instructions from there.
### Configuration
* `default_permissions` - Recommended to remove `admin` so that anonymous
users can't do nefarious things.
* `password` - Recommended to add a password for yourself to give yourself `admin` permissions.
* `read` - allows reading the library, current playlist, and playback status.
* `add` - allows adding songs, loading playlists, and uploading songs.
* `control` - allows controlling playback state and manipulating playlists.
* `admin` - allows updating the db, killing mpd, deleting songs from the
library, and updating song tags.
* `audio_output` - Uncomment the "httpd" one and configure the port to enable
streaming. Recommended "vorbis" encoder for better browser support.
* `sticker_file` - Groove Basin will not run without one set.
* `gapless_mp3_playback` - "yes" recommended. <3 gapless playback.
* `volume_normalization` - "yes" recommended. Replaygain scanners are not
implemented for all the formats that can be played back. Volume normalization
works on all formats.
* `max_command_list_size` - "16384" recommended. You do not want mpd crashing
when you try to remove a ton of songs from the playlist at once.
* `auto_update` - "yes" recommended. Required for uploaded songs to show up
in your library.
## Configuring Groove Basin
See http://npmjs.org/doc/config.html#Per-Package-Config-Settings
See the "config" section of `package.json` for configuration options and
defaults.
Example:
```
$ npm config set groovebasin:mpd_conf ~/.mpd/mpd.conf
$ npm config set groovebasin:port 80
$ npm -g --groovebasin:port 80 start groovebasin
```
## Developing
Install dependencies and run mpd as described in the Get Started section.
Then, from a clean clone of the source repository:
```
$ npm run-script dev --groovebasin:development_mode true
```
## Release Notes
### 0.0.6
* 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
* 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
* 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
* 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
* 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

File diff suppressed because it is too large Load diff

2607
src/client/app.js Normal file

File diff suppressed because it is too large Load diff

542
src/client/playerclient.js Normal file
View file

@ -0,0 +1,542 @@
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var uuid = require('uuid');
var MusicLibraryIndex = require('music-library-index');
var keese = require('keese');
var jsondiffpatch = require('jsondiffpatch');
module.exports = PlayerClient;
var compareSortKeyAndId = makeCompareProps(['sortKey', 'id']);
PlayerClient.REPEAT_OFF = 0;
PlayerClient.REPEAT_ONE = 1;
PlayerClient.REPEAT_ALL = 2;
util.inherits(PlayerClient, EventEmitter);
function PlayerClient(socket) {
EventEmitter.call(this);
window.__debug_PlayerClient = this;
var self = this;
self.socket = socket;
self.serverTimeOffset = 0;
self.serverTrackStartDate = null;
self.playlistFromServer = undefined;
self.playlistFromServerVersion = null;
self.libraryFromServer = undefined;
self.libraryFromServerVersion = null;
self.resetServerState();
self.socket.on('disconnect', function() {
self.resetServerState();
});
if (self.socket.isConnected) {
self.handleConnectionStart();
} else {
self.socket.on('connect', self.handleConnectionStart.bind(self));
}
self.socket.on('time', function(o) {
self.serverTimeOffset = new Date(o) - new Date();
self.updateTrackStartDate();
self.emit('statusupdate');
});
self.socket.on('volume', function(volume) {
self.volume = volume;
self.emit('statusupdate');
});
self.socket.on('repeat', function(repeat) {
self.repeat = repeat;
self.emit('statusupdate');
});
self.socket.on('currentTrack', function(o) {
self.isPlaying = o.isPlaying;
self.serverTrackStartDate = o.trackStartDate && new Date(o.trackStartDate);
self.pausedTime = o.pausedTime;
self.currentItemId = o.currentItemId;
self.updateTrackStartDate();
self.updateCurrentItem();
self.emit('statusupdate');
self.emit('currentTrack');
});
self.socket.on('playlist', function(o) {
if (o.reset) self.playlistFromServer = undefined;
self.playlistFromServer = jsondiffpatch.patch(self.playlistFromServer, o.delta);
deleteUndefineds(self.playlistFromServer);
self.playlistFromServerVersion = o.version;
self.updatePlaylistIndex();
self.emit('statusupdate');
self.emit('playlistupdate');
});
self.socket.on('library', function(o) {
if (o.reset) self.libraryFromServer = undefined;
self.libraryFromServer = jsondiffpatch.patch(self.libraryFromServer, o.delta);
deleteUndefineds(self.libraryFromServer);
self.libraryFromServerVersion = o.version;
self.library.clear();
for (var key in self.libraryFromServer) {
var track = self.libraryFromServer[key];
self.library.addTrack(track);
}
self.library.rebuild();
self.updatePlaylistIndex();
self.haveFileListCache = true;
var lastQuery = self.lastQuery;
self.lastQuery = null;
self.search(lastQuery);
});
function deleteUndefineds(o) {
for (var key in o) {
if (o[key] === undefined) delete o[key];
}
}
}
PlayerClient.prototype.handleConnectionStart = function(){
this.sendCommand('subscribe', { name: 'library', delta: true, });
this.sendCommand('subscribe', {name: 'volume'});
this.sendCommand('subscribe', {name: 'repeat'});
this.sendCommand('subscribe', {name: 'currentTrack'});
this.sendCommand('subscribe', {
name: 'playlist',
delta: true,
version: this.playlistFromServerVersion,
});
};
PlayerClient.prototype.updateTrackStartDate = function() {
this.trackStartDate = (this.serverTrackStartDate != null) ?
new Date(new Date(this.serverTrackStartDate) - this.serverTimeOffset) : null;
};
PlayerClient.prototype.updateCurrentItem = function() {
this.currentItem = (this.currentItemId != null) ?
this.playlist.itemTable[this.currentItemId] : null;
};
PlayerClient.prototype.updatePlaylistIndex = function() {
this.clearPlaylist();
if (!this.playlistFromServer) return;
for (var id in this.playlistFromServer) {
var item = this.playlistFromServer[id];
var track = this.library.trackTable[item.key];
this.playlist.itemTable[id] = {
id: id,
sortKey: item.sortKey,
isRandom: item.isRandom,
track: track,
playlist: this.playlist,
};
}
this.refreshPlaylistList();
this.updateCurrentItem();
};
PlayerClient.prototype.search = function(query) {
query = query.trim();
var words = query.split(/\s+/);
query = words.join(" ");
if (query === this.lastQuery) return;
this.lastQuery = query;
this.searchResults = this.library.search(query);
this.emit('libraryupdate');
this.emit('playlistupdate');
this.emit('statusupdate');
};
PlayerClient.prototype.getDefaultQueuePosition = function() {
var previousKey = this.currentItem && this.currentItem.sortKey;
var nextKey = null;
var startPos = this.currentItem ? this.currentItem.index + 1 : 0;
for (var i = startPos; i < this.playlist.itemList.length; i += 1) {
var track = this.playlist.itemList[i];
var sortKey = track.sortKey;
if (track.isRandom) {
nextKey = sortKey;
break;
}
previousKey = sortKey;
}
return {
previousKey: previousKey,
nextKey: nextKey
};
};
PlayerClient.prototype.queueTracks = function(keys, previousKey, nextKey) {
if (!keys.length) return;
if (previousKey == null && nextKey == null) {
var defaultPos = this.getDefaultQueuePosition();
previousKey = defaultPos.previousKey;
nextKey = defaultPos.nextKey;
}
var items = {};
for (var i = 0; i < keys.length; i += 1) {
var key = keys[i];
var sortKey = keese(previousKey, nextKey);
var id = uuid();
items[id] = {
key: key,
sortKey: sortKey,
};
this.playlist.itemTable[id] = {
id: id,
key: key,
sortKey: sortKey,
isRandom: false,
track: this.library.trackTable[key],
};
previousKey = sortKey;
}
this.refreshPlaylistList();
this.sendCommand('addid', items);
this.emit('playlistupdate');
};
PlayerClient.prototype.queueTracksNext = function(keys) {
var prevKey = this.currentItem && this.currentItem.sortKey;
var nextKey = null;
var itemList = this.playlist.itemList;
for (var i = 0; i < itemList.length; ++i) {
var track = itemList[i];
if (prevKey == null || track.sortKey > prevKey) {
if (nextKey == null || track.sortKey < nextKey) {
nextKey = track.sortKey;
}
}
}
this.queueTracks(keys, prevKey, nextKey);
};
PlayerClient.prototype.clear = function(){
this.sendCommand('clear');
this.clearPlaylist();
this.emit('playlistupdate');
};
PlayerClient.prototype.shuffle = function(){
this.sendCommand('shuffle');
};
PlayerClient.prototype.play = function(){
this.sendCommand('play');
if (this.isPlaying === false) {
this.trackStartDate = elapsedToDate(this.pausedTime);
this.isPlaying = true;
this.emit('statusupdate');
}
};
PlayerClient.prototype.stop = function(){
this.sendCommand('stop');
if (this.isPlaying === true) {
this.pausedTime = 0;
this.isPlaying = false;
this.emit('statusupdate');
}
};
PlayerClient.prototype.pause = function(){
this.sendCommand('pause');
if (this.isPlaying === true) {
this.pausedTime = dateToElapsed(this.trackStartDate);
this.isPlaying = false;
this.emit('statusupdate');
}
};
PlayerClient.prototype.next = function(){
var index = this.currentItem ? this.currentItem.index + 1 : 0;
// handle the case of Repeat All
if (index >= this.playlist.itemList.length &&
this.repeat === PlayerClient.REPEAT_ALL)
{
index = 0;
}
var item = this.playlist.itemList[index];
var id = item && item.id;
this.seek(id, 0);
};
PlayerClient.prototype.prev = function(){
var index = this.currentItem ? this.currentItem.index - 1 : this.playlist.itemList.length - 1;
// handle case of Repeat All
if (index < 0 && this.repeat === PlayerClient.REPEAT_ALL) {
index = this.playlist.itemList.length - 1;
}
var item = this.playlist.itemList[index];
var id = item && item.id;
this.seek(id, 0);
};
PlayerClient.prototype.moveIds = function(trackIds, previousKey, nextKey){
var track, i;
var tracks = [];
for (i = 0; i < trackIds.length; i += 1) {
var id = trackIds[i];
track = this.playlist.itemTable[id];
if (track) tracks.push(track);
}
tracks.sort(compareSortKeyAndId);
var items = {};
for (i = 0; i < tracks.length; i += 1) {
track = tracks[i];
var sortKey = keese(previousKey, nextKey);
items[track.id] = {
sortKey: sortKey,
};
track.sortKey = sortKey;
previousKey = sortKey;
}
this.refreshPlaylistList();
this.sendCommand('move', items);
this.emit('playlistupdate');
};
PlayerClient.prototype.shiftIds = function(trackIdSet, offset) {
// an example of shifting 5 items (a,c,f,g,i) "down":
// offset: +1, reverse: false, this -> way
// selection: * * * * *
// before: a, b, c, d, e, f, g, h, i
// \ \ \ \ |
// \ \ \ \ |
// after: b, a, d, c, e, h, f, g, i
// selection: * * * * *
// (note that "i" does not move because it has no futher to go.)
//
// an alternate way to think about it: some items "leapfrog" backwards over the selected items.
// this ends up being much simpler to compute, and even more compact to communicate.
// selection: * * * * *
// before: a, b, c, d, e, f, g, h, i
// / / ___/
// / / /
// after: b, a, d, c, e, h, f, g, i
// selection: * * * * *
// (note that the moved items are not the selected items)
var itemList = this.playlist.itemList;
var movedItems = {};
var reverse = offset === -1;
function getKeeseBetween(itemA, itemB) {
if (reverse) {
var tmp = itemA;
itemA = itemB;
itemB = tmp;
}
var keyA = itemA == null ? null : itemA.sortKey;
var keyB = itemB == null ? null : itemB.sortKey;
return keese(keyA, keyB);
}
if (reverse) {
// to make this easier, just reverse the item list in place so we can write one iteration routine.
// note that we are editing our data model live! so don't forget to refresh it later.
itemList.reverse();
}
for (var i = itemList.length - 1; i >= 1; i--) {
var track = itemList[i];
if (!(track.id in trackIdSet) && (itemList[i - 1].id in trackIdSet)) {
// this one needs to move backwards (e.g. found "h" is not selected, and "g" is selected)
i--; // e.g. g
i--; // e.g. f
while (true) {
if (i < 0) {
// fell off the end (or beginning) of the list
track.sortKey = getKeeseBetween(null, itemList[0]);
break;
}
if (!(itemList[i].id in trackIdSet)) {
// this is where it goes (e.g. found "d" is not selected)
track.sortKey = getKeeseBetween(itemList[i], itemList[i + 1]);
break;
}
i--;
}
movedItems[track.id] = {sortKey: track.sortKey};
i++;
}
}
// we may have reversed the table and adjusted all the sort keys, so we need to refresh this.
this.refreshPlaylistList();
this.sendCommand('move', movedItems);
this.emit('playlistupdate');
};
PlayerClient.prototype.removeIds = function(trackIds){
if (trackIds.length === 0) return;
var ids = [];
for (var i = 0; i < trackIds.length; i += 1) {
var trackId = trackIds[i];
var currentId = this.currentItem && this.currentItem.id;
if (currentId === trackId) {
this.currentItemId = null;
this.currentItem = null;
}
ids.push(trackId);
var item = this.playlist.itemTable[trackId];
delete this.playlist.itemTable[item.id];
this.refreshPlaylistList();
}
this.sendCommand('deleteid', ids);
this.emit('playlistupdate');
};
PlayerClient.prototype.seek = function(id, pos) {
pos = parseFloat(pos || 0, 10);
var item = id ? this.playlist.itemTable[id] : this.currentItem;
if (pos < 0) pos = 0;
if (pos > item.duration) pos = item.duration;
this.sendCommand('seek', {
id: item.id,
pos: pos,
});
this.currentItem = item;
this.currentItemId = item.id;
this.isPlaying = true;
this.duration = item.track.duration;
this.trackStartDate = elapsedToDate(pos);
this.emit('statusupdate');
};
PlayerClient.prototype.setVolume = function(vol){
if (vol > 1.0) vol = 1.0;
if (vol < 0.0) vol = 0.0;
this.volume = vol;
this.sendCommand('setvol', this.volume);
this.emit('statusupdate');
};
PlayerClient.prototype.setRepeatMode = function(mode) {
this.repeat = mode;
this.sendCommand('repeat', mode);
this.emit('statusupdate');
};
PlayerClient.prototype.sendCommand = function(name, args) {
this.socket.send(name, args);
};
PlayerClient.prototype.clearPlaylist = function(){
this.playlist = {
itemList: [],
itemTable: {},
index: null,
name: null
};
};
PlayerClient.prototype.anticipatePlayId = function(trackId){
var item = this.playlist.itemTable[trackId];
this.currentItem = item;
this.currentItemId = item.id;
this.isPlaying = true;
this.duration = item.track.duration;
this.trackStartDate = new Date();
this.emit('statusupdate');
};
PlayerClient.prototype.anticipateSkip = function(direction) {
if (this.currentItem) {
var nextItem = this.playlist.itemList[this.currentItem.index + direction];
if (nextItem) this.anticipatePlayId(nextItem.id);
}
};
PlayerClient.prototype.refreshPlaylistList = function(){
this.playlist.itemList = [];
var item;
for (var id in this.playlist.itemTable) {
item = this.playlist.itemTable[id];
item.playlist = this.playlist;
this.playlist.itemList.push(item);
}
this.playlist.itemList.sort(compareSortKeyAndId);
for (var i = 0; i < this.playlist.itemList.length; i += 1) {
item = this.playlist.itemList[i];
item.index = i;
}
};
// sort keys according to how they appear in the library
PlayerClient.prototype.sortKeys = function(keys) {
var realLib = this.library;
var lib = new MusicLibraryIndex();
keys.forEach(function(key) {
var track = realLib.trackTable[key];
if (track) lib.addTrack(track);
});
lib.rebuild();
var results = [];
lib.artistList.forEach(function(artist) {
artist.albumList.forEach(function(album) {
album.trackList.forEach(function(track) {
results.push(track.key);
});
});
});
return results;
};
PlayerClient.prototype.resetServerState = function(){
this.haveFileListCache = false;
this.library = new MusicLibraryIndex({
searchFields: MusicLibraryIndex.defaultSearchFields.concat('file'),
});
this.searchResults = this.library;
this.lastQuery = "";
this.clearPlaylist();
this.repeat = 0;
this.currentItem = null;
this.currentItemId = null;
this.stored_playlist_table = {};
this.stored_playlist_item_table = {};
this.stored_playlists = [];
};
function elapsedToDate(elapsed){
return new Date(new Date() - elapsed * 1000);
}
function dateToElapsed(date){
return (new Date() - date) / 1000;
}
function noop(err){
if (err) throw err;
}
function operatorCompare(a, b){
if (a === b) {
return 0;
}
if (a < b) {
return -1;
} else {
return 1;
}
}
function makeCompareProps(props){
return function(a, b) {
for (var i = 0; i < props.length; i += 1) {
var prop = props[i];
var result = operatorCompare(a[prop], b[prop]);
if (result) return result;
}
return 0;
};
}

55
src/client/socket.js Normal file
View file

@ -0,0 +1,55 @@
var EventEmitter = require('events').EventEmitter;
var util = require('util');
module.exports = Socket;
util.inherits(Socket, EventEmitter);
function Socket() {
var self = this;
EventEmitter.call(self);
self.isConnected = false;
createWs();
function createWs() {
var host = window.document.location.host;
var pathname = window.document.location.pathname;
var match = host.match(/^(.+):(\d+)$/);
var port = match ? parseInt(match[2], 10) : 80;
var hostName = match ? match[1] : host;
var wsUrl = 'ws://' + hostName + ':' + port + pathname;
self.ws = new WebSocket(wsUrl);
self.ws.addEventListener('message', onMessage, false);
self.ws.addEventListener('error', timeoutThenCreateNew, false);
self.ws.addEventListener('close', timeoutThenCreateNew, false);
self.ws.addEventListener('open', onOpen, false);
function onOpen() {
self.isConnected = true;
self.emit('connect');
}
function onMessage(event) {
var msg = JSON.parse(event.data);
self.emit(msg.name, msg.args);
}
function timeoutThenCreateNew() {
self.ws.removeEventListener('error', timeoutThenCreateNew, false);
self.ws.removeEventListener('close', timeoutThenCreateNew, false);
self.ws.removeEventListener('open', onOpen, false);
if (self.isConnected) {
self.isConnected = false;
self.emit('disconnect');
}
setTimeout(createWs, 1000);
}
}
}
Socket.prototype.send = function(name, args) {
this.ws.send(JSON.stringify({
name: name,
args: args,
}));
}

View file

@ -1,10 +0,0 @@
window.SocketMpd = class SocketMpd extends window.Mpd
constructor: (@socket) ->
super()
@socket.on 'FromMpd', @receive
@socket.on 'MpdConnect', @handleConnectionStart
@socket.on 'MpdDisconnect', @resetServerState
@socket.on 'disconnect', @resetServerState
rawSend: (msg) =>
@socket.emit 'ToMpd', msg

101
src/client/streaming.js Normal file
View file

@ -0,0 +1,101 @@
exports.getUrl = getUrl;
exports.toggleStatus = toggleStatus;
exports.init = init;
var tryingToStream = false;
var actuallyStreaming = false;
var stillBuffering = false;
var player = null;
var audio = new Audio();
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) {
if (actuallyStreaming) {
if (stillBuffering) {
return "Stream: Buffering";
} else {
return "Stream: On";
}
} else {
return "Stream: Paused";
}
} else {
return "Stream: Off";
}
}
function renderStreamButton(){
var label = getButtonLabel();
$streamBtn
.button("option", "label", label)
.prop("checked", tryingToStream)
.button("refresh");
}
function toggleStatus() {
tryingToStream = !tryingToStream;
renderStreamButton();
updatePlayer();
return false;
}
function getUrl(){
return "/stream.mp3";
}
function onPlaying() {
stillBuffering = false;
renderStreamButton();
}
function clearBuffer() {
if (tryingToStream) {
tryingToStream = !tryingToStream;
updatePlayer();
tryingToStream = !tryingToStream;
updatePlayer();
}
}
function updatePlayer() {
var shouldStream = tryingToStream && player.isPlaying === true;
if (actuallyStreaming === shouldStream) return;
if (shouldStream) {
audio.src = getUrl();
audio.load();
audio.play();
stillBuffering = true;
} else {
audio.pause();
stillBuffering = false;
}
actuallyStreaming = shouldStream;
renderStreamButton();
}
function setUpUi() {
$streamBtn.button({
icons: {
primary: "ui-icon-signal-diag"
}
});
$streamBtn.on('click', toggleStatus);
}
function init(playerInstance, socket) {
player = playerInstance;
player.on('currentTrack', updatePlayer);
socket.on('seek', clearBuffer);
setUpUi();
}

View file

@ -1,3 +1,6 @@
@import "vendor/reset.min.css"
@import "vendor/jquery-ui-1.10.4.custom.min.css"
user-select()
-moz-user-select arguments
-khtml-user-select arguments
@ -10,17 +13,15 @@ border-radius()
border-radius arguments
selected-div()
border 1px solid #222
background #00498F url(vendor/css/dot-luv/images/ui-bg_dots-small_40_00498f_2x2.png) 50% 50% repeat
font-weight bold
background #00498F url(images/ui-bg_dots-small_40_00498f_2x2.png) 50% 50% repeat
color #fff
html
background url(vendor/css/dot-luv/images/ui-bg_diagonals-thick_15_0b3e6f_40x40.png)
background url(images/ui-bg_diagonals-thick_15_0b3e6f_40x40.png)
html.groovebasin
background url(groovebasin.jpg)
background url(img/groovebasin.jpg)
html.nggyu
background url(nggyu.jpg)
background url(img/nggyu.jpg)
body
font-family Arial, Helvetica, sans-serif
@ -28,6 +29,9 @@ body
#track-slider
margin 10px 10px 5px
background url("img/bright-10.png") left no-repeat
background-size 0% 100%
#nowplaying
margin 6px auto
text-align center
@ -41,7 +45,7 @@ body
list-style none outside none
margin 2px
padding 4px
h1
letter-spacing 0.1em
margin-top 8px
@ -83,11 +87,11 @@ body
width 400px
position absolute
#library-tab
#library-pane
.window-header
height 30px
#lib-tabs
#tabs
user-select none
ul
@ -97,13 +101,13 @@ body
display inline
span
font-size .6em
font-size .625em
padding 2px 4px
font-weight normal
cursor pointer
#library
#library, #stored-playlists
overflow-y auto
user-select none
@ -134,16 +138,11 @@ body
div.cursor
text-decoration underline
#lib-filter
margin 4px
width 175px
span.chat-user
color #8888ff
span.chat-user-self
color #888888
#user-id
cursor pointer
@ -215,7 +214,7 @@ span.chat-user-self
div.selected
selected-div()
div.current
border 1px solid #096AC8
background-color #292929
@ -223,7 +222,8 @@ span.chat-user-self
color #75abff
div.cursor
text-decoration underline
span
text-decoration underline
div.border-bottom
border-bottom 2px dashed #ffffff
@ -243,14 +243,22 @@ span.chat-user-self
#upload-widget
padding 10px
#menu
position absolute
padding 2px
li
a
display block
text-decoration none
padding 6px
#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
@ -279,7 +287,7 @@ span.chat-user-self
dd
display inline
#mpd-error
#main-err-msg
margin 200px auto
width 300px
padding 4px
@ -307,3 +315,6 @@ span.chat-user-self
font-size .9em
li:before
content "\2713"
.accesskey
text-decoration: underline

File diff suppressed because one or more lines are too long

View file

@ -1,56 +0,0 @@
_exports = exports ? window.Util = {}
_exports.schedule = (delay, func) -> window.setInterval(func, delay)
_exports.wait = (delay, func) -> setTimeout func, delay
_exports.shuffle = (array) ->
top = array.length
while --top > 0
current = Math.floor(Math.random() * (top + 1))
tmp = array[current]
array[current] = array[top]
array[top] = tmp
_exports.formatTime = (seconds) ->
seconds = Math.floor seconds
minutes = Math.floor seconds / 60
seconds -= minutes * 60
hours = Math.floor minutes / 60
minutes -= hours * 60
zfill = (n) ->
if n < 10 then "0" + n else "" + n
if hours != 0
return "#{hours}:#{zfill minutes}:#{zfill seconds}"
else
return "#{minutes}:#{zfill seconds}"
# converts any string into an HTML id, guaranteed to be unique
ok_id_chars = {}
ok_id_chars[c] = true for c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"
_exports.toHtmlId = (string) ->
out = ""
for c in string
if ok_id_chars[c]
out += c
else
out += "_" + c.charCodeAt(0)
return out
# compares 2 arrays with positive integers, returning > 0, 0, or < 0
_exports.compareArrays = (arr1, arr2) ->
for val1, i1 in arr1
val2 = arr2[i1]
diff = (val1 ? -1) - (val2 ? -1)
return diff if diff isnt 0
return 0
_exports.parseQuery = (query) ->
obj = {}
return obj unless query?
for [param, val] in (valset.split('=') for valset in query.split('&'))
obj[unescape(param)] = unescape(val)
return obj

View file

@ -1,17 +0,0 @@
{{#albums}}
<li>
<div class="album expandable" id="{{albumid key}}" data-key="{{key}}">
<div class="ui-icon ui-icon-triangle-1-e"></div>
<span>{{#if name}}{{name}}{{else}}[Unknown Album]{{/if}}</span>
</div>
<ul style="display: none;">
{{#tracks}}
<li>
<div class="track" id="{{trackid file}}" data-file="{{file}}">
<span>{{#if track}}{{track}}. {{/if}}{{#if artist_disambiguation}}{{artist_disambiguation}} - {{/if}}{{name}}</span>
</div>
</li>
{{/tracks}}
</ul>
</li>
{{/albums}}

View file

@ -1,7 +0,0 @@
<ul>
{{#each chats}}
<li>
<span class="{{class}}">{{user_name}}</span>: {{message}}
</li>
{{/each}}
</ul>

View file

@ -1,5 +0,0 @@
{{#each users}}
<li>
<span class="{{class}}">{{user_name}}</span>
</li>
{{/each}}

View file

@ -1,18 +0,0 @@
{{#if artists}}
<ul>
{{#artists}}
<li>
<div class="artist expandable" id="{{artistid name}}">
<div class="ui-icon ui-icon-triangle-1-e"></div>
<span>{{#if name}}{{name}}{{else}}[Unknown Artist]{{/if}}</span>
</div>
<ul></ul>
</li>
{{/artists}}
</ul>
{{else}}
<p class="ui-state-highlight ui-corner-all">
<span class="ui-icon ui-icon-info"></span>
<strong>{{empty_library_message}}</strong>
</p>
{{/if}}

View file

@ -1,27 +0,0 @@
<ul id="menu" class="ui-widget-content ui-corner-all">
<li><a href="#" class="queue hoverable">Queue</a></li>
<li><a href="#" class="queue-next hoverable">Queue Next</a></li>
<li><a href="#" class="queue-random hoverable">Queue in Random Order</a></li>
<li><a href="#" class="queue-next-random hoverable">Queue Next in Random Order</a></li>
<li>
{{#if status.delete_enabled}}
{{#if permissions.admin}}
<a href="#" class="delete hoverable">Delete</a>
{{else}}
<span title="Delete is disabled: insufficient privileges. See Settings.">Delete</span>
{{/if}}
{{else}}
<span title="Delete is disabled due to invalid server configuration.">Delete</span>
{{/if}}
</li>
{{#if track}}
<li>
{{#if status.download_enabled}}
<a href="library/{{track.file}}" class="download hoverable" target="_blank">Download</a>
{{else}}
<span title="Download is disabled due to invalid server configuration.">Download</span>
{{/if}}
</li>
{{/if}}
</ul>

View file

@ -1,12 +0,0 @@
{{#if playlist}}
{{#playlist}}
<div class="pl-item" id="playlist-track-{{id}}" data-id="{{id}}">
<span class="track">{{track.track}}</span>
<span class="title">{{track.name}}</span>
<span class="artist">{{track.artist_name}}</span>
<span class="album">{{track.album_name}}</span>
<span class="time">{{time track.time}}</span>
</div>
{{/playlist}}
{{/if}}

View file

@ -1,21 +0,0 @@
<ul id="menu" class="ui-widget-content ui-corner-all">
<li><a href="#" class="remove hoverable">Remove</a></li>
<li>
{{#if status.delete_enabled}}
{{#if permissions.admin}}
<a href="#" class="delete hoverable">Delete From Library</a>
{{else}}
<span title="Delete is disabled: insufficient privileges. See Settings.">Delete From Library</span>
{{/if}}
{{else}}
<span title="Delete is disabled due to invalid server configuration.">Delete From Library</span>
{{/if}}
</li>
<li>
{{#if status.download_enabled}}
<a href="library/{{item.track.file}}" class="download hoverable" target="_blank">Download</a>
{{else}}
<span title="Download is disabled due to invalid server configuration.">Download</span>
{{/if}}
</li>
</ul>

View file

@ -1,48 +0,0 @@
<div class="section">
<h1>Authentication</h1>
<p>
{{#if auth.show_edit}}
<input type="text" id="auth-password" placeholder="password" />
<button class="auth-save">Save</button>
{{#if auth.password }}
<button class="auth-cancel">Cancel</button>
{{/if}}
{{else}}
Using password <em>{{auth.password}}</em>
<button class="auth-edit">Edit</button>
<button class="auth-clear">Clear</button>
{{/if}}
</p>
<h2>Permissions</h2>
<ul>
{{#if auth.permissions.read}}
<li>Reading the library, current playlist, and playback status</li>
{{/if}}
{{#if auth.permissions.add}}
<li>Adding songs, loading playlists, and uploading songs.</li>
{{/if}}
{{#if auth.permissions.control}}
<li>Control playback state, and manipulate playlists.</li>
{{/if}}
{{#if auth.permissions.admin}}
<li>Deleting songs, updating tags, organizing library.</li>
{{/if}}
</ul>
</div>
<div class="section">
<h1>Last.fm</h1>
{{#if lastfm.username}}
<p>
Authenticated as
<a href="http://last.fm/user/{{lastfm.username}}">{{lastfm.username}}</a>.
<button class="signout">Sign out</button>
</p>
<p>
Scrobbling is <input type="checkbox" id="toggle-scrobble"{{#if lastfm.scrobbling_on}} checked="checked"{{/if}}><label for="toggle-scrobble">{{#if lastfm.scrobbling_on}}On{{else}}Off{{/if}}</label>
</p>
{{else}}
<p>
<a href="{{lastfm.auth_url}}">Authenticate with Last.fm</a>
</p>
{{/if}}
</div>

View file

@ -1,145 +0,0 @@
<div id="shortcuts" style="display: none">
<h1>Playback</h1>
<dl>
<dt>Space</dt>
<dd>Toggle playback</dd>
</dl>
<dl>
<dt>Left <em>and</em> Right</dt>
<dd>Skip 10 seconds in the song</dd>
</dl>
<dl>
<dt>Shift</dt>
<dd>Hold to skip by 10% instead of 10 seconds</dd>
</dl>
<dl>
<dt>&lt; <em>or</em> Ctrl + Left <em>and</em> &gt; <em>or</em> Ctrl + Right</dt>
<dd>Skip track</dd>
</dl>
<dl>
<dt>- <em>and</em> +</dt>
<dd>Change volume</dd>
</dl>
<dl>
<dt>s</dt>
<dd>Toggle streaming</dd>
</dl>
<h1>Playlist</h1>
<dl>
<dt>Up <em>and</em> Down</dt>
<dd>Select the next song</dd>
</dl>
<dl>
<dt>Ctrl + Up <em>and</em> Ctrl + Down</dt>
<dd>Move selection up or down one</dd>
</dl>
<dl>
<dt>Enter</dt>
<dd>Play the selected song</dd>
</dl>
<dl>
<dt>C</dt>
<dd>Clear playlist</dd>
</dl>
<dl>
<dt>H</dt>
<dd>Shuffle playlist</dd>
</dl>
<dl>
<dt>d</dt>
<dd>Toggle dynamic playlist mode</dd>
</dl>
<dl>
<dt>r</dt>
<dd>Change repeat state</dd>
</dl>
<dl>
<dt>Del</dt>
<dd>Remove selected songs from playlist</dd>
</dl>
<dl>
<dt>Shift + Del</dt>
<dd>Delete selected songs from library</dd>
</dl>
<h1>Navigation</h1>
<dl>
<dt>l</dt>
<dd>Switch to Library tab</dd>
</dl>
<dl>
<dt>u</dt>
<dd>Switch to Upload tab</dd>
</dl>
<dl>
<dt>t</dt>
<dd>Focus chat box</dd>
</dl>
<h1>Library Search Box</h1>
<dl>
<dt>/</dt>
<dd>Focus library search</dd>
</dl>
<dl>
<dt>Esc</dt>
<dd>Clear filter. If filter is already clear, remove focus.</dd>
</dl>
<dl>
<dt>Enter</dt>
<dd>Queue all search results</dd>
</dl>
<dl>
<dt>Down</dt>
<dd>Select the first search result</dd>
</dl>
<h1>Library</h1>
<dl>
<dt>Up <em>and</em> Down</dt>
<dd>Select the next item up or down</dd>
</dl>
<dl>
<dt>Left <em>and</em> Right</dt>
<dd>Expand or collapse selected item</dd>
</dl>
<dl>
<dt>Enter</dt>
<dd>Queue selected items<dd>
</dl>
<dl>
<dt>Del</dt>
<dd>Delete selected songs from library</dd>
</dl>
<h1>Miscellaneous</h1>
<dl>
<dt>?</dt>
<dd>Displays keyboard shortcuts</dd>
</dl>
<dl>
<dt>Esc</dt>
<dd>Close menu, cancel drag, clear selection</dd>
</dl>
<dl>
<dt>Alt</dt>
<dd>Hold when right clicking to get the normal browser menu</dd>
</dl>
<dl>
<dt>Shift</dt>
<dd>Hold while queuing to queue next<dd>
</dl>
<dl>
<dt>Alt</dt>
<dd>Hold while queuing to queue in random order<dd>
</dl>
<dl>
<dt>Ctrl</dt>
<dd>Hold to select multiple items<dd>
</dl>
<dl>
<dt>Shift</dt>
<dd>Hold while selecting to select all items in between<dd>
</dl>
</div>

View file

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

View file

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 5 KiB

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

365
src/public/index.html Normal file
View file

@ -0,0 +1,365 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Groove Basin</title>
<link rel="stylesheet" href="app.css">
<link rel="shortcut icon" href="/favicon.png">
</head>
<body>
<div id="nowplaying" class="ui-widget-content ui-corner-all" style="display: none">
<ul class="playback-btns ui-widget ui-helper-clearfix">
<li class="ui-state-default ui-corner-all hoverable prev">
<span class="ui-icon ui-icon-seek-prev"></span>
</li>
<li class="ui-state-default ui-corner-all hoverable toggle">
<span class="ui-icon ui-icon-pause"></span>
</li>
<li class="ui-state-default ui-corner-all hoverable stop">
<span class="ui-icon ui-icon-stop"></span>
</li>
<li class="ui-state-default ui-corner-all hoverable next">
<span class="ui-icon ui-icon-seek-next"></span>
</li>
</ul>
<div id="vol">
<span class="ui-icon ui-icon-volume-off"></span>
<div id="vol-slider"></div>
<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>
</div>
<h1 id="track-display"></h1>
<div id="track-slider"></div>
<span class="time elapsed"></span>
<span class="time left"></span>
<div style="clear: both;"></div>
</div>
<div id="left-window" style="display: none">
<div id="tabs" class="ui-widget ui-corner-all">
<ul class="ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-corner-all">
<li class="ui-state-default ui-corner-top ui-state-active library-tab"><span>Library</span></li>
<li class="ui-state-default ui-corner-top upload-tab"><span>Upload</span></li>
<li class="ui-state-default ui-corner-top settings-tab"><span>Settings</span></li>
</ul>
</div>
<div id="library-pane" class="ui-widget-content ui-corner-all">
<div class="window-header">
<input type="text" id="lib-filter" placeholder="filter">
<select id="organize">
<option selected="selected">Artist / Album / Song</option>
</select>
</div>
<div id="library">
<ul id="library-artists">
</ul>
<p id="library-no-items" class="ui-state-highlight ui-corner-all">
<span class="ui-icon ui-icon-info"></span>
<strong id="empty-library-message">loading...</strong>
</p>
</div>
</div>
<div id="upload-pane" class="ui-widget-content ui-corner-all" style="display: none">
<div id="upload">
<input id="upload-by-url" type="text" placeholder="Paste URL here">
<div id="upload-widget">
<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>
</div>
</div>
</div>
<div id="settings-pane" class="ui-widget-content ui-corner-all" style="display: none">
<div id="settings">
<div class="section">
<h1>Authentication</h1>
<div id="settings-edit-password">
<input type="text" id="auth-password" placeholder="password" />
<button id="settings-auth-save">Save</button>
<button id="settings-auth-cancel">Cancel</button>
</div>
<div id="settings-show-password">
Using password <em id="password-display">...</em>
<button id="settings-auth-edit">Edit</button>
<button id="settings-auth-clear">Clear</button>
</div>
<h2>Permissions</h2>
<ul>
<li id="auth-perm-read">Reading the library, current playlist, and playback status</li>
<li id="auth-perm-add">Adding songs, loading playlists, and uploading songs.</li>
<li id="auth-perm-control">Control playback state, and manipulate playlists.</li>
<li id="auth-perm-admin">Deleting songs, updating tags, organizing library.</li>
</ul>
</div>
<div class="section">
<h1>Last.fm</h1>
<div id="settings-lastfm-in">
<p>
Authenticated as
<a id="settings-lastfm-user" href="#">...</a>.
<button id="lastfm-sign-out">Sign out</button>
</p>
<p>
Scrobbling is
<input class="jquery-button" type="checkbox" id="toggle-scrobble"><label for="toggle-scrobble">Off</label>
</p>
</div>
<div id="settings-lastfm-out">
<p>
<a id="lastfm-auth-url" href="#">Authenticate with Last.fm</a>
</p>
</div>
</div>
<div class="section">
<h1>About</h1>
<ul>
<li><a id="settings-stream-url" href="#">Stream URL</a></li>
<li><a href="http://github.com/andrewrk/groovebasin">GrooveBasin on GitHub</a></li>
</ul>
</div>
</div>
</div>
</div>
<div id="playlist-window" class="ui-widget-content ui-corner-all" style="display: none">
<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>
</div>
<div id="playlist">
<div class="header">
<span class="track">&nbsp;</span>
<span class="title">Title</span>
<span class="artist">Artist</span>
<span class="album">Album</span>
<span class="time">Time</span>
</div>
<div id="playlist-items">
</div>
</div>
</div>
<div style="clear: both"></div>
<div id="main-err-msg" class="ui-state-error ui-corner-all">
<p>
<span class="ui-icon ui-icon-alert"></span>
<div id="main-err-msg-text">Loading...</div>
</p>
</div>
<div id="shortcuts" style="display: none" tabindex="-1">
<h1>Playback</h1>
<dl>
<dt>Space</dt>
<dd>Toggle playback</dd>
</dl>
<dl>
<dt>Left <em>and</em> Right</dt>
<dd>Skip 10 seconds in the song</dd>
</dl>
<dl>
<dt>Shift</dt>
<dd>Hold to skip by 10% instead of 10 seconds</dd>
</dl>
<dl>
<dt>&lt; <em>or</em> Ctrl + Left <em>and</em> &gt; <em>or</em> Ctrl + Right</dt>
<dd>Skip track</dd>
</dl>
<dl>
<dt>- <em>and</em> +</dt>
<dd>Change volume</dd>
</dl>
<dl>
<dt>s</dt>
<dd>Toggle streaming</dd>
</dl>
<h1>Playlist</h1>
<dl>
<dt>Up <em>and</em> Down</dt>
<dd>Select the next song</dd>
</dl>
<dl>
<dt>Alt + Up <em>and</em> Alt + Down</dt>
<dd>Move selected tracks up or down one</dd>
</dl>
<dl>
<dt>Enter</dt>
<dd>Play the selected song</dd>
</dl>
<dl>
<dt>C</dt>
<dd>Clear playlist</dd>
</dl>
<dl>
<dt>H</dt>
<dd>Shuffle playlist</dd>
</dl>
<dl>
<dt>d</dt>
<dd>Toggle dynamic playlist mode</dd>
</dl>
<dl>
<dt>r</dt>
<dd>Change repeat state</dd>
</dl>
<dl>
<dt>Del</dt>
<dd>Remove selected songs from playlist</dd>
</dl>
<dl>
<dt>Shift + Del</dt>
<dd>Delete selected songs from library</dd>
</dl>
<h1>Navigation</h1>
<dl>
<dt>l</dt>
<dd>Switch to Library tab</dd>
</dl>
<dl>
<dt>u</dt>
<dd>Switch to Upload tab and focus the upload by URL box</dd>
</dl>
<h1>Library Search Box</h1>
<dl>
<dt>/</dt>
<dd>Focus library search</dd>
</dl>
<dl>
<dt>Esc</dt>
<dd>Clear filter. If filter is already clear, remove focus.</dd>
</dl>
<dl>
<dt>Enter</dt>
<dd>Queue all search results</dd>
</dl>
<dl>
<dt>Down</dt>
<dd>Select the first search result</dd>
</dl>
<h1>Library</h1>
<dl>
<dt>Up <em>and</em> Down</dt>
<dd>Select the next item up or down</dd>
</dl>
<dl>
<dt>Left <em>and</em> Right</dt>
<dd>Expand or collapse selected item</dd>
</dl>
<dl>
<dt>Enter</dt>
<dd>Queue selected items<dd>
</dl>
<dl>
<dt>Del</dt>
<dd>Delete selected songs from library</dd>
</dl>
<h1>Miscellaneous</h1>
<dl>
<dt>?</dt>
<dd>Displays keyboard shortcuts</dd>
</dl>
<dl>
<dt>Esc</dt>
<dd>Close menu, cancel drag, clear selection</dd>
</dl>
<dl>
<dt>Alt</dt>
<dd>Hold when right clicking to get the normal browser menu</dd>
</dl>
<dl>
<dt>Shift</dt>
<dd>Hold while queuing to queue next<dd>
</dl>
<dl>
<dt>Alt</dt>
<dd>Hold while queuing to queue in random order<dd>
</dl>
<dl>
<dt>Ctrl</dt>
<dd>Hold to select multiple items<dd>
</dl>
<dl>
<dt>Shift</dt>
<dd>Hold while selecting to select all items in between<dd>
</dl>
</div>
<div id="edit-tags" style="display: none">
<input type="checkbox" id="edit-tag-multi-name">
<label accesskey="i">T<span class="accesskey">i</span>tle: <input id="edit-tag-name"></label><br>
<input type="checkbox" id="edit-tag-multi-track">
<label accesskey="k">Trac<span class="accesskey">k</span> Number: <input id="edit-tag-track"></label><br>
<input type="checkbox" id="edit-tag-multi-file">
<label>Filename: <input id="edit-tag-file"></label><br>
<hr>
<input type="checkbox" id="edit-tag-multi-artistName">
<label accesskey="a"><span class="accesskey">A</span>rtist: <input id="edit-tag-artistName"></label><br>
<input type="checkbox" id="edit-tag-multi-composerName">
<label accesskey="c"><span class="accesskey">C</span>omposer: <input id="edit-tag-composerName"></label><br>
<input type="checkbox" id="edit-tag-multi-performerName">
<label>Performer: <input id="edit-tag-performerName"></label><br>
<input type="checkbox" id="edit-tag-multi-genre">
<label accesskey="g"><span class="accesskey">G</span>enre: <input id="edit-tag-genre"></label><br>
<hr>
<input type="checkbox" id="edit-tag-multi-albumName">
<label accesskey="b">Al<span class="accesskey">b</span>um: <input id="edit-tag-albumName"></label><br>
<input type="checkbox" id="edit-tag-multi-albumArtistName">
<label>Album Artist: <input id="edit-tag-albumArtistName"></label><br>
<input type="checkbox" id="edit-tag-multi-trackCount">
<label>Track Count: <input id="edit-tag-trackCount"></label><br>
<input type="checkbox" id="edit-tag-multi-year">
<label accesskey="y"><span class="accesskey">Y</span>ear: <input id="edit-tag-year"></label><br>
<input type="checkbox" id="edit-tag-multi-disc">
<label accesskey="d"><span class="accesskey">D</span>isc Number: <input id="edit-tag-disc"></label><br>
<input type="checkbox" id="edit-tag-multi-discCount">
<label>Disc Count: <input id="edit-tag-discCount"></label><br>
<input type="checkbox" id="edit-tag-multi-compilation">
<label accesskey="m">Co<span class="accesskey">m</span>pilation: <input type="checkbox" id="edit-tag-compilation"></label><br>
<hr>
<div style="float: right">
<button id="edit-tags-ok" accesskey="v">Sa<span class="accesskey">v</span>e &amp; Close</button>
<button id="edit-tags-cancel">Cancel</button>
</div>
<button id="edit-tags-prev" type="button" accesskey="p"><span class="accesskey">P</span>revious</button>
<button id="edit-tags-next" type="button" accesskey="n"><span class="accesskey">N</span>ext</button>
<label accesskey="r" id="edit-tags-per-label"><input id="edit-tags-per" type="checkbox">Pe<span class="accesskey">r</span> Track</label>
</div>
<ul id="menu-playlist" style="display: none">
<li><a href="#" class="remove">Remove</a></li>
<li><a href="#" class="delete">Delete From Library</a></li>
<li><a href="#" class="download" target="_blank">Download</a></li>
<li><a href="#" class="edit-tags">Edit Tags</a></li>
</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>
</ul>
<script src="vendor/jquery-2.1.0.min.js"></script>
<script src="vendor/jquery-ui-1.10.4.custom.min.js"></script>
<script src="app.js"></script>
</body>
</html>

4
src/public/vendor/jquery-2.1.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,42 +0,0 @@
# parses an mpd conf file, returning an object with all the values
exports.parse = (file_contents) ->
obj = {}
stack = []
audio_outputs = []
for line in file_contents.split("\n")
line = line.trim()
continue if line.length == 0
continue if line.substring(0, 1) == "#"
if line.substring(0, 1) == "}"
obj = stack.pop()
else
parts = line.match(/([^\s]*)\s+([^#]*)/)
key = parts[1]
val = parts[2]
if val == "{"
stack.push obj
if key == 'audio_output'
audio_outputs.push new_obj = {}
else
obj[key] = new_obj = {}
obj = new_obj
else
val = JSON.parse(val)
if key is 'bind_to_address'
obj[key] ?= {}
if val[0] is '/'
obj[key].unix_socket = val
else
obj[key].network = val
else if key is 'password'
(obj[key] ||= []).push val
else
obj[key] = val
# arrange audio_outputs by type
obj.audio_output = {}
for audio_output in audio_outputs
obj.audio_output[audio_output.type] = audio_output
return obj

View file

@ -1,12 +0,0 @@
exports.Plugin = class
constructor: (@log, @onStateChanged, @onStatusChanged) ->
@mpd = null
@conf = null
@is_enabled = true
saveState: (state) =>
restoreState: (state) =>
handleRequest: (request, response) => false
setConf: (conf, conf_path) =>
setMpd: (mpd) =>
onSocketConnection: (socket, getPermissions) =>
onSendStatus: (status) =>

View file

@ -1,59 +0,0 @@
Plugin = require('../plugin').Plugin
exports.Plugin = class Chat extends Plugin
constructor: ->
super
# the online users list is always blank at startup
@users = []
restoreState: (state) =>
@next_user_id = state.next_user_id ? 0
@user_names = state.status.user_names ? {}
@chats = state.status.chats ? []
saveState: (state) =>
state.next_user_id = @next_user_id
state.status.users = @users
state.status.user_names = @user_names
state.status.chats = @chats
setMpd: (@mpd) =>
@mpd.on 'chat', @scrubStaleUserNames
onSocketConnection: (socket) =>
user_id = "user_" + @next_user_id
@next_user_id += 1
@users.push user_id
socket.emit 'Identify', user_id
socket.on 'Chat', (data) =>
chat_object =
user_id: user_id
message: data.toString()
@chats.push(chat_object)
chats_limit = 100
@chats.splice(0, @chats.length - chats_limit) if @chats.length > chats_limit
@onStatusChanged()
socket.on 'SetUserName', (data) =>
user_name = data.toString().trim().split(/\s+/).join(" ")
if user_name != ""
user_name_limit = 20
user_name = user_name.substr(0, user_name_limit)
@user_names[user_id] = user_name
else
delete @user_names[user_id]
@onStatusChanged()
socket.on 'disconnect', =>
@users = (id for id in @users when id != user_id)
@scrubStaleUserNames()
@onStatusChanged()
scrubStaleUserNames: =>
keep_user_ids = {}
for user_id in @users
keep_user_ids[user_id] = true
for chat_object in @chats
keep_user_ids[chat_object.user_id] = true
@log.debug "keep_ids #{(copy for copy of keep_user_ids)}"
for user_id of @user_names
delete @user_names[user_id] unless keep_user_ids[user_id]
@onStatusChanged()

View file

@ -1,38 +0,0 @@
fs = require('fs')
path = require('path')
{Plugin} = require('../plugin')
# ability to delete songs from your library
exports.Plugin = class extends Plugin
constructor: ->
super
saveState: (state) =>
state.status.delete_enabled = @is_enabled
onSocketConnection: (socket, getPermissions) =>
socket.on 'DeleteFromLibrary', (data) =>
if not getPermissions().admin
@log.warn "User without admin permission trying to delete songs"
return
files = JSON.parse data.toString()
file = null
next = (err) =>
if err
@log.error "deleting #{file}: #{err.toString()}"
else if file?
@log.info "deleted #{file}"
if not (file = files.shift())?
@mpd.scanFiles files
else # tail call recursion, bitch
fs.unlink path.join(@music_lib_path, file), next
next()
setMpd: (@mpd) =>
setConf: (conf, conf_path) =>
if conf.music_directory?
@music_lib_path = conf.music_directory
else
@is_enabled = false
@log.warn "Delete disabled - music directory not found in #{conf_path}"

View file

@ -1,99 +0,0 @@
Plugin = require('../plugin').Plugin
fs = require 'fs'
url = require 'url'
zipstream = require 'zipstream'
exports.Plugin = class Download extends Plugin
constructor: ->
super
@is_enabled = false
saveState: (state) =>
state.status.download_enabled = @is_enabled
setConf: (conf, conf_path) =>
@is_enabled = true
unless conf.music_directory?
@is_enabled = false
@log.warn "music_directory not found in #{conf_path}. Download disabled."
return
# set up library link
library_link = "./public/library"
try fs.unlinkSync library_link
try
fs.symlinkSync conf.music_directory, library_link
catch error
@is_enabled = false
@log.warn "Unable to link public/library to #{conf.music_directory}: #{error}. Download disabled."
return
try
fs.readdirSync library_link
catch error
@is_enabled = false
@log.warn "Unable to access music directory: #{error}. Download disabled."
return
handleRequest: (request, response) ->
# too bad we don't have startsWith and endsWith
request_path = decodeURI url.parse(request.url).pathname
if request_path == "/library/"
relative_path = ""
zip_name = "library.zip"
else if (match = request_path.match /^\/library\/(.*)\/$/)?
relative_path = "/" + match[1]
zip_name = windowsSafePath(match[1].replace(/\//g, " - ")) + ".zip"
return false unless relative_path?
@log.debug "request to download a library directory: #{relative_path}"
prefix = "./public/library"
walk prefix + relative_path, (err, files) ->
if err
response.writeHead 404, {}
response.end()
return
response.writeHead 200,
"Content-Type": "application/zip"
"Content-Disposition": "attachment; filename=#{zip_name}"
zip = zipstream.createZip {}
zip.pipe response
i = 0
nextFile = ->
file_path = files[i++]
if file_path?
options =
"name": file_path.substr prefix.length + 1
"store": true
zip.addFile fs.createReadStream(file_path), options, nextFile
else
zip.finalize ->
response.end()
nextFile()
return true
# translated from http://stackoverflow.com/a/5827895/367916
walk = (dir, done) ->
results = []
fs.readdir dir, (err, list) ->
return done(err) if err?
i = 0
next = ->
file = list[i++]
return done(null, results) unless file?
file = dir + '/' + file
fs.stat file, (err, stat) ->
if stat?.isDirectory()
walk file, (err, res) ->
results = results.concat res
next()
else
results.push file
next()
next()
windowsSafePath = (string) ->
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
# this is a good start
string.replace /<|>|:|"|\/|\\|\||\?|\*/g, "_"

View file

@ -1,157 +0,0 @@
Plugin = require('../plugin').Plugin
mpd = require '../mpd'
history_size = parseInt(process.env.npm_package_config_dynamicmode_history_size)
future_size = parseInt(process.env.npm_package_config_dynamicmode_future_size)
LAST_QUEUED_STICKER = "groovebasin.last-queued"
exports.Plugin = class DynamicMode extends Plugin
constructor: ->
super
@previous_ids = {}
@is_enabled = false
@got_stickers = false
restoreState: (state) =>
@is_on = state.status.dynamic_mode ? false
@random_ids = state.status.random_ids ? {}
saveState: (state) =>
state.status.dynamic_mode = @is_on
state.status.dynamic_mode_enabled = @is_enabled
state.status.random_ids = @random_ids
setConf: (conf, conf_path) =>
@is_enabled = true
unless conf.sticker_file?
@is_enabled = false
@is_on = false
@log.warn "sticker_file not set in #{conf_path}. Dynamic Mode disabled."
setMpd: (@mpd) =>
@mpd.on 'statusupdate', @checkDynamicMode
@mpd.on 'playlistupdate', @checkDynamicMode
@mpd.on 'libraryupdate', @updateStickers
onSocketConnection: (socket) =>
socket.on 'DynamicMode', (data) =>
return unless @is_enabled
args = JSON.parse data.toString()
@log.debug "DynamicMode args:"
@log.debug args
did_anything = false
for key, value of args
switch key
when "dynamic_mode"
continue if @is_on == value
did_anything = true
@is_on = value
if did_anything
@checkDynamicMode()
@onStatusChanged()
checkDynamicMode: =>
return unless @is_enabled
return unless @mpd.library.artists.length
return unless @got_stickers
item_list = @mpd.playlist.item_list
current_id = @mpd.status.current_item?.id
current_index = -1
all_ids = {}
new_files = []
for item, i in item_list
if item.id == current_id
current_index = i
all_ids[item.id] = true
new_files.push item.track.file unless @previous_ids[item.id]?
# tag any newly queued tracks
@mpd.sendCommands ("sticker set song \"#{file}\" \"#{LAST_QUEUED_STICKER}\" #{JSON.stringify new Date()}" for file in new_files)
# anticipate the changes
@mpd.library.track_table[file].last_queued = new Date() for file in new_files
# if no track is playing, assume the first track is about to be
if current_index == -1
current_index = 0
else
# any tracks <= current track don't count as random anymore
for i in [0..current_index]
delete @random_ids[item_list[i].id]
if @is_on
commands = []
delete_count = Math.max(current_index - history_size, 0)
if history_size < 0
delete_count = 0
for i in [0...delete_count]
commands.push "deleteid #{item_list[i].id}"
add_count = Math.max(future_size + 1 - (item_list.length - current_index), 0)
commands = commands.concat ("addid #{JSON.stringify file}" for file in @getRandomSongFiles add_count)
@mpd.sendCommands commands, (msg) =>
# track which ones are the automatic ones
changed = false
for line in msg.split("\n")
[name, value] = line.split(": ")
continue if name != "Id"
@random_ids[value] = 1
changed = true
@onStatusChanged() if changed
# scrub the random_ids
new_random_ids = {}
for id of @random_ids
if all_ids[id]
new_random_ids[id] = 1
@random_ids = new_random_ids
@previous_ids = all_ids
@onStatusChanged()
updateStickers: =>
@mpd.sendCommand "sticker find song \"/\" \"#{LAST_QUEUED_STICKER}\"", (msg) =>
current_file = null
for line in msg.split("\n")
[name, value] = mpd.split_once line, ": "
if name == "file"
current_file = value
else if name == "sticker"
value = mpd.split_once(value, "=")[1]
track = @mpd.library.track_table[current_file]
if track?
track.last_queued = new Date(value)
else
@log.error "#{current_file} has a last-queued sticker of #{value} but we don't have it in our library cache."
@got_stickers = true
getRandomSongFiles: (count) =>
return [] if count == 0
never_queued = []
sometimes_queued = []
for _, track of @mpd.library.track_table
if track.last_queued?
sometimes_queued.push track
else
never_queued.push track
# backwards by time
sometimes_queued.sort (a, b) =>
b.last_queued.getTime() - a.last_queued.getTime()
# distribution is a triangle for ever queued, and a rectangle for never queued
# ___
# /| |
# / | |
# /__|_|
max_weight = sometimes_queued.length
triangle_area = Math.floor(max_weight * max_weight / 2)
rectangle_area = max_weight * never_queued.length
total_size = triangle_area + rectangle_area
# decode indexes through the distribution shape
files = []
for i in [0...count]
index = Math.random() * total_size
if index < triangle_area
# triangle
track = sometimes_queued[Math.floor Math.sqrt index]
else
# rectangle
track = never_queued[Math.floor((index - triangle_area) / max_weight)]
files.push track.file
files

View file

@ -1,149 +0,0 @@
Plugin = require('../plugin').Plugin
LastFmNode = require('lastfm').LastFmNode
exports.Plugin = class LastFm extends Plugin
constructor: ->
super
@lastfm = new LastFmNode
api_key: process.env.npm_package_config_lastfm_api_key
secret: process.env.npm_package_config_lastfm_secret
@previous_now_playing_id = null
@last_playing_item = null
@playing_start = new Date()
@playing_time = 0
@previous_play_state = null
setTimeout @flushScrobbleQueue, 120000
restoreState: (state) =>
@scrobblers = state.lastfm_scrobblers ? {}
@scrobbles = state.scrobbles ? []
saveState: (state) =>
state.lastfm_scrobblers = @scrobblers
state.scrobbles = @scrobbles
state.status.lastfm_api_key = process.env.npm_package_config_lastfm_api_key
setMpd: (@mpd) =>
@mpd.on 'statusupdate', =>
@updateNowPlaying()
@checkScrobble()
onSocketConnection: (socket) =>
socket.on 'LastfmGetSession', (data) =>
@log.debug "getting session with #{data}"
@lastfm.request "auth.getSession",
token: data.toString()
handlers:
success: (data) =>
# clear them from the scrobblers
delete @scrobblers[data?.session?.name]
socket.emit 'LastfmGetSessionSuccess', JSON.stringify(data)
@log.debug "success from last.fm auth.getSession: #{JSON.stringify data}"
error: (error) =>
@log.error "error from last.fm auth.getSession: #{error.message}"
socket.emit 'LastfmGetSessionError', JSON.stringify(error)
socket.on 'LastfmScrobblersAdd', (data) =>
data_str = data.toString()
@log.debug "LastfmScrobblersAdd: #{data_str}"
params = JSON.parse(data_str)
# ignore if scrobbling user already exists. this is a fake request.
return if @scrobblers[params.username]?
@scrobblers[params.username] = params.session_key
@onStateChanged()
socket.on 'LastfmScrobblersRemove', (data) =>
params = JSON.parse(data.toString())
session_key = @scrobblers[params.username]
if session_key is params.session_key
delete @scrobblers[params.username]
@onStateChanged()
else
@log.warn "Invalid session key from user trying to remove scrobbler: #{params.username}"
flushScrobbleQueue: =>
@log.debug "flushing scrobble queue"
max_simultaneous = 10
count = 0
while (params = @scrobbles.shift())? and count++ < max_simultaneous
@log.info "scrobbling #{params.track} for session #{params.sk}"
params.handlers =
error: (error) =>
@log.error "error from last.fm track.scrobble: #{error.message}"
if not error?.code? or error.code is 11 or error.code is 16
# retryable - add to queue
@scrobbles.push params
@onStateChanged()
@lastfm.request 'track.scrobble', params
@onStateChanged()
queueScrobble: (params) =>
@scrobbles.push params
@onStateChanged()
checkTrackNumber: (trackNumber) =>
if parseInt(trackNumber) >= 0 then trackNumber else ""
checkScrobble: =>
this_item = @mpd.status.current_item
if @mpd.status.state is 'play'
if @previous_play_state isnt 'play'
@playing_start = new Date(new Date().getTime() - @playing_time)
@previous_play_state = @mpd.status.state
@playing_time = new Date().getTime() - @playing_start.getTime()
@log.debug "playtime so far: #{@playing_time}"
return unless this_item?.id isnt @last_playing_item?.id
@log.debug "ids are different"
if (track = @last_playing_item?.track)?
# then scrobble it
min_amt = 15 * 1000
max_amt = 4 * 60 * 1000
half_amt = track.time / 2 * 1000
if @playing_time >= min_amt and (@playing_time >= max_amt or @playing_time >= half_amt)
if track.artist_name
for username, session_key of @scrobblers
@log.debug "queuing scrobble: #{track.name} for #{username}"
@queueScrobble
sk: session_key
timestamp: Math.round(@playing_start.getTime() / 1000)
album: track.album?.name or ""
track: track.name or ""
artist: track.artist_name or ""
albumArtist: track.album_artist_name or ""
duration: track.time or ""
trackNumber: @checkTrackNumber track.track
@flushScrobbleQueue()
else
@log.warn "Not scrobbling #{track.name} - missing artist."
@last_playing_item = this_item
@previous_play_state = @mpd.status.state
@playing_start = new Date()
@playing_time = 0
updateNowPlaying: =>
return unless @mpd.status.state is 'play'
return unless (track = @mpd.status.current_item?.track)?
return unless @previous_now_playing_id isnt @mpd.status.current_item.id
@previous_now_playing_id = @mpd.status.current_item.id
if not track.artist_name
@log.warn "Not updating last.fm now playing for #{track.name}: missing artist"
return
for username, session_key of @scrobblers
@log.debug "update now playing with session_key: #{session_key}, track: #{track.name}, artist: #{track.artist_name}, album: #{track.album?.name}"
@lastfm.request "track.updateNowPlaying",
sk: session_key
track: track.name or ""
artist: track.artist_name or ""
album: track.album?.name or ""
albumArtist: track.album_artist_name or ""
trackNumber: @checkTrackNumber track.track
duration: track.time or ""
handlers:
error: (error) =>
@log.error "error from last.fm track.updateNowPlaying: #{error.message}"

View file

@ -1,31 +0,0 @@
Plugin = require('../plugin').Plugin
exports.Plugin = class Stream extends Plugin
constructor: ->
super
@port = null
@format = null
@is_enabled = false
saveState: (state) =>
state.status.stream_httpd_port = @port
state.status.stream_httpd_format = @format
setConf: (conf, conf_path) =>
@is_enabled = true
if (httpd = conf.audio_output?.httpd)?
@port = httpd.port
if httpd.encoder is 'lame'
@format = 'mp3'
if httpd.quality?
@log.warn "Use audio_output.bitrate for setting quality when using mp3 streaming in #{conf_path}"
else if httpd.encoder is 'vorbis'
@format = 'ogg'
if httpd.bitrate?
@log.warn "Use audio_output.quality for setting quality when using vorbis streaming in #{conf_path}"
else
@format = 'unknown'
if httpd.format isnt "44100:16:2"
@log.warn "Recommended 44100:16:2 for audio_output.format in #{conf_path}"
else
@is_enabled = false
@log.warn "httpd audio_output not enabled in #{conf_path}. Streaming disabled."

View file

@ -1,121 +0,0 @@
Plugin = require('../plugin').Plugin
mpd = require '../mpd'
url = require 'url'
formidable = require 'formidable'
util = require 'util'
mkdirp = require 'mkdirp'
fs = require 'fs'
path = require 'path'
bad_file_chars = {}
bad_file_chars[c] = "_" for c in '/\\?%*:|"<>'
fileEscape = (filename) ->
out = ""
for c in filename
out += bad_file_chars[c] ? c
out
zfill = (n) -> (if n < 10 then "0" else "") + n
getSuggestedPath = (track, default_name=mpd.trackNameFromFile(track.file)) ->
_path = ""
_path += "#{fileEscape track.album_artist_name}/" if track.album_artist_name
_path += "#{fileEscape track.album_name}/" if track.album_name
_path += "#{fileEscape zfill track.track} " if track.track
ext = path.extname(track.file)
if track.name is mpd.trackNameFromFile(track.file)
_path += fileEscape default_name
else
_path += fileEscape track.name
_path += ext
return _path
stripFilename = (_path) ->
parts = _path.split('/')
parts[0...parts.length-1].join('/')
exports.Plugin = class Upload extends Plugin
constructor: ->
super
@is_enabled = false
@random_ids = null
restoreState: (state) =>
@want_to_queue = state.want_to_queue ? []
saveState: (state) =>
state.want_to_queue = @want_to_queue
state.status.upload_enabled = @is_enabled
setConf: (conf, conf_path) =>
@is_enabled = true
unless conf.bind_to_address?.unix_socket?
@is_enabled = false
@log.warn "bind_to_address does not have a unix socket enabled in #{conf_path}. Uploading disabled."
unless conf.bind_to_address?.network == "localhost"
@is_enabled = false
@log.warn "bind_to_address does not have a definition that is 'localhost' in #{conf_path}. Uploading disabled."
if conf.music_directory?
@music_lib_path = conf.music_directory
@music_lib_path += '/' if @music_lib_path.substring(@music_lib_path.length - 1, 1) isnt '/'
else
@is_enabled = false
@log.warn "music directory not found in #{conf_path}. Uploading disabled."
setMpd: (@mpd) =>
@mpd.on 'libraryupdate', @flushWantToQueue
handleRequest: (request, response) =>
parsed_url = url.parse(request.url)
return false unless parsed_url.pathname is '/upload' and request.method is 'POST'
unless @is_enabled
response.writeHead 500, {'content-type': 'text/plain'}
response.end JSON.stringify {success: false, reason: "Uploads disabled"}
return true
form = new formidable.IncomingForm()
form.parse request, (err, fields, file) =>
tmp_with_ext = file.qqfile.path + path.extname(file.qqfile.filename)
@moveFile file.qqfile.path, tmp_with_ext, =>
@mpd.getFileInfo "file://#{tmp_with_ext}", (track) =>
suggested_path = getSuggestedPath(track, file.qqfile.filename)
dest = @music_lib_path + suggested_path
mkdirp stripFilename(dest), (err) =>
if err
@log.error err
else
@moveFile tmp_with_ext, dest, =>
@want_to_queue.push suggested_path
@onStateChanged()
@log.info "Track was uploaded: #{dest}"
response.writeHead 200, {'content-type': 'text/html'}
response.end JSON.stringify {success: true}
return true
onSendStatus: (status) =>
@random_ids = status?.random_ids
queueFilesPos: =>
pos = @mpd.playlist.item_list.length
return pos unless @random_ids?
for item, i in @mpd.playlist.item_list
return i if @random_ids[item.id]?
flushWantToQueue: =>
i = 0
files = []
while i < @want_to_queue.length
file = @want_to_queue[i]
if @mpd.library.track_table[file]?
files.push file
@want_to_queue.splice i, 1
else
i++
@mpd.queueFiles files, @queueFilesPos()
@onStateChanged() if files.length
moveFile: (source, dest, cb=->) =>
in_stream = fs.createReadStream(source)
out_stream = fs.createWriteStream(dest)
out_stream.on 'error', (error) => @log.error error
util.pump in_stream, out_stream, -> fs.unlink source, cb

Some files were not shown because too many files have changed in this diff Show more