Compare commits
469 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a27807082a | ||
|
|
fae50a8d69 | ||
|
|
fa21bdc3d2 | ||
|
|
cb6077bc87 | ||
|
|
7ea657e0d4 | ||
|
|
0e3f87b8f1 | ||
|
|
ec7ebcfda1 | ||
|
|
bb1effc161 | ||
|
|
656d3068c4 | ||
|
|
9a24b61d81 | ||
|
|
6745455a74 | ||
|
|
26474c9002 | ||
|
|
35648a0a1d | ||
|
|
7527911bb2 | ||
|
|
3971dc65f6 | ||
|
|
92f0c80e9f | ||
|
|
d17157fcc9 | ||
|
|
45b8d28dcb | ||
|
|
4133c55fa7 | ||
|
|
53d1cbe9b0 | ||
|
|
fa8fbf60fc | ||
|
|
3c02511c9e | ||
|
|
d642629fd0 | ||
|
|
b861d4776c | ||
|
|
656aab43ab | ||
|
|
876e72db0c | ||
|
|
f36efe16c7 | ||
|
|
8c2b78efc8 | ||
|
|
8fe56b52bf | ||
|
|
b1a48d5b27 | ||
|
|
43d96fb348 | ||
|
|
8f13aa795a | ||
|
|
7d0ac9dfd4 | ||
|
|
2b3aff220f | ||
|
|
cc8f0abea4 | ||
|
|
145e39ff02 | ||
|
|
52e54b0152 | ||
|
|
e1bd006566 | ||
|
|
453143040d | ||
|
|
6dbfb820ea | ||
|
|
7cc928e1e9 | ||
|
|
deed3a7eb8 | ||
|
|
bf83c04902 | ||
|
|
ac69158091 | ||
|
|
94ea856fae | ||
|
|
f9c820bdbf | ||
|
|
78cd66d8df | ||
|
|
259f247290 | ||
|
|
cc7183ce53 | ||
|
|
e02c986e86 | ||
|
|
057a2e8776 | ||
|
|
c6ada32047 | ||
|
|
3325a248ee | ||
|
|
f6054e662b | ||
|
|
f91849db47 | ||
|
|
3fee3df88c | ||
|
|
9fa3ebac52 | ||
|
|
6a959e6d12 | ||
|
|
09463c9d0a | ||
|
|
5c8f45b6e9 | ||
|
|
4e560b93e8 | ||
|
|
c230081b3c | ||
|
|
0d6e6cb203 | ||
|
|
ab1b828f4d | ||
|
|
1ec74cea49 | ||
|
|
693e06a137 | ||
|
|
207f77e4c9 | ||
|
|
cde56a3e6b | ||
|
|
c7e2387325 | ||
|
|
dec92a1b39 | ||
|
|
b0bc750c9b | ||
|
|
398e3e9db8 | ||
|
|
9fa80e43ff | ||
|
|
1c0129a779 | ||
|
|
b0839774f0 | ||
|
|
113f008115 | ||
|
|
a4b0fa4a12 | ||
|
|
b693b9f6f6 | ||
|
|
de234fde4f | ||
|
|
825fa5d5d0 | ||
|
|
889e8b0bec | ||
|
|
540266c058 | ||
|
|
bf02f0d3c5 | ||
|
|
17f86d9735 | ||
|
|
42f66c817f | ||
|
|
11068623b5 | ||
|
|
3be4ad9cb6 | ||
|
|
1a7dc0e0a3 | ||
|
|
d30276f415 | ||
|
|
af72b80c60 | ||
|
|
515845e3f9 | ||
|
|
44609d7ca3 | ||
|
|
0115580727 | ||
|
|
b6479f868c | ||
|
|
628b1f2e4c | ||
|
|
ce9f89aa3f | ||
|
|
b5cc67ab7e | ||
|
|
be90006315 | ||
|
|
90e2bf10d5 | ||
|
|
565aed7915 | ||
|
|
c59a4befae | ||
|
|
56358da460 | ||
|
|
2b4225e156 | ||
|
|
ece81343c6 | ||
|
|
929260899c | ||
|
|
a2816003be | ||
|
|
9ccec6485a | ||
|
|
d811c2e7f6 | ||
|
|
e0a6b97fff | ||
|
|
0358e5270f | ||
|
|
baf5ebeb4e | ||
|
|
2756f4c06f | ||
|
|
87f4657484 | ||
|
|
9c24952e3f | ||
|
|
72cf1783b7 | ||
|
|
543a735848 | ||
|
|
1d3bc7c3db | ||
|
|
70e445c80d | ||
|
|
71521d90f6 | ||
|
|
f748199894 | ||
|
|
c736632282 | ||
|
|
9768c840b9 | ||
|
|
0852ab8cff | ||
|
|
223afa1ed0 | ||
|
|
3525d6a4e0 | ||
|
|
59d0f8cd62 | ||
|
|
99c92a9203 | ||
|
|
ea77be50b5 | ||
|
|
6068a19f3e | ||
|
|
a345f24f93 | ||
|
|
5c8e0fcf9e | ||
|
|
bc6ecfb65d | ||
|
|
725f10ef97 | ||
|
|
c5bb5b2e66 | ||
|
|
09f64ecb5c | ||
|
|
f2712ddaad | ||
|
|
a0c6d8c36d | ||
|
|
5102ef205c | ||
|
|
7696b9791f | ||
|
|
0ca3c286f2 | ||
|
|
fbc6386960 | ||
|
|
35354ac81e | ||
|
|
406eae55aa | ||
|
|
667fd0c76f | ||
|
|
9be515c764 | ||
|
|
e937b798e4 | ||
|
|
c018b6cb3a | ||
|
|
b1755a4fd9 | ||
|
|
3a953c22e4 | ||
|
|
d004ca79e5 | ||
|
|
585b13bad1 | ||
|
|
6e567086d9 | ||
|
|
920884263b | ||
|
|
f23a31d5db | ||
|
|
e51e19846d | ||
|
|
e8fa0e83a3 | ||
|
|
054f025064 | ||
|
|
22a0507595 | ||
|
|
c2e4429e48 | ||
|
|
69aefc0739 | ||
|
|
5951806436 | ||
|
|
cf30edab0f | ||
|
|
b258ac5ce9 | ||
|
|
0a5cab8370 | ||
|
|
866c7e62d5 | ||
|
|
3196ba9494 | ||
|
|
de7f535e50 | ||
|
|
65c58a95da | ||
|
|
03404fa01f | ||
|
|
4c78a11b64 | ||
|
|
cc2b619978 | ||
|
|
5eb430da3b | ||
|
|
f23ec23c0e | ||
|
|
990b4177e2 | ||
|
|
5aaaa26348 | ||
|
|
78648898b9 | ||
|
|
1c13ec2068 | ||
|
|
0b345463da | ||
|
|
3adc2b8759 | ||
|
|
14540b14b1 | ||
|
|
5c1f18abf1 | ||
|
|
a796e51724 | ||
|
|
2843faa39b | ||
|
|
3e270b68d1 | ||
|
|
6acf4a89ed | ||
|
|
2e36bfa2f3 | ||
|
|
ee83c996fa | ||
|
|
972d2cd0f8 | ||
|
|
11ea30fecb | ||
|
|
22c13caeda | ||
|
|
5331c55306 | ||
|
|
2fec70a126 | ||
|
|
76dcbe7c15 | ||
|
|
a265f082e0 | ||
|
|
d21f81877d | ||
|
|
31375a5afa | ||
|
|
e859d86940 | ||
|
|
e4538f67a5 | ||
|
|
601f1d054e | ||
|
|
c9c5241c83 | ||
|
|
1c660be774 | ||
|
|
8c1b01cd7d | ||
|
|
d45530e5e7 | ||
|
|
a6fb766787 | ||
|
|
69acca675e | ||
|
|
0dd8994b0b | ||
|
|
90792a3a4f | ||
|
|
ae8de85c07 | ||
|
|
11a306c4df | ||
|
|
bf4800b278 | ||
|
|
d5be670a27 | ||
|
|
f77e7d534c | ||
|
|
514cb3eb66 | ||
|
|
4ad9700e2e | ||
|
|
634015bc89 | ||
|
|
d1421ca7d1 | ||
|
|
593de05286 | ||
|
|
e5abea9b2a | ||
|
|
faf3a9abf0 | ||
|
|
ccb7781909 | ||
|
|
cc1e779926 | ||
|
|
258a8d35eb | ||
|
|
d1bf652f87 | ||
|
|
7b1433b3c1 | ||
|
|
c9f6fc22a2 | ||
|
|
07a75eea39 | ||
|
|
7d53ecf42c | ||
|
|
7a38834093 | ||
|
|
8724737bbc | ||
|
|
f19fd10bb7 | ||
|
|
25c816872a | ||
|
|
5302a2880d | ||
|
|
1349bf6f91 | ||
|
|
983937132e | ||
|
|
02ffef20bc | ||
|
|
3d73555f20 | ||
|
|
d841d01ed3 | ||
|
|
4d949ccb33 | ||
|
|
0770149c55 | ||
|
|
83ec17de20 | ||
|
|
d6a685507a | ||
|
|
83f93be3ab | ||
|
|
53d17666ff | ||
|
|
dc268cda0a | ||
|
|
6d9d2846a0 | ||
|
|
e84277a0b5 | ||
|
|
0a11734015 | ||
|
|
7294ed2397 | ||
|
|
ddbdac738a | ||
|
|
64aacb4312 | ||
|
|
5bbfe519c2 | ||
|
|
940396e588 | ||
|
|
d6bf428cc3 | ||
|
|
a2c0a5e62b | ||
|
|
da02be78af | ||
|
|
e07ea0c3b8 | ||
|
|
c13241484c | ||
|
|
27ac56243f | ||
|
|
1804bc40d9 | ||
|
|
28174551a9 | ||
|
|
55c5c088e4 | ||
|
|
d32fff0a3f | ||
|
|
4226183402 | ||
|
|
653148cd39 | ||
|
|
f60b414852 | ||
|
|
77c3f6b831 | ||
|
|
f0a8309f84 | ||
|
|
317d7dc7fb | ||
|
|
f13e58155e | ||
|
|
493d7d26f2 | ||
|
|
454a326442 | ||
|
|
cdf4068ef7 | ||
|
|
77c74e34ac | ||
|
|
e490c171c2 | ||
|
|
dd22d6465e | ||
|
|
13edb5c3aa | ||
|
|
e1da9b3339 | ||
|
|
704d1bbb3c | ||
|
|
8b9923c07e | ||
|
|
b3876c4c0e | ||
|
|
a6ad49ca53 | ||
|
|
2b4adcd8c5 | ||
|
|
9e97651153 | ||
|
|
2f8c64bc77 | ||
|
|
d7e8f875fa | ||
|
|
b9e1101c54 | ||
|
|
b1d7750da7 | ||
|
|
1e31ce3651 | ||
|
|
3b91ee6514 | ||
|
|
2b897818d0 | ||
|
|
5d332aef87 | ||
|
|
fa27a28fac | ||
|
|
7b5f7b6f41 | ||
|
|
92c40b661c | ||
|
|
c2488d6ca2 | ||
|
|
1cd7b916ad | ||
|
|
067e40e288 | ||
|
|
77fae4847a | ||
|
|
33bd38d333 | ||
|
|
8c25b9056f | ||
|
|
ffa639b126 | ||
|
|
058768262d | ||
|
|
1acb3d4345 | ||
|
|
4f184bedef | ||
|
|
b1afc21542 | ||
|
|
0d4200b7ba | ||
|
|
9026ca64b9 | ||
|
|
dd3b94b395 | ||
|
|
ae4d4ece5a | ||
|
|
4251c21504 | ||
|
|
8151a09535 | ||
|
|
f3fe53f8c6 | ||
|
|
6caabeb2b7 | ||
|
|
de63299146 | ||
|
|
83dcfa22ec | ||
|
|
7884a544c4 | ||
|
|
9df89da399 | ||
|
|
9b83c23ee0 | ||
|
|
4ab273e09c | ||
|
|
72f359c358 | ||
|
|
8090be3fa3 | ||
|
|
a9226285b1 | ||
|
|
8149bbab11 | ||
|
|
bcd0dd62b9 | ||
|
|
22def50783 | ||
|
|
2cb1a5de9c | ||
|
|
3e2a86ff6c | ||
|
|
c4de6edf44 | ||
|
|
7114b72176 | ||
|
|
f636e7d6c7 | ||
|
|
6940043de2 | ||
|
|
171e741138 | ||
|
|
25902935c5 | ||
|
|
d45a4ecad3 | ||
|
|
790a1d3689 | ||
|
|
8336da00da | ||
|
|
041be54efa | ||
|
|
d15f0792fa | ||
|
|
42e8b9bc40 | ||
|
|
db1a14b0b1 | ||
|
|
cb4b0302c8 | ||
|
|
eb24d4827e | ||
|
|
38722a72ec | ||
|
|
e691b6cf02 | ||
|
|
48bd88b27a | ||
|
|
c789febe7f | ||
|
|
c8afc5b569 | ||
|
|
55988b45de | ||
|
|
8c69710f7f | ||
|
|
f88758bb1a | ||
|
|
bd51ef0647 | ||
|
|
1f16013436 | ||
|
|
a35e3dbcf9 | ||
|
|
297e031555 | ||
|
|
ba5a955053 | ||
|
|
6fb72a7f1a | ||
|
|
fd8ac91623 | ||
|
|
61a1474ef2 | ||
|
|
8e22ebbee9 | ||
|
|
ca3183458c | ||
|
|
09eafbcf36 | ||
|
|
5adea466bc | ||
|
|
f53b878869 | ||
|
|
b8275a4170 | ||
|
|
88d96dc11c | ||
|
|
7ad7e21a0a | ||
|
|
a596786a58 | ||
|
|
528653d2d5 | ||
|
|
ffa5c4dd75 | ||
|
|
90fba721c4 | ||
|
|
2909750c9d | ||
|
|
b229318c77 | ||
|
|
9cd649507f | ||
|
|
abca154122 | ||
|
|
bef0b5940b | ||
|
|
ebdcc6aa7d | ||
|
|
431e95fc37 | ||
|
|
60fed15769 | ||
|
|
d5ba226320 | ||
|
|
ffb718d6a4 | ||
|
|
03a3d7fca9 | ||
|
|
736de99f05 | ||
|
|
e79f3f647c | ||
|
|
f5c2b8b054 | ||
|
|
31e266d77e | ||
|
|
ff8e794587 | ||
|
|
3cca9f2e26 | ||
|
|
1f125afa64 | ||
|
|
2a1f171a78 | ||
|
|
e5f399c30a | ||
|
|
4bcd76c3ad | ||
|
|
eb23c8bc4a | ||
|
|
27649f6838 | ||
|
|
ca98326cb8 | ||
|
|
ec458146de | ||
|
|
2b05602357 | ||
|
|
c80bbffb85 | ||
|
|
ce0dcee396 | ||
|
|
b50ec33d6c | ||
|
|
3ab3b52745 | ||
|
|
fff4566018 | ||
|
|
f07a2292c4 | ||
|
|
212d1b3388 | ||
|
|
dc293281e2 | ||
|
|
880569f9c4 | ||
|
|
e37f483259 | ||
|
|
696ab6ae13 | ||
|
|
3e9eb5244c | ||
|
|
8108cd7d88 | ||
|
|
65e6c01461 | ||
|
|
90a5b9ed2a | ||
|
|
bccd690fd4 | ||
|
|
9f600dffea | ||
|
|
22d68befc3 | ||
|
|
1bda313393 | ||
|
|
e5f3febf6d | ||
|
|
55a0cf0f59 | ||
|
|
8ffa8ddf39 | ||
|
|
e35a778e3e | ||
|
|
18c1743618 | ||
|
|
c5acea0572 | ||
|
|
fe39aa4501 | ||
|
|
525d5072a2 | ||
|
|
76b8f602a3 | ||
|
|
d856321789 | ||
|
|
f6ea244c44 | ||
|
|
89a1865fe5 | ||
|
|
872dca5916 | ||
|
|
fbb53ee861 | ||
|
|
8c64fa74ec | ||
|
|
5dfa9c8a24 | ||
|
|
8f1a2d685e | ||
|
|
cb31102976 | ||
|
|
97cd77dab5 | ||
|
|
3f228e51d3 | ||
|
|
ded94c9e2a | ||
|
|
b31531722d | ||
|
|
afffbed6eb | ||
|
|
5e177ae037 | ||
|
|
4cd519089d | ||
|
|
994f84acc6 | ||
|
|
d8e0cecd20 | ||
|
|
5e70b17795 | ||
|
|
e45fc6a9fb | ||
|
|
73e0cb0961 | ||
|
|
45dcded470 | ||
|
|
36d20e2c45 | ||
|
|
3f5f96ceb5 | ||
|
|
76c715255b | ||
|
|
e7cc261be0 | ||
|
|
6b70bbc1c3 | ||
|
|
599d1e0ad5 | ||
|
|
bf531b39a0 | ||
|
|
2c1222254f | ||
|
|
5387067af0 | ||
|
|
9d7904cd90 | ||
|
|
725ef9659d | ||
|
|
6d682593f5 | ||
|
|
7a3fdfaab3 | ||
|
|
67b7b81375 | ||
|
|
490227b168 | ||
|
|
d7a57bd342 | ||
|
|
027fde923b | ||
|
|
3f04a55552 | ||
|
|
09b26d0312 | ||
|
|
efc3880ebf | ||
|
|
6e49bb5617 | ||
|
|
6fce8e21f6 | ||
|
|
94663d5617 |
17
.gitignore
vendored
|
|
@ -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
|
|
@ -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.
|
||||
}
|
||||
13
.npmignore
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
_build
|
||||
_static
|
||||
_templates
|
||||
287
docs/changelog.rst
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
1897
lib/player.js
Normal file
347
lib/player_server.js
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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();
|
||||
51
lib/web_socket_api_client.js
Normal 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');
|
||||
});
|
||||
};
|
||||
68
package.json
|
|
@ -1,48 +1,54 @@
|
|||
{
|
||||
"name": "groovebasin",
|
||||
"description": "No-nonsense music client and daemon based on mpd",
|
||||
"description": "Music player server with a web-based interface inspired by Amarok 1.4",
|
||||
"author": "Andrew Kelley <superjoe30@gmail.com>",
|
||||
"version": "0.1.1",
|
||||
"licenses": [{
|
||||
"version": "1.0.1",
|
||||
"licenses": [
|
||||
{
|
||||
"type": "MIT",
|
||||
"url": "https://raw.github.com/superjoe30/groovebasin/master/LICENSE"
|
||||
}],
|
||||
"url": "https://raw.github.com/andrewrk/groovebasin/master/LICENSE"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": "~0.8.2"
|
||||
"node": ">=0.10.20"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/superjoe30/groovebasin.git"
|
||||
"url": "git://github.com/andrewrk/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"
|
||||
"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": {
|
||||
"coffee-script": "~1.3.3",
|
||||
"handlebars": "~1.0.4beta",
|
||||
"stylus": "~0.28.1",
|
||||
"node-dev": "~0.2.3"
|
||||
"stylus": "^0.42.3",
|
||||
"browserify": "^3.41.0"
|
||||
},
|
||||
"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
|
||||
"build": "npm install && ./build",
|
||||
"dev": "npm run build && npm start"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"> </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>
|
||||
|
Before Width: | Height: | Size: 260 B |
|
Before Width: | Height: | Size: 98 B |
|
Before Width: | Height: | Size: 83 B |
|
Before Width: | Height: | Size: 84 B |
|
Before Width: | Height: | Size: 83 B |
|
Before Width: | Height: | Size: 180 B |
|
Before Width: | Height: | Size: 211 B |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
|
@ -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%; }
|
||||
31
public/vendor/fileuploader/fileuploader.css
vendored
|
|
@ -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;}
|
||||
1276
public/vendor/fileuploader/fileuploader.js
vendored
BIN
public/vendor/fileuploader/loading.gif
vendored
|
Before Width: | Height: | Size: 455 B |
223
public/vendor/handlebars.runtime.js
vendored
|
|
@ -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 = {
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
"`": "`"
|
||||
};
|
||||
|
||||
var badChars = /&(?!\w+;)|[<>"'`]/g;
|
||||
var possible = /[&<>"'`]/;
|
||||
|
||||
var escapeChar = function(chr) {
|
||||
return escape[chr] || "&";
|
||||
};
|
||||
|
||||
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;
|
||||
;
|
||||
4
public/vendor/jquery-1.7.1.min.js
vendored
356
public/vendor/jquery-ui-1.8.17.custom.min.js
vendored
BIN
public/vendor/socket.io/WebSocketMain.swf
generated
vendored
BIN
public/vendor/socket.io/WebSocketMainInsecure.swf
generated
vendored
3750
public/vendor/socket.io/socket.io.js
generated
vendored
2
public/vendor/socket.io/socket.io.min.js
generated
vendored
|
|
@ -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);
|
||||
BIN
public/vendor/soundmanager2/soundmanager2_flash9.swf
vendored
216
readme.md
|
|
@ -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
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 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
|
||||
|
||||
2607
src/client/app.js
Normal file
542
src/client/playerclient.js
Normal 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
|
|
@ -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,
|
||||
}));
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -139,11 +143,6 @@ body
|
|||
margin 4px
|
||||
width 175px
|
||||
|
||||
span.chat-user
|
||||
color #8888ff
|
||||
span.chat-user-self
|
||||
color #888888
|
||||
|
||||
#user-id
|
||||
cursor pointer
|
||||
|
||||
|
|
@ -223,6 +222,7 @@ span.chat-user-self
|
|||
color #75abff
|
||||
|
||||
div.cursor
|
||||
span
|
||||
text-decoration underline
|
||||
|
||||
div.border-bottom
|
||||
|
|
@ -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
|
||||
7
src/client/styles/vendor/jquery-ui-1.10.4.custom.min.css
vendored
Normal 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
|
||||
|
||||
|
|
@ -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}}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<ul>
|
||||
{{#each chats}}
|
||||
<li>
|
||||
<span class="{{class}}">{{user_name}}</span>: {{message}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{{#each users}}
|
||||
<li>
|
||||
<span class="{{class}}">{{user_name}}</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
|
|
@ -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}}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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}}
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>< <em>or</em> Ctrl + Left <em>and</em> > <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>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
BIN
src/public/images/animated-overlay.gif
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/public/images/ui-bg_diagonals-thick_15_0b3e6f_40x40.png
Normal file
|
After Width: | Height: | Size: 424 B |
BIN
src/public/images/ui-bg_dots-medium_30_0b58a2_4x4.png
Normal file
|
After Width: | Height: | Size: 237 B |
BIN
src/public/images/ui-bg_dots-small_20_333333_2x2.png
Normal file
|
After Width: | Height: | Size: 206 B |
BIN
src/public/images/ui-bg_dots-small_30_a32d00_2x2.png
Normal file
|
After Width: | Height: | Size: 218 B |
BIN
src/public/images/ui-bg_dots-small_40_00498f_2x2.png
Normal file
|
After Width: | Height: | Size: 224 B |
BIN
src/public/images/ui-bg_flat_0_aaaaaa_40x100.png
Normal file
|
After Width: | Height: | Size: 212 B |
BIN
src/public/images/ui-bg_flat_40_292929_40x100.png
Normal file
|
After Width: | Height: | Size: 230 B |
BIN
src/public/images/ui-bg_gloss-wave_20_111111_500x100.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/public/images/ui-icons_00498f_256x240.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src/public/images/ui-icons_98d2fb_256x240.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src/public/images/ui-icons_9ccdfc_256x240.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src/public/images/ui-icons_ffffff_256x240.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
src/public/img/bright-10.png
Normal file
|
After Width: | Height: | Size: 141 B |
|
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 5 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
365
src/public/index.html
Normal 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"> </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>< <em>or</em> Ctrl + Left <em>and</em> > <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 & 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
7
src/public/vendor/jquery-ui-1.10.4.custom.min.js
vendored
Normal 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
|
||||
|
|
@ -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) =>
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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}"
|
||||
|
|
@ -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, "_"
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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}"
|
||||
|
||||
|
||||
|
|
@ -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."
|
||||
|
|
@ -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
|
||||
|
||||