Compare commits

...

224 Commits

Author SHA1 Message Date
IrosTheBeggar
7d40b034ed Merge branch 'master' of github.com:IrosTheBeggar/mStream into master 2025-04-07 14:09:09 -04:00
IrosTheBeggar
0a22c1f738 fix missing limit bug 2025-04-07 14:08:44 -04:00
Paul
41c1432eeb
Merge pull request #489 from Hegi/feature/format-date-time-for-shared-playlist
feat: format expire date on admin ui
2025-04-05 20:29:28 -04:00
Istvan Hegedus
adacc6a0f6
feat: format expire date on admin ui 2025-01-27 22:31:01 -07:00
IrosTheBeggar
81a931c884 5.13.1 2024-12-21 11:27:27 -05:00
IrosTheBeggar
ace6d952ec remove 32 bit windows build 2024-12-21 11:27:11 -05:00
IrosTheBeggar
0547301905 5.13.0 2024-12-21 11:05:29 -05:00
IrosTheBeggar
50b0cf1d1e remove binaries 2024-12-21 11:03:19 -05:00
IrosTheBeggar
ea2736432b remove binaries 2024-12-21 11:00:57 -05:00
IrosTheBeggar
63d4d5eb02 code cleanup 2024-12-21 10:40:01 -05:00
IrosTheBeggar
dd8dc84a07 config to use new scan code 2024-12-21 01:37:35 -05:00
IrosTheBeggar
2b296220c2 config to use new scan code 2024-12-21 01:03:46 -05:00
IrosTheBeggar
bada002845 new scanner code 2024-12-21 00:45:57 -05:00
IrosTheBeggar
af2fad9dd1 compress images defaults to true 2024-12-21 00:27:10 -05:00
IrosTheBeggar
e6bb1b3322 Merge branch 'master' of github.com:IrosTheBeggar/mStream into master 2024-12-21 00:22:03 -05:00
IrosTheBeggar
17da6533de update packages 2024-12-21 00:19:48 -05:00
IrosTheBeggar
e38725e755 update packages 2024-12-21 00:15:40 -05:00
Paul
4055f5411b
Update README.md 2024-07-15 21:28:53 -04:00
Paul
183e45faf3
Update README.md 2024-07-15 21:26:18 -04:00
IrosTheBeggar
02ae8c1ada update github actions 2024-05-05 21:06:04 -04:00
IrosTheBeggar
6488cd1db7 v5.12.2 2024-05-05 20:42:41 -04:00
IrosTheBeggar
09dd860bd3 update npm packages 2024-05-05 20:40:48 -04:00
IrosTheBeggar
4200b08e5a update github actions 2024-05-05 20:36:41 -04:00
IrosTheBeggar
b9945ba75e update github action 2024-05-05 20:31:28 -04:00
IrosTheBeggar
69332b6393 v5.12.1 2024-05-05 10:46:19 -04:00
IrosTheBeggar
b759ef8d48 update github actions 2024-05-05 10:41:25 -04:00
IrosTheBeggar
3bd282dd4a fix node min version 2024-05-05 10:38:58 -04:00
IrosTheBeggar
688dc55c9e fix node min version 2024-05-05 10:37:11 -04:00
IrosTheBeggar
9ddb83761c v5.12.0 2024-03-13 23:42:53 -04:00
IrosTheBeggar
380bda7652 update electron version 2024-03-13 23:42:24 -04:00
IrosTheBeggar
164a97f86a update dependency 2024-03-13 23:37:46 -04:00
IrosTheBeggar
14b6c92644 update dependacy 2024-03-13 16:40:53 -04:00
IrosTheBeggar
70fdf88f88 fix xml parser 2024-03-13 16:17:23 -04:00
IrosTheBeggar
3e7c5092e5 fix syncthing enable api 2024-03-13 16:08:43 -04:00
IrosTheBeggar
c686ace36c live playlist mode 2024-02-07 00:46:41 -05:00
IrosTheBeggar
b66c6d5be3 live playlist mode 2024-02-07 00:34:49 -05:00
IrosTheBeggar
c37b87e631 live playlist mode 2024-01-28 19:30:34 -05:00
Paul
06f15420d8
Merge pull request #415 from nguyenbnt/master
Fix config folders when adding a new directory
2023-11-09 09:36:51 -05:00
IrosTheBeggar
795d68cbe5 update npm dependencies 2023-11-07 16:04:16 -05:00
Nguyen Bui
49924b3450 Fix config folders when adding a new directory 2023-06-28 10:44:55 +07:00
IrosTheBeggar
01d63ed9e1 remove regex validation for username 2022-12-10 23:10:07 -05:00
IrosTheBeggar
d58ec37679 revert music metadata 2022-12-10 00:44:01 -05:00
IrosTheBeggar
ba51894cab update deps 2022-12-10 00:29:41 -05:00
IrosTheBeggar
c1a2114773 update electron builder 2022-12-10 00:22:06 -05:00
Paul
fb1e080a42
Merge pull request #384 from viktor02/master
Added page title changes
2022-12-10 00:04:51 -05:00
Paul
c84b4c822d
Merge pull request #398 from tdammers/issue-397
Escape question marks in filenames
2022-12-09 23:56:51 -05:00
Paul
1146fd914f
Merge pull request #388 from thewriteway/master
update dependencies to latest supported versions.
2022-12-09 23:49:16 -05:00
Tobias Dammers
cb9992836c Escape question marks in filenames 2022-11-30 14:15:50 +01:00
chosenpath
d0077659c6 update m3u8-parser dependency 2022-09-29 09:02:21 +01:00
chosenpath
b37414745f update dependencies to latest supported versions. 2022-09-27 17:13:54 +01:00
Viktor Karpov
30af7689f9 fix: swap artist and name position 2022-08-03 06:18:26 +03:00
Viktor Karpov
a7439ebf3b Added page title changes 2022-08-03 03:47:42 +03:00
IrosTheBeggar
6d5fb03e3f loop one song option 2022-04-26 21:31:35 -04:00
IrosTheBeggar
780fbc56d8 chnage default transcode algo 2022-04-05 13:09:29 -04:00
IrosTheBeggar
27f276bb31 ui update 2022-03-29 13:32:05 -04:00
IrosTheBeggar
e587b4b21d stats 2022-03-29 13:19:34 -04:00
IrosTheBeggar
ad9764a149 new scrobble api 2022-03-28 23:12:50 -04:00
IrosTheBeggar
5706ec8647 audio book folders on server 2022-03-26 18:00:13 -04:00
IrosTheBeggar
21565a42cb add genre to db 2022-03-26 00:47:57 -04:00
IrosTheBeggar
f037aa9d1d update dependencies 2022-03-24 14:37:23 -04:00
IrosTheBeggar
c94c5c5eea vpath filter for db api 2022-03-24 13:52:01 -04:00
IrosTheBeggar
765286ccd5 v5.11.4 2022-03-22 11:27:00 -04:00
IrosTheBeggar
24c177a163 selfhost play stoee logo 2022-03-21 14:20:08 -04:00
IrosTheBeggar
bad3895d06 Update readme 2022-03-21 14:01:11 -04:00
IrosTheBeggar
22c12ee030 v5.11.3 2022-02-27 01:45:17 -05:00
IrosTheBeggar
55205f12ad add fileapth to metadata panel 2022-02-27 01:43:05 -05:00
IrosTheBeggar
95439866e9 metadata info 2022-02-27 01:27:53 -05:00
IrosTheBeggar
d1c278ebf7 increase cookie experation time 2022-02-26 23:47:32 -05:00
IrosTheBeggar
0c327a3a77 install docs 2022-02-26 23:46:37 -05:00
IrosTheBeggar
40579fb5e9 v5.11.2 2022-02-22 00:13:18 -05:00
IrosTheBeggar
b79ebb6c07 edit server max request size 2022-02-22 00:05:12 -05:00
IrosTheBeggar
47cf9e366a allow larger requests 2022-02-21 12:33:10 -05:00
IrosTheBeggar
00411f7845 pull metadata on file explorer api 2022-02-20 20:14:52 -05:00
IrosTheBeggar
9e22625476 batch metadata endpoint 2022-02-20 19:30:52 -05:00
IrosTheBeggar
086ead9b17 v5.11.0 2022-02-02 13:36:13 -05:00
IrosTheBeggar
e5e8750e9c image compression script 2022-02-02 13:32:59 -05:00
IrosTheBeggar
825633152e add compression to webapp 2022-02-02 00:58:56 -05:00
IrosTheBeggar
f93ba629b4 update admin page 2022-02-02 00:45:43 -05:00
IrosTheBeggar
f0408dd659 compression edits 2022-02-02 00:28:16 -05:00
IrosTheBeggar
7ec3774ea7 update album art api 2022-02-01 21:34:31 -05:00
IrosTheBeggar
6e38686c30 image compression 2022-02-01 17:02:01 -05:00
IrosTheBeggar
cb1d48db4b bump min version 2022-01-31 10:45:13 -05:00
IrosTheBeggar
4f466e4595 v5.10.0 2022-01-31 01:22:19 -05:00
IrosTheBeggar
7f4f5283ec update dependacies 2022-01-31 01:21:03 -05:00
IrosTheBeggar
b4aa11af9a IPv6 support 2022-01-31 00:56:15 -05:00
IrosTheBeggar
3ea7a48b62 code cleanup 2022-01-31 00:24:55 -05:00
IrosTheBeggar
4763202afd fix redirect bug 2022-01-31 00:20:14 -05:00
IrosTheBeggar
48638fc363 fix async error bug 2022-01-30 22:52:10 -05:00
IrosTheBeggar
5ad80e40f5 another access error 2022-01-30 21:41:41 -05:00
IrosTheBeggar
de061eecb6 fix access error 2022-01-30 21:34:59 -05:00
IrosTheBeggar
e48bed3e0f v5.9.4 2021-12-29 10:27:56 -05:00
IrosTheBeggar
369ac4b68f whoops 2021-12-29 10:25:06 -05:00
IrosTheBeggar
723314a3e7 v5.9.2 2021-12-29 10:17:29 -05:00
IrosTheBeggar
db01674eaf fix pakcage json for webapp 2021-12-29 10:14:37 -05:00
IrosTheBeggar
e665e98680 build webapp step 2021-12-29 10:09:24 -05:00
IrosTheBeggar
5192139568 desktop app stuff 2021-12-29 10:00:59 -05:00
IrosTheBeggar
3a8827c533 v5.9.1 2021-12-28 11:58:17 -05:00
IrosTheBeggar
c76c055d18 bugfixes and styling fixes 2021-12-28 11:20:57 -05:00
IrosTheBeggar
c4995ede32 v5.9.0 2021-12-28 00:00:15 -05:00
IrosTheBeggar
8827acdd7e use cookies for navigation 2021-12-27 23:47:25 -05:00
IrosTheBeggar
a27011c2bb layout 2021-12-27 18:57:15 -05:00
IrosTheBeggar
426807e0ab playback controls 2021-12-27 18:16:40 -05:00
IrosTheBeggar
9b1d13c01d layout buttons 2021-12-24 13:47:24 -05:00
IrosTheBeggar
580565ae1c layout options 2021-12-23 14:11:34 -05:00
IrosTheBeggar
8515570709 style cleanup 2021-12-23 12:49:51 -05:00
Paul
81fb44d9f8
Merge pull request #349 from IrosTheBeggar/alpha-edit
Alpha edit
2021-12-23 02:00:17 -05:00
IrosTheBeggar
a088235e39 scan status 2021-12-23 01:58:17 -05:00
IrosTheBeggar
8983283519 responsive design 2021-12-23 01:41:17 -05:00
IrosTheBeggar
81a4f875ff upload 2021-12-22 00:15:54 -05:00
IrosTheBeggar
0898957061 ui updates 2021-12-21 20:15:34 -05:00
IrosTheBeggar
b69c902911 working desktop app 2021-12-20 08:43:43 -05:00
IrosTheBeggar
309993f10c search function 2021-12-14 13:50:09 -05:00
IrosTheBeggar
f12854c32d stuff 2021-12-13 09:08:16 -05:00
IrosTheBeggar
34d9ed4cbe better handling of win drives 2021-12-12 16:00:13 -05:00
IrosTheBeggar
b31556c163 local search 2021-12-11 12:56:22 -05:00
IrosTheBeggar
b17487fb40 fix broken scroll 2021-12-08 17:34:59 -05:00
IrosTheBeggar
a0347979e9 styling updates 2021-12-08 09:13:15 -05:00
IrosTheBeggar
d8d8e036ab transcode update 2021-12-07 18:43:19 -05:00
IrosTheBeggar
9676b65b22 features 2021-12-06 13:17:19 -05:00
IrosTheBeggar
6c7182d0b2 stuff 2021-12-05 19:09:57 -05:00
IrosTheBeggar
653bd7735e new webapp 2021-12-05 01:48:05 -05:00
IrosTheBeggar
07cbb19c43 replay gain 2021-12-04 16:12:27 -05:00
IrosTheBeggar
05f4a8d66b more ui stuff 2021-12-04 14:55:05 -05:00
IrosTheBeggar
a1f5f972a7 web player 2021-12-04 07:52:08 -05:00
IrosTheBeggar
e8e2b306b6 alpha player 2021-12-02 16:46:21 -05:00
IrosTheBeggar
800a915455 fix bug caused by script defer 2021-12-02 13:43:38 -05:00
IrosTheBeggar
b4a4e54405 remove axios from shared page 2021-12-02 13:21:55 -05:00
IrosTheBeggar
f3edf033c3 new webapp stuff 2021-12-01 19:04:38 -05:00
IrosTheBeggar
b2b48f0cdf alpha edit 2021-11-30 08:32:41 -05:00
IrosTheBeggar
9c128711f4 alpha design 2021-11-30 07:57:39 -05:00
IrosTheBeggar
5aaa673db1 cleanup alpha 2021-11-29 14:53:56 -05:00
IrosTheBeggar
f13e208d74 remove jquery 2021-11-27 13:53:08 -05:00
IrosTheBeggar
28b9042d2d new modal plugin 2021-11-27 02:35:03 -05:00
IrosTheBeggar
4c8a73a4c9 added hyst modal 2021-11-26 14:00:40 -05:00
IrosTheBeggar
18464de107 forgot a line 2021-11-24 00:30:30 -05:00
IrosTheBeggar
34f8071bc8 readme 2021-11-24 00:29:17 -05:00
IrosTheBeggar
e9acfa4fb9 fix typo 2021-11-24 00:08:00 -05:00
IrosTheBeggar
cef579fdd7 v5.8.1 2021-11-23 00:32:46 -05:00
IrosTheBeggar
d6b940bbdc add m3u to dir 2021-11-23 00:31:37 -05:00
IrosTheBeggar
4f8a3d3442 m3u fix 2021-11-23 00:20:24 -05:00
IrosTheBeggar
28d858fa58 try again 2021-11-22 14:56:49 -05:00
IrosTheBeggar
298eff0ff1 fix download again 2021-11-22 14:51:49 -05:00
IrosTheBeggar
02cc287e64 fix directory download bbug 2021-11-22 13:23:18 -05:00
IrosTheBeggar
db962d0347 fix m3u bug 2021-11-21 19:39:30 -05:00
IrosTheBeggar
ae0cb0181f v5.8.0 2021-11-21 18:38:34 -05:00
IrosTheBeggar
7b811a1386 another scroll bug 2021-11-21 18:36:49 -05:00
IrosTheBeggar
0ed62f8983 fix scroll bug 2021-11-21 18:30:02 -05:00
IrosTheBeggar
de42cd89ba Merge branch 'master' of github.com:IrosTheBeggar/mStream into master 2021-11-18 15:11:07 -05:00
IrosTheBeggar
1e7b3e5292 better async error handling 2021-11-18 15:10:56 -05:00
Paul
0832a19e49
Merge pull request #346 from ericpaulbishop/add-directory-scan-fix
Fix another add-directory error
2021-11-13 21:26:02 -05:00
Eric
60279bbdf0 Fix error that prevents directory from being properly scanned when directory is first added 2021-11-13 16:39:26 -05:00
Paul
b40f2db946
Merge pull request #345 from tannineo/master
fix admin add folder error
2021-11-13 09:11:30 -05:00
Chao Chen
bbe458f367 fix admin add folder error 2021-11-12 19:09:43 +00:00
IrosTheBeggar
6a08cfb73b imrpove db stats api 2021-11-10 23:59:09 -05:00
IrosTheBeggar
1b58946e58 more error handling improvements 2021-11-10 23:40:31 -05:00
IrosTheBeggar
0ad01a09c1 download api 2021-11-10 23:17:21 -05:00
IrosTheBeggar
ca2c984932 admin api update 2021-11-10 23:13:43 -05:00
IrosTheBeggar
34993cd182 more api updates 2021-11-10 23:10:56 -05:00
IrosTheBeggar
bb6e13fdb7 more apis 2021-11-10 22:53:02 -05:00
IrosTheBeggar
074a46e9a8 update playlist api 2021-11-10 21:33:58 -05:00
IrosTheBeggar
2436db83ad update shared 2021-11-10 21:29:34 -05:00
IrosTheBeggar
35d60fcac3 cleanup auth middleware 2021-11-10 21:16:20 -05:00
IrosTheBeggar
7a31305d7c new error class 2021-11-10 20:47:29 -05:00
IrosTheBeggar
3a24fddcb1 remove body parser 2021-11-10 20:17:36 -05:00
IrosTheBeggar
21654b1216 new error handling pattern 2021-11-08 10:03:40 -05:00
IrosTheBeggar
2c7993be46 updated docs 2021-10-12 16:10:56 -04:00
IrosTheBeggar
96a284c857 clickable metadats 2021-10-06 22:19:07 -04:00
IrosTheBeggar
6282d5d2ac another relative url fix 2021-10-06 12:51:24 -04:00
IrosTheBeggar
fc68493958 v5.7.2 2021-10-04 16:40:59 -04:00
IrosTheBeggar
18202c30f6 more bugfixes 2021-10-04 16:40:37 -04:00
IrosTheBeggar
59608bd989 v5.7.1 2021-10-04 16:23:08 -04:00
IrosTheBeggar
79fa20f2d8 fix relative url bug 2021-10-04 16:22:47 -04:00
IrosTheBeggar
4cf737715c v5.7.0 2021-10-03 01:46:06 -04:00
IrosTheBeggar
9888d7a729 transcoding 2021-10-03 01:45:39 -04:00
IrosTheBeggar
a4a48c668b better transcoding 2021-10-02 03:09:18 -04:00
IrosTheBeggar
b7a4a3c522 v5.6.2 2021-10-01 03:05:48 -04:00
IrosTheBeggar
51fac59d28 v5.6.1 2021-10-01 02:54:54 -04:00
IrosTheBeggar
5c294ea325 clened up explorer ui 2021-10-01 02:53:53 -04:00
IrosTheBeggar
d069e3b0e6 bugfix 2021-10-01 01:05:52 -04:00
IrosTheBeggar
f94477b9b5 use year in album songs api 2021-10-01 00:34:26 -04:00
IrosTheBeggar
70999f4ef1 add year to album apis 2021-10-01 00:07:48 -04:00
IrosTheBeggar
857c8ea4f3 better navigation 2021-09-30 21:24:39 -04:00
IrosTheBeggar
84af6e2350 create playlist button 2021-09-29 21:11:12 -04:00
IrosTheBeggar
d003f939c0 relative URLs 2021-09-29 14:42:48 -04:00
IrosTheBeggar
fa93162f56 v5.5.0 2021-09-29 01:06:08 -04:00
IrosTheBeggar
b14a2b09d5 ssl managment UI 2021-09-29 01:05:26 -04:00
IrosTheBeggar
786fe37abe fix jukebox bug 2021-09-28 19:49:22 -04:00
IrosTheBeggar
e252323c4f remove SSL 2021-09-28 19:10:55 -04:00
IrosTheBeggar
e3dcdd34c1 v5.4.5 2021-09-28 13:26:32 -04:00
IrosTheBeggar
295516600d v5.4.4 2021-09-28 09:30:31 -04:00
IrosTheBeggar
0040d41b82 v5.4.3 2021-09-28 00:47:09 -04:00
IrosTheBeggar
decd2f40f1 store user settings 2021-09-28 00:32:36 -04:00
IrosTheBeggar
049252c58e get version api 2021-09-27 23:32:56 -04:00
IrosTheBeggar
5afa97c5a4 v5.4.2 2021-09-27 17:25:36 -04:00
IrosTheBeggar
7ed6a18fc5 more play now controls 2021-09-27 17:25:11 -04:00
IrosTheBeggar
93eca0a283 v5.4.1 2021-09-27 16:58:30 -04:00
IrosTheBeggar
142486968d play now 2021-09-27 16:57:40 -04:00
IrosTheBeggar
ec7ee22179 v5.4.0 2021-09-27 00:06:03 -04:00
IrosTheBeggar
9e4c964e1e add files to playlist 2021-09-26 23:45:08 -04:00
IrosTheBeggar
75846b3eff cleanup js 2021-09-26 18:53:28 -04:00
IrosTheBeggar
38f4521272 v5.3.0 2021-09-24 23:59:05 -04:00
IrosTheBeggar
80f11c213d fix broken click events 2021-09-12 02:55:30 -04:00
IrosTheBeggar
536bb95bd9 bugfix 2021-08-31 18:23:29 -04:00
IrosTheBeggar
1880fc85c0 null playlist entry 2021-08-31 18:20:28 -04:00
IrosTheBeggar
3321a6b142 jquery removal + plugin upgrades 2021-08-31 17:46:37 -04:00
IrosTheBeggar
da334171fd jquery removal 2021-08-30 23:47:57 -04:00
IrosTheBeggar
474f304a79 jquery removal 2021-08-30 23:32:44 -04:00
IrosTheBeggar
fb02eca7d7 jquery removal 2021-08-30 23:05:39 -04:00
IrosTheBeggar
3a1b5676f0 more jquery removal 2021-08-30 01:29:33 -04:00
IrosTheBeggar
b7074a1ff6 fix rate song bug 2021-08-30 00:37:41 -04:00
IrosTheBeggar
fd8f3a0f37 jquery removal + download bugfix 2021-08-30 00:31:29 -04:00
IrosTheBeggar
dcf1c00e13 more jquery removal 2021-08-29 23:36:30 -04:00
IrosTheBeggar
7c7d2180d1 jquery removal 2021-08-29 22:29:07 -04:00
IrosTheBeggar
1e2043c196 update playlist APIs 2021-08-29 16:50:01 -04:00
IrosTheBeggar
e534a245d9 delete playlist 2021-08-29 15:27:41 -04:00
IrosTheBeggar
d4286485e5 more jwuery removal 2021-08-29 12:39:50 -04:00
IrosTheBeggar
c3111f5095 more jquery removal 2021-08-29 01:28:48 -04:00
IrosTheBeggar
8d5e8427ff remove jquery, fix bug 2021-08-29 00:24:34 -04:00
IrosTheBeggar
fb0d94db8c more jquery removal 2021-08-28 19:26:05 -04:00
IrosTheBeggar
8b0479861b jquery removal 2021-08-26 11:37:25 -04:00
IrosTheBeggar
01fe83d678 more jquery removal 2021-08-26 01:10:36 -04:00
IrosTheBeggar
0984bcab01 more jquery removal 2021-08-26 00:22:56 -04:00
IrosTheBeggar
5b1d14afa5 removing jquery 2021-08-25 23:26:32 -04:00
IrosTheBeggar
93f2a43756 start jquery removal 2021-08-25 21:58:30 -04:00
99 changed files with 17866 additions and 19274 deletions

36
.github/workflows/build-webapp.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Build Webapp
on:
push:
# Pattern matched against refs/tags
tags:
- '*'
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v4
with:
node-version: 14
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1.6.0
with:
package_root: webapp/
# GitHub token, automatically provided to the action
# (No need to define this secret in the repo settings)
github_token: ${{ secrets.github_token }}
# If the commit is tagged with a version (e.g. "v1.0.0"),
# release the app after building
release: ${{ startsWith(github.ref, 'refs/tags/v') }}

View File

@ -1,6 +1,10 @@
name: build-electron
on: push
on:
push:
tags:
- '*'
jobs:
release:
runs-on: ${{ matrix.os }}
@ -11,12 +15,12 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: 14
node-version: 20
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1.6.0

View File

@ -8,43 +8,15 @@ on:
types: [published]
jobs:
# build:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v2
# - uses: actions/setup-node@v2
# with:
# node-version: 12
# - run: npm ci
# - run: npm test
publish-npm:
# needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 12
node-version: 20
registry-url: https://registry.npmjs.org/
# - run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
# publish-gpr:
# # needs: build
# runs-on: ubuntu-latest
# permissions:
# contents: read
# packages: write
# steps:
# - uses: actions/checkout@v2
# - uses: actions/setup-node@v2
# with:
# node-version: 12
# registry-url: https://npm.pkg.github.com/
# # - run: npm ci
# - run: npm publish
# env:
# NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}

View File

@ -10,21 +10,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: get node
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: '14'
- name: checkout 1
uses: actions/checkout@v2
node-version: '20'
- name: checkout mstream.io source code
uses: actions/checkout@v4
with:
path: website
repository: IrosTheBeggar/mstream-website
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: checkout 2
uses: actions/checkout@v2
token: ${{ secrets.github_token }}
ref: master
- name: checkout mStream app code
uses: actions/checkout@v4
with:
path: mstream
repository: IrosTheBeggar/mStream
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
token: ${{ secrets.github_token }}
ref: master
- run: ls
- run: |
cd website

2
.gitignore vendored
View File

@ -1,4 +1,5 @@
node_modules/*
webapp/node_modules/*
image-cache/*
!image-cache/README.md
@ -20,7 +21,6 @@ bin/ffmpeg/*
save/*.json
package-lock.json
.DS_Store
.vscode/*

View File

@ -8,7 +8,7 @@ Main|Shared|Admin
## Demo & Other Links
### [Check Out The Demo!](https://demo.mstream.io/)
#### [Check Out The Demo!](https://demo.mstream.io/)
#### [Discord Channel](https://discord.gg/AM896Rr)
@ -28,35 +28,33 @@ Main|Shared|Admin
## Installing mStream
* [Install From Source](docs/install.md)
* [Docker Instructions](https://github.com/linuxserver/docker-mstream)
* [Binaries for Win/OSX/Linux](https://mstream.io/server) - mStream binaries are compiled with Electron and have some extra features
- Runs in background and starts mStream on boot
- Automatic updates
- Adds a tray icon to manage mStream
* [Binaries for Win/OSX/Linux](https://mstream.io/server)
* [Install From Source](docs/install.md)
* [AWS Cloud using Terraform](https://gitlab.com/SiliconTao-Systems/nova)
## Mobile Apps
[<img src="/webapp/assets/img/app-store-logo.png" alt="mStream iOS App" width="200" />](https://apps.apple.com/us/app/mstream-player/id1605378892)
[<img src="/webapp/assets/img/play-store-logo.png" alt="mStream Android App" width="200" />](https://play.google.com/store/apps/details?id=com.nieratechinc.mstreamplayer&hl=en_US)
[Made by Niera Tech](https://mplayer.nieratech.com/)
## Quick Install from CLI
Deploying an mStream server is simple.
```shell
# Install From Git
git clone https://github.com/IrosTheBeggar/mStream.git
cd mStream
npm install --production
# Boot mStream
node cli-boot-wrapper.js
# Install dependencies and run
npm run-script wizard
```
## Android App
**The old Android App will not work with v5!**
There's a new Android App being developed. It's not on Google Play yet, bu you can download an early release here:
https://github.com/IrosTheBeggar/mstream_music/releases
## Technical Details
* **Dependencies:** NodeJS v10 or greater

View File

@ -1 +0,0 @@
Stores binaries for connecting with mStreams Reverse Proxy Network. Binaries are unused if they are not enabled in configuration. You can safely remove these at any time

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

116
build/index.html Normal file
View File

@ -0,0 +1,116 @@
<!doctype html>
<html lang="en">
<head>
<link href="../webapp/assets/css/materialize.css" rel="stylesheet">
<script src="../webapp/assets/js/lib/materialize.js"></script>
<style>
body {
background-color: #1e2228;
}
/* label color */
input{
color: #FFF;
}
.card {
background-color: #2d333b ;
}
.card-override {
padding-top: 10px;
padding-left: 10px;
margin-bottom: 0;
}
</style>
</head>
<body class="white-text">
<div class="container">
<div class="row">
<div class="col s12">
<div class="row">
<div class="col s12 m6">
<form>
<div class="row">
<h5>
Admin User
</h5>
<div class="input-field col s6">
<input id="username" type="text">
<label for="first_name">Username</label>
</div>
<div class="input-field col s6">
<input id="password" type="password">
<label for="password">Password</label>
</div>
</div>
<div class="row">
<h5>
Port
</h5>
<div class="input-field col s12 m6">
<input class="col" id="port" type="number" value="3000">
</div>
</div>
<div class="row">
<div class="col12">
<label>
<input type="checkbox" checked="checked" />
<span><b>Start Server On Boot</b></span>
</label>
</div>
</div>
<div class="row">
<button class="btn waves-effect waves-light col s12" type="submit" name="action" id="boot-server-button">
Boot Server
</button>
</div>
</form>
</div>
<div class="col s12 m6">
<div class="card">
<h5 class="card-override">Folders</h5>
<div class="card-content">
<a class="waves-effect waves-light btn blue">Add Folder</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const dialog = require('electron').remote.dialog;
const {ipcRenderer } = require('electron');
document.getElementById("boot-server-button").onclick = () => {
ipcRenderer.send('start-server', {});
}
document.getElementById("add-folder").onclick = () => {
dialog.showOpenDialog({properties: [ 'openDirectory']}, (selectedDirectory) => {
if(selectedDirectory.length === 0 ){
return;
}
// loadJson.folders[key] = { root: selectedDirectory[0] };
// fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
// this.resetFlag = true;
// this.instances[0].close(index);
});
}
</script>
</body>

View File

@ -4,27 +4,29 @@
// Check if we are in an electron environment
if (process.versions["electron"]) {
// off to a separate electron boot environment
return require("./build/electron");
require("./build/electron");
} else {
const version = require('./package.json').version;
const { Command } = require('commander');
const program = new Command();
program
.version(version)
.option('-j, --json <json>', 'Specify JSON Boot File', require('path').join(__dirname, 'save/conf/default.json'))
.parse(process.argv);
console.clear();
console.log(`
____ _
_ __ ___ / ___|| |_ _ __ ___ __ _ _ __ ___
| '_ \` _ \\\\___ \\| __| '__/ _ \\/ _\` | '_ \` _ \\
| | | | | |___) | |_| | | __/ (_| | | | | | |
|_| |_| |_|____/ \\__|_| \\___|\\__,_|_| |_| |_|`);
console.log(`v${program.version()}`);
console.log();
console.log('Check out our Discord server:');
console.log('https://discord.gg/AM896Rr');
console.log();
// Boot the server
require("./src/server").serveIt(program.opts().json);
}
const program = require('commander');
program
.version(require('./package.json').version)
.option('-j, --json <json>', 'Specify JSON Boot File', require('path').join(__dirname, 'save/conf/default.json'))
.parse(process.argv);
console.clear();
console.log(`
____ _
_ __ ___ / ___|| |_ _ __ ___ __ _ _ __ ___
| '_ \` _ \\\\___ \\| __| '__/ _ \\/ _\` | '_ \` _ \\
| | | | | |___) | |_| | | __/ (_| | | | | | |
|_| |_| |_|____/ \\__|_| \\___|\\__,_|_| |_| |_|`);
console.log(`v${program.version()}`);
console.log();
console.log('Check out our Discord server:');
console.log('https://discord.gg/AM896Rr');
console.log();
// Boot the server
require("./src/server").serveIt(program.json);

View File

@ -14,8 +14,8 @@ git clone https://github.com/IrosTheBeggar/mStream.git
cd mStream
# Install without dev dependencies
npm install --production
# Install dependencies and run
npm run-script wizard
```
# Running mStream as a Background Process
@ -38,7 +38,7 @@ To update mStream just pull the changes from git and reboot your server
```shell
git pull
npm install --production
npm install --only=prod
# Reboot mStream with PM2
pm2 restart all
```

View File

@ -257,4 +257,19 @@ set `writeLogs` to `true` to enable writing logs to the filesystem
## UI
Folder that contains the frontend for mStream. Defaults to `public` if not set
Folder that contains the frontend for mStream. Defaults to `public` if not set
## Supported Files
```json
{
"supportedAudioFiles": {
"mp3": true,
"m3u": false,
}
}
```
The object key is the file extension and the value is true/false.
If true, the file will be scanned and saved the db as an audio file. If false, the file will not be scanned but still be viewable in the file explorer

8107
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,19 @@
{
"name": "mstream",
"version": "5.2.9",
"version": "5.13.1",
"description": "music streaming server",
"main": "cli-boot-wrapper.js",
"bin": {
"mstream": "cli-boot-wrapper.js"
},
"engines": {
"node": ">=8.0.0"
"node": ">=10.16.0"
},
"scripts": {
"start": "node cli-boot-wrapper.js",
"pack": "electron-builder --dir",
"dist": "electron-builder"
"dist": "electron-builder",
"wizard": "npm install --only=prod && node cli-boot-wrapper.js"
},
"repository": {
"type": "git",
@ -27,7 +28,7 @@
"build": {
"appId": "io.mstream.server",
"productName": "mStream Server",
"electronVersion": "12.0.0",
"electronVersion": "29.1.4",
"asar": false,
"files": [
"**/*",
@ -42,7 +43,6 @@
],
"mac": {
"files": [
"bin/rpn/rpn-osx",
"bin/syncthing/syncthing-osx"
],
"category": "public.app-category.music",
@ -53,22 +53,19 @@
},
"win": {
"files": [
"bin/rpn/rpn-win.exe",
"bin/syncthing/syncthing.exe"
],
"target": [
{
"target": "nsis",
"arch": [
"x64",
"ia32"
"x64"
]
}
]
},
"linux": {
"files": [
"bin/rpn/rpn-linux",
"bin/syncthng/syncthing-linux"
],
"target": [
@ -89,33 +86,35 @@
}
},
"dependencies": {
"archiver": "^5.3.0",
"axios": "^0.21.1",
"body-parser": "^1.19.0",
"busboy": "^0.3.1",
"commander": "^6.2.1",
"cookie-parser": "^1.4.5",
"electron-updater": "^4.3.9",
"archiver": "^7.0.1",
"axios": "^1.7.9",
"busboy": "^1.6.0",
"commander": "^12.1.0",
"cookie-parser": "^1.4.7",
"electron-updater": "^6.3.9",
"escape-string-regexp": "^4.0.0",
"express": "^4.17.1",
"fast-xml-parser": "^3.19.0",
"ffbinaries": "^1.1.4",
"fluent-ffmpeg": "^2.1.2",
"express": "^4.21.2",
"fast-xml-parser": "^4.5.1",
"ffbinaries": "^1.1.6",
"fluent-ffmpeg": "^2.1.3",
"http-proxy": "^1.18.1",
"joi": "^17.4.0",
"jsonwebtoken": "^8.5.1",
"jimp": "^0.22.12",
"jimpv1": "npm:jimp@^1.6.0",
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.2",
"lokijs": "^1.5.12",
"m3u8-parser": "^4.7.0",
"m3u8-parser": "^7.2.0",
"make-dir": "^3.1.0",
"mime-types": "^2.1.31",
"music-metadata": "^7.8.5",
"nanoid": "^3.1.23",
"mime-types": "^2.1.35",
"mm-v10": "npm:music-metadata@^10.6.4",
"music-metadata": "^7.14.0",
"nanoid": "^3.3.6",
"tree-kill": "^1.2.2",
"winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.5",
"ws": "^7.4.6"
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0",
"ws": "^8.18.0"
},
"devDependencies": {
"electron-builder": "22.10.5"
"electron-builder": "25.1.8"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ const winston = require('winston');
const auth = require('../util/auth');
const config = require('../state/config');
const shared = require('../api/shared');
const WebError = require('../util/web-error');
exports.setup = (mstream) => {
mstream.post('/api/v1/auth/login', async (req, res) => {
@ -14,79 +15,82 @@ exports.setup = (mstream) => {
});
await schema.validateAsync(req.body);
if (!config.program.users[req.body.username]) { throw 'user not found'; }
if (!config.program.users[req.body.username]) { throw new Error('user not found'); }
await auth.authenticateUser(config.program.users[req.body.username].password, config.program.users[req.body.username].salt, req.body.password)
const token = jwt.sign({ username: req.body.username }, config.program.secret);
res.cookie('x-access-token', token, {
maxAge: 157784630000, // 5 years in ms
sameSite: 'Strict',
});
res.json({
vpaths: config.program.users[req.body.username].vpaths,
token: jwt.sign({ username: req.body.username }, config.program.secret)
token: token
});
}catch (err) {
winston.warn(`Failed login attempt from ${req.ip}. Username: ${req.body.username}`);
} catch (err) {
winston.warn(`Failed login attempt from ${req.ip}. Username: ${req.body.username}`, { stack: err });
setTimeout(() => { res.status(401).json({ error: 'Login Failed' }); }, 800);
}
});
mstream.use((req, res, next) => {
try {
// Handle No Users
if (Object.keys(config.program.users).length === 0
&& !req.path.startsWith('/api/v1/scanner/')
) {
req.user = {
vpaths: Object.keys(config.program.folders),
username: 'mstream-user',
admin: true
};
// Handle No Users
if (Object.keys(config.program.users).length === 0
&& !req.path.startsWith('/api/v1/scanner/')
) {
req.user = {
vpaths: Object.keys(config.program.folders),
username: 'mstream-user',
admin: true
};
return next();
}
const token = req.body.token || req.query.token || req.headers['x-access-token'] || req.cookies['x-access-token'];
if (!token) { throw 'Token Not Found'; }
req.token = token;
const decoded = jwt.verify(token, config.program.secret);
if (decoded.scan === true && req.path.startsWith('/api/v1/scanner/')) {
req.scanApproved = true;
return next();
}
// handle federation invite tokens
if (decoded.invite && decoded.invite === true) {
// Invite tokens can only be used with one API path
if (req.path === '/federation/invite/exchange') { return next(); }
throw 'Invalid Invite Token';
}
if (!decoded.username || !config.program.users[decoded.username]) {
throw 'Invalid Auth Token';
}
req.user = config.program.users[decoded.username];
req.user.username = decoded.username;
// Handle Shared Tokens
if (decoded.shareToken && decoded.shareToken === true) {
const playlistItem = shared.lookupPlaylist(decoded.playlistId);
if (
req.path !== '/api/v1/download/shared' &&
req.path !== '/api/v1/db/metadata' &&
req.path.substring(0,11) !== '/album-art/' &&
playlistItem.playlist.indexOf(decodeURIComponent(req.path).slice(7)) === -1
) {
throw 'Invalid Share Token';
}
req.sharedPlaylistId = decoded.playlistId;
}
next();
} catch (err) {
return res.status(403).json({ error: 'Access Denied' });
return next();
}
const token = req.body.token || req.query.token || req.headers['x-access-token'] || req.cookies['x-access-token'];
if (!token) { throw new WebError('Authentication Error', 401); }
req.token = token;
const decoded = jwt.verify(token, config.program.secret);
if (decoded.scan === true && req.path.startsWith('/api/v1/scanner/')) {
req.scanApproved = true;
return next();
}
// handle federation invite tokens
if (decoded.invite && decoded.invite === true) {
// Invite tokens can only be used with one API path
if (req.path === '/federation/invite/exchange') { return next(); }
throw new WebError('Authentication Error', 401);
}
if (!decoded.username || !config.program.users[decoded.username]) {
throw new WebError('Authentication Error', 401);
}
req.user = config.program.users[decoded.username];
req.user.username = decoded.username;
// Handle Shared Tokens
if (decoded.shareToken && decoded.shareToken === true) {
const playlistItem = shared.lookupPlaylist(decoded.playlistId);
if (
req.path !== '/api/v1/download/shared' &&
req.path !== '/api/v1/db/metadata' &&
req.path.substring(0,11) !== '/album-art/' &&
playlistItem.playlist.indexOf(decodeURIComponent(req.path).slice(7)) === -1
) {
throw new WebError('Authentication Error', 401);
}
req.sharedPlaylistId = decoded.playlistId;
}
next();
});
}

View File

@ -1,21 +1,11 @@
const winston = require('winston');
const Joi = require('joi');
const path = require('path');
const escapeStringRegexp = require('escape-string-regexp');
const vpath = require('../util/vpath');
const dbQueue = require('../db/task-queue');
const db = require('../db/manager');
getNumberOfFiles = (vpaths) => {
if (!db.getFileCollection()) { return 0; }
let total = 0;
for (const vpath of vpaths) {
total += db.getFileCollection().count({ 'vpath': vpath })
}
return total;
}
const { joiValidate } = require('../util/validation');
const WebError = require('../util/web-error');
const mapFunDefault = (left, right) => {
return {
@ -45,22 +35,28 @@ function renderMetadataObj(row) {
"hash": row.hash ? row.hash : null,
"album": row.album ? row.album : null,
"track": row.track ? row.track : null,
"disk": row.disk ? row.disk : null,
"title": row.title ? row.title : null,
"year": row.year ? row.year : null,
"album-art": row.aaFile ? row.aaFile : null,
"rating": row.rating ? row.rating : null,
"play-count": row.playCount ? row.playCount : null,
"last-played": row.lastPlayed ? row.lastPlayed : null,
"replaygain-track": row.replaygainTrack ? row.replaygainTrack : null
}
};
}
function renderOrClause(vpaths) {
function renderOrClause(vpaths, ignoreVPaths) {
if (vpaths.length === 1) {
return { 'vpath': { '$eq': vpaths[0] } };
}
const returnThis = { '$or': [] }
for (let vpath of vpaths) {
if (ignoreVPaths && typeof ignoreVPaths === 'object' && ignoreVPaths.includes(vpath)) {
continue;
}
returnThis['$or'].push({ 'vpath': { '$eq': vpath } })
}
@ -69,178 +65,196 @@ function renderOrClause(vpaths) {
exports.setup = (mstream) => {
mstream.get('/api/v1/db/status', (req, res) => {
try {
res.json({
totalFileCount: getNumberOfFiles(req.user.vpaths),
locked: dbQueue.isScanning()
});
}catch(err) {
winston.error('Db Error', { stack: err });
res.status(500).json({});
let total = 0;
if (db.getFileCollection()) {
for (const vpath of req.user.vpaths) {
total += db.getFileCollection().count({ 'vpath': vpath })
}
}
res.json({
totalFileCount: total,
locked: dbQueue.isScanning()
});
});
mstream.post('/api/v1/db/metadata', (req, res) => {
try {
const pathInfo = vpath.getVPathInfo(req.body.filepath, req.user);
if (!pathInfo) { throw 'File Not Found' }
if (!db.getFileCollection()) { return res.json({ "filepath": req.body.filepath, "metadata": {} }); }
const leftFun = (leftData) => {
return leftData.hash + '-' + req.user.username;
};
const result = db.getFileCollection().chain().find({ '$and': [{'filepath': pathInfo.relativePath}, {'vpath': pathInfo.vpath}] }, true)
.eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data();
if (!result || !result[0]) {
return res.json({ "filepath": req.body.filepath, "metadata": {} });
}
res.json(renderMetadataObj(result[0]));
} catch (err) {
winston.error('Db Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
res.json(this.pullMetaData(req.body.filepath, req.user));
});
mstream.post('/api/v1/db/metadata/batch', (req, res) => {
const returnThis = {};
req.body.forEach(f => {
console.log(f)
returnThis[f] = this.pullMetaData(f, req.user);
});
res.json(returnThis);
});
exports.pullMetaData = (filepath, user) => {
const pathInfo = vpath.getVPathInfo(filepath, user);
if (!db.getFileCollection()) { return { "filepath": filepath, "metadata": null }; }
const leftFun = (leftData) => {
return leftData.hash + '-' + user.username;
};
const result = db.getFileCollection().chain().find({ '$and': [{'filepath': pathInfo.relativePath}, {'vpath': pathInfo.vpath}] }, true)
.eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data();
if (!result || !result[0]) {
return { "filepath": filepath, "metadata": null };
}
return renderMetadataObj(result[0]);
}
// legacy enpoint, moved to POST
mstream.get('/api/v1/db/artists', (req, res) => {
try {
const artists = { "artists": [] };
if (!db.getFileCollection()) { res.json(artists); }
const results = db.getFileCollection().find(renderOrClause(req.user.vpaths));
const store = {};
for (let row of results) {
if (!store[row.artist] && !(row.artist === undefined || row.artist === null)) {
store[row.artist] = true;
}
}
artists.artists = Object.keys(store).sort((a, b) => {
return a.localeCompare(b);
});
res.json(artists);
} catch (err) {
winston.error('Db Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
const artists = getArtists(req);
res.json(artists);
});
mstream.post('/api/v1/db/artists', (req, res) => {
const artists = getArtists(req);
res.json(artists);
});
function getArtists(req) {
const artists = { "artists": [] };
if (!db.getFileCollection()) { res.json(artists); }
const results = db.getFileCollection().find(renderOrClause(req.user.vpaths, req.body.ignoreVPaths));
const store = {};
for (let row of results) {
if (!store[row.artist] && !(row.artist === undefined || row.artist === null)) {
store[row.artist] = true;
}
}
artists.artists = Object.keys(store).sort((a, b) => {
return a.localeCompare(b);
});
return artists;
}
mstream.post('/api/v1/db/artists-albums', (req, res) => {
try {
const albums = { "albums": [] };
if (!db.getFileCollection()) { return res.json(albums); }
const albums = { "albums": [] };
if (!db.getFileCollection()) { return res.json(albums); }
const results = db.getFileCollection().chain().find({
'$and': [
renderOrClause(req.user.vpaths),
{'artist': { '$eq': String(req.body.artist) }}
]
}).simplesort('year', true).data();
const results = db.getFileCollection().chain().find({
'$and': [
renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
{'artist': { '$eq': String(req.body.artist) }}
]
}).simplesort('year', true).data();
const store = {};
for (let row of results) {
const store = {};
for (let row of results) {
if (row.album === null) {
if (!store[row.album]) {
albums.albums.push({
name: row.album,
name: null,
year: null,
album_art_file: row.aaFile ? row.aaFile : null
});
store[row.album] = true;
}
} else if (!store[`${row.album}${row.year}`]) {
albums.albums.push({
name: row.album,
year: row.year,
album_art_file: row.aaFile ? row.aaFile : null
});
store[`${row.album}${row.year}`] = true;
}
res.json(albums);
} catch (err) {
winston.error('Db Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
res.json(albums);
});
mstream.get('/api/v1/db/albums', (req, res) => {
try {
const albums = { "albums": [] };
if (!db.getFileCollection()) { return res.json(albums); }
const results = db.getFileCollection().find(renderOrClause(req.user.vpaths));
const store = {};
for (let row of results) {
if (!store[row.album] && !(row.album === undefined || row.album === null)) {
albums.albums.push({ name: row.album, album_art_file: row.aaFile });
store[row.album] = true;
}
}
albums.albums.sort((a, b) => {
return a.name.localeCompare(b.name);
});
res.json(albums);
} catch (err) {
winston.error('Db Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
const albums = getAlbums(req);
res.json(albums);
});
mstream.post('/api/v1/db/albums', (req, res) => {
const albums = getAlbums(req);
res.json(albums);
});
function getAlbums(req) {
const albums = { "albums": [] };
if (!db.getFileCollection()) { return res.json(albums); }
const results = db.getFileCollection().find(renderOrClause(req.user.vpaths, req.body.ignoreVPaths));
const store = {};
for (let row of results) {
if (store[`${row.album}${row.year}`] || (row.album === undefined || row.album === null)) {
continue;
}
albums.albums.push({ name: row.album, album_art_file: row.aaFile, year: row.year });
store[`${row.album}${row.year}`] = true;
}
albums.albums.sort((a, b) => {
return a.name.localeCompare(b.name);
});
return albums;
}
mstream.post('/api/v1/db/album-songs', (req, res) => {
try {
if (!db.getFileCollection()) { throw 'DB Not Working'; }
if (!db.getFileCollection()) { throw new Error('DB Not Working'); }
let artistClause;
if (req.body.artist) {
artistClause = {'artist': { '$eq': req.body.artist }};
}
const searchClause = [
renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
{'album': { '$eq': req.body.album ? String(req.body.album) : null }}
];
const leftFun = (leftData) => {
return leftData.hash + '-' + req.user.username;
};
const album = req.body.album ? String(req.body.album) : null;
const results = db.getFileCollection().chain().find({
'$and': [
renderOrClause(req.user.vpaths),
{'album': { '$eq': album }},
artistClause
]
}).compoundsort(['disk','track','filepath']).eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data();
const songs = [];
for (const row of results) {
songs.push(renderMetadataObj(row));
}
res.json(songs);
} catch (err) {
winston.error('Db Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
if (req.body.artist) {
searchClause.push({ 'artist': { '$eq': req.body.artist }});
}
if (req.body.year) {
searchClause.push({ 'year': { '$eq': Number(req.body.year) }});
}
const leftFun = (leftData) => {
return leftData.hash + '-' + req.user.username;
};
const results = db.getFileCollection().chain().find({
'$and': searchClause
}).compoundsort(['disk','track','filepath']).eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data();
const songs = [];
for (const row of results) {
songs.push(renderMetadataObj(row));
}
res.json(songs);
});
mstream.post('/api/v1/db/search', async (req, res) => {
try {
const schema = Joi.object({
search: Joi.string().required(),
noArtists: Joi.boolean().optional(),
noAlbums: Joi.boolean().optional(),
noTitles: Joi.boolean().optional(),
noFiles: Joi.boolean().optional(),
});
await schema.validateAsync(req.body);
}catch (err) {
return res.status(500).json({ error: 'Validation Error' });
}
mstream.post('/api/v1/db/search', (req, res) => {
const schema = Joi.object({
search: Joi.string().required(),
noArtists: Joi.boolean().optional(),
noAlbums: Joi.boolean().optional(),
noTitles: Joi.boolean().optional(),
noFiles: Joi.boolean().optional(),
ignoreVPaths: Joi.array().items(Joi.string()).optional()
});
joiValidate(schema, req.body);
try {
// Get user inputs
const artists = req.body.noArtists === true ? [] : searchByX(req, 'artist');
const albums = req.body.noAlbums === true ? [] : searchByX(req, 'album');
const files = req.body.noFiles === true ? [] : searchByX(req, 'filepath');
const title = req.body.noTitles === true ? [] : searchByX(req, 'title', 'filepath');
res.json({artists, albums, files, title });
} catch (err) {
winston.error('Db Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
// Get user inputs
const artists = req.body.noArtists === true ? [] : searchByX(req, 'artist');
const albums = req.body.noAlbums === true ? [] : searchByX(req, 'album');
const files = req.body.noFiles === true ? [] : searchByX(req, 'filepath');
const title = req.body.noTitles === true ? [] : searchByX(req, 'title', 'filepath');
res.json({artists, albums, files, title });
});
function searchByX(req, searchCol, resCol) {
@ -253,7 +267,7 @@ exports.setup = (mstream) => {
const findThis = {
'$and': [
renderOrClause(req.user.vpaths),
renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
{[searchCol]: {'$regex': [escapeStringRegexp(String(req.body.search)), 'i']}}
]
};
@ -286,237 +300,321 @@ exports.setup = (mstream) => {
}
mstream.get('/api/v1/db/rated', (req, res) => {
try {
if (!db.getFileCollection()) { throw 'DB Not Ready'; }
const mapFun = (left, right) => {
return {
artist: right.artist,
album: right.album,
hash: right.hash,
track: right.track,
title: right.title,
year: right.year,
aaFile: right.aaFile,
filepath: right.filepath,
rating: left.rating,
"replaygain-track-db": right.replaygainTrackDb,
vpath: right.vpath
};
};
const leftFun = (leftData) => {
return leftData.hash + '-' + leftData.user;
};
const rightFun = (rightData) => {
return rightData.hash + '-' + req.user.username;
};
const results = db.getUserMetadataCollection().chain().eqJoin(db.getFileCollection().chain(), leftFun, rightFun, mapFun).find({
'$and': [
renderOrClause(req.user.vpaths),
{ 'rating': { '$gt': 0 } }
]
}).simplesort('rating', true).data();
const songs = [];
for (const row of results) {
songs.push(renderMetadataObj(row));
}
res.json(songs);
} catch (err) {
winston.error('Db Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
const songs = getRatedSongs(req);
res.json(songs);
});
mstream.post('/api/v1/db/rate-song', async (req, res) => {
try {
const schema = Joi.object({
filepath: Joi.string().required(),
rating: Joi.number().integer().min(0).max(10).allow(null).required()
mstream.post('/api/v1/db/rated', (req, res) => {
const songs = getRatedSongs(req);
res.json(songs);
});
function getRatedSongs(req) {
if (!db.getFileCollection()) { throw new Error('DB Not Ready'); }
const mapFun = (left, right) => {
return {
artist: right.artist,
album: right.album,
hash: right.hash,
track: right.track,
title: right.title,
year: right.year,
aaFile: right.aaFile,
filepath: right.filepath,
rating: left.rating,
"replaygain-track-db": right.replaygainTrackDb,
vpath: right.vpath
};
};
const leftFun = (leftData) => {
return leftData.hash + '-' + leftData.user;
};
const rightFun = (rightData) => {
return rightData.hash + '-' + req.user.username;
};
const results = db.getUserMetadataCollection().chain().eqJoin(db.getFileCollection().chain(), leftFun, rightFun, mapFun).find({
'$and': [
renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
{ 'rating': { '$gt': 0 } }
]
}).simplesort('rating', true).data();
const songs = [];
for (const row of results) {
songs.push(renderMetadataObj(row));
}
return songs;
}
mstream.post('/api/v1/db/rate-song', (req, res) => {
const schema = Joi.object({
filepath: Joi.string().required(),
rating: Joi.number().integer().min(0).max(10).allow(null).required()
});
joiValidate(schema, req.body);
const pathInfo = vpath.getVPathInfo(req.body.filepath);
if (!db.getUserMetadataCollection() || !db.getFileDbName()) { throw new Error('No DB'); }
const result = db.getFileCollection().findOne({ '$and':[{ 'filepath': pathInfo.relativePath}, { 'vpath': pathInfo.vpath }] });
if (!result) { throw new Error('File Not Found'); }
const result2 = db.getUserMetadataCollection().findOne({ '$and':[{ 'hash': result.hash}, { 'user': req.user.username }] });
if (!result2) {
db.getUserMetadataCollection().insert({
user: req.user.username,
hash: result.hash,
rating: req.body.rating
});
await schema.validateAsync(req.body);
}catch (err) {
return res.status(500).json({ error: 'Validation Error' });
} else {
result2.rating = req.body.rating;
db.getUserMetadataCollection().update(result2);
}
try{
const pathInfo = vpath.getVPathInfo(req.body.filepath);
if (!pathInfo) { return res.status(500).json({ error: 'Could not find file' }); }
if (!db.getUserMetadataCollection() || !db.getFileDbName()) { throw 'No DB' }
const result = db.getFileCollection().findOne({ '$and':[{ 'filepath': pathInfo.relativePath}, { 'vpath': pathInfo.vpath }] });
if (!result) { throw 'File Not Found' }
const result2 = db.getUserMetadataCollection().findOne({ '$and':[{ 'hash': result.hash}, { 'user': req.user.username }] });
if (!result2) {
db.getUserMetadataCollection().insert({
user: req.user.username,
hash: result.hash,
rating: req.body.rating
});
} else {
result2.rating = req.body.rating;
db.getUserMetadataCollection().update(result2);
}
res.json({});
db.saveUserDB();
}catch (err) {
winston.error('Db Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
res.json({});
db.saveUserDB();
});
mstream.post('/api/v1/db/recent/added', async (req, res) => {
try {
const schema = Joi.object({ limit: Joi.number().integer().min(1).required() });
await schema.validateAsync(req.body);
}catch (err) {
return res.status(500).json({ error: 'Validation Error' });
mstream.post('/api/v1/db/recent/added', (req, res) => {
const schema = Joi.object({
limit: Joi.number().integer().min(1).required(),
ignoreVPaths: Joi.array().items(Joi.string()).optional()
});
joiValidate(schema, req.body);
if (!db.getFileCollection()) { throw new Error('DB Not Ready'); }
const leftFun = (leftData) => {
return leftData.hash + '-' + req.user.username;
};
const results = db.getFileCollection().chain().find({
'$and': [
renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
{ 'ts': { '$gt': 0 } }
]
}).simplesort('ts', true).limit(req.body.limit).eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data();
const songs = [];
for (const row of results) {
songs.push(renderMetadataObj(row));
}
try {
if (!db.getFileCollection()) { throw 'DB Not Ready'; }
res.json(songs);
});
const leftFun = (leftData) => {
return leftData.hash + '-' + req.user.username;
mstream.post('/api/v1/db/stats/recently-played', (req, res) => {
const schema = Joi.object({
limit: Joi.number().integer().min(1).required(),
ignoreVPaths: Joi.array().items(Joi.string()).optional()
});
joiValidate(schema, req.body);
if (!db.getFileCollection()) { throw new Error('DB Not Ready'); }
const mapFun = (left, right) => {
return {
artist: right.artist,
album: right.album,
hash: right.hash,
track: right.track,
title: right.title,
year: right.year,
aaFile: right.aaFile,
filepath: right.filepath,
rating: left.rating,
lastPlayed: left.lp,
playCount: left.pc,
"replaygain-track-db": right.replaygainTrackDb,
vpath: right.vpath
};
const results = db.getFileCollection().chain().find({
'$and': [
renderOrClause(req.user.vpaths),
{ 'ts': { '$gt': 0 } }
]
}).simplesort('ts', true).limit(req.body.limit).eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data();
const songs = [];
for (const row of results) {
songs.push(renderMetadataObj(row));
}
};
const leftFun = (leftData) => {
return leftData.hash + '-' + leftData.user;
};
const rightFun = (rightData) => {
return rightData.hash + '-' + req.user.username;
};
res.json(songs);
}catch (err) {
winston.error('Db Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
const results = db.getUserMetadataCollection().chain().eqJoin(db.getFileCollection().chain(), leftFun, rightFun, mapFun).find({
'$and': [
renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
{ 'lastPlayed': { '$gt': 0 } }
]
}).simplesort('lastPlayed', true).limit(req.body.limit).data();
const songs = [];
for (const row of results) {
songs.push(renderMetadataObj(row));
}
res.json(songs);
});
mstream.post('/api/v1/db/stats/most-played', (req, res) => {
const schema = Joi.object({
limit: Joi.number().integer().min(1).required(),
ignoreVPaths: Joi.array().items(Joi.string()).optional()
});
joiValidate(schema, req.body);
if (!db.getFileCollection()) { throw new Error('DB Not Ready'); }
const mapFun = (left, right) => {
return {
artist: right.artist,
album: right.album,
hash: right.hash,
track: right.track,
title: right.title,
year: right.year,
aaFile: right.aaFile,
filepath: right.filepath,
rating: left.rating,
lastPlayed: left.lp,
playCount: left.pc,
"replaygain-track-db": right.replaygainTrackDb,
vpath: right.vpath
};
};
const leftFun = (leftData) => {
return leftData.hash + '-' + leftData.user;
};
const rightFun = (rightData) => {
return rightData.hash + '-' + req.user.username;
};
const results = db.getUserMetadataCollection().chain().eqJoin(db.getFileCollection().chain(), leftFun, rightFun, mapFun).find({
'$and': [
renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
{ 'playCount': { '$gt': 0 } }
]
}).simplesort('playCount', true).limit(req.body.limit).data();
const songs = [];
for (const row of results) {
songs.push(renderMetadataObj(row));
}
res.json(songs);
});
mstream.post('/api/v1/db/random-songs', (req, res) => {
try {
if (!db.getFileDbName()) { throw 'No DB'; };
if (!db.getFileDbName()) { throw new Error('No DB'); };
// Ignore list
let ignoreList = [];
if (req.body.ignoreList && Array.isArray(req.body.ignoreList)) {
ignoreList = req.body.ignoreList;
}
let ignorePercentage = .5;
if (req.body.ignorePercentage && typeof req.body.ignorePercentage === 'number' && req.body.ignorePercentage < 1 && !req.body.ignorePercentage < 0) {
ignorePercentage = req.body.ignorePercentage;
}
let orClause = { '$or': [] };
for (let vpath of req.user.vpaths) {
if (req.body.ignoreVPaths && typeof req.body.ignoreVPaths === 'object' && req.body.ignoreVPaths[vpath] === true) {
continue;
}
orClause['$or'].push({ 'vpath': { '$eq': vpath } });
}
let minRating = Number(req.body.minRating);
// Add Rating clause
if (minRating && typeof minRating === 'number' && minRating <= 10 && !minRating < 1) {
orClause = {'$and': [
orClause,
{ 'rating': { '$gte': req.body.minRating } }
]};
}
const leftFun = (leftData) => {
return leftData.hash + '-' + req.user.username;
};
const results = db.getFileCollection().chain().eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).find(orClause).data();
const count = results.length;
if (count === 0) { throw 'No songs that match criteria'; }
while (ignoreList.length > count * ignorePercentage) {
ignoreList.shift();
}
const returnThis = { songs: [], ignoreList: [] };
let randomNumber = Math.floor(Math.random() * count);
while (ignoreList.indexOf(randomNumber) > -1) {
randomNumber = Math.floor(Math.random() * count);
}
const randomSong = results[randomNumber];
returnThis.songs.push(renderMetadataObj(randomSong));
ignoreList.push(randomNumber);
returnThis.ignoreList = ignoreList;
res.json(returnThis);
}catch (err) {
winston.error('Db Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
// Ignore list
let ignoreList = [];
if (req.body.ignoreList && Array.isArray(req.body.ignoreList)) {
ignoreList = req.body.ignoreList;
}
let ignorePercentage = .5;
if (req.body.ignorePercentage && typeof req.body.ignorePercentage === 'number' && req.body.ignorePercentage < 1 && !req.body.ignorePercentage < 0) {
ignorePercentage = req.body.ignorePercentage;
}
let orClause = { '$or': [] };
for (let vpath of req.user.vpaths) {
if (req.body.ignoreVPaths && typeof req.body.ignoreVPaths === 'object' && req.body.ignoreVPaths.includes(vpath)) {
continue;
}
orClause['$or'].push({ 'vpath': { '$eq': vpath } });
}
let minRating = Number(req.body.minRating);
// Add Rating clause
if (minRating && typeof minRating === 'number' && minRating <= 10 && !minRating < 1) {
orClause = {'$and': [
orClause,
{ 'rating': { '$gte': req.body.minRating } }
]};
}
const leftFun = (leftData) => {
return leftData.hash + '-' + req.user.username;
};
const results = db.getFileCollection().chain().eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).find(orClause).data();
const count = results.length;
if (count === 0) { throw new WebError('No songs that match criteria', 400); }
while (ignoreList.length > count * ignorePercentage) {
ignoreList.shift();
}
const returnThis = { songs: [], ignoreList: [] };
let randomNumber = Math.floor(Math.random() * count);
while (ignoreList.indexOf(randomNumber) > -1) {
randomNumber = Math.floor(Math.random() * count);
}
const randomSong = results[randomNumber];
returnThis.songs.push(renderMetadataObj(randomSong));
ignoreList.push(randomNumber);
returnThis.ignoreList = ignoreList;
res.json(returnThis);
});
mstream.post('/api/v1/playlist/load', (req, res) => {
try {
if (!db.getPlaylistCollection()){ throw 'No DB'; }
if (!db.getFileDbName()){ throw 'No DB'; }
const playlist = String(req.body.playlistname);
const returnThis = [];
const results = db.getPlaylistCollection().find({
'$and': [{
'user': { '$eq': req.user.username }
}, {
'name': { '$eq': playlist }
}]
});
for (const row of results) {
// Look up metadata
const pathInfo = vpath.getVPathInfo(row.filepath, req.user);
if (!pathInfo) { continue; }
const leftFun = (leftData) => {
return leftData.hash + '-' + req.user.username;
};
const result = db.getFileCollection().chain().find({ '$and': [{'filepath': pathInfo.relativePath}, { 'vpath': pathInfo.vpath }] }, true)
.eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data();
if (!db.getPlaylistCollection()){ throw new Error('No DB'); }
if (!db.getFileDbName()){ throw new Error('No DB'); }
let metadata = {};
if (result && result[0]) {
metadata = {
"artist": result[0].artist ? result[0].artist : null,
"hash": result[0].hash ? result[0].hash : null,
"album": result[0].album ? result[0].album : null,
"track": result[0].track ? result[0].track : null,
"title": result[0].title ? result[0].title : null,
"year": result[0].year ? result[0].year : null,
"album-art": result[0].aaFile ? result[0].aaFile : null,
"rating": result[0].rating ? result[0].rating : null,
"replaygain-track-db": result[0]['replaygain-track-db'] ? result[0]['replaygain-track-db'] : null
};
}
returnThis.push({ lokiId: row['$loki'], filepath: row.filepath, metadata: metadata });
const playlist = String(req.body.playlistname);
const returnThis = [];
const results = db.getPlaylistCollection().find({
'$and': [
{ 'user': { '$eq': req.user.username }},
{ 'name': { '$eq': playlist }},
{ 'filepath': { '$ne': null }},
]
});
const leftFun = (leftData) => {
return leftData.hash + '-' + req.user.username;
};
for (const row of results) {
// Look up metadata
try{
var pathInfo = vpath.getVPathInfo(row.filepath, req.user);
} catch(err) { continue; }
const result = db.getFileCollection().chain().find({ '$and': [{'filepath': pathInfo.relativePath}, { 'vpath': pathInfo.vpath }] }, true)
.eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data();
let metadata = {};
if (result && result[0]) {
metadata = {
"artist": result[0].artist ? result[0].artist : null,
"hash": result[0].hash ? result[0].hash : null,
"album": result[0].album ? result[0].album : null,
"track": result[0].track ? result[0].track : null,
"title": result[0].title ? result[0].title : null,
"year": result[0].year ? result[0].year : null,
"album-art": result[0].aaFile ? result[0].aaFile : null,
"rating": result[0].rating ? result[0].rating : null,
"replaygain-track-db": result[0]['replaygain-track-db'] ? result[0]['replaygain-track-db'] : null
};
}
res.json(returnThis);
}catch (err) {
winston.error('Db Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
returnThis.push({ lokiId: row['$loki'], filepath: row.filepath, metadata: metadata });
}
res.json(returnThis);
});
// mstream.post('/api/v1/db/song-position', (req, res) => {
// });
}

View File

@ -5,81 +5,78 @@ const winston = require('winston');
const vpath = require('../util/vpath');
const shared = require('../api/shared');
const m3u = require('../util/m3u');
const WebError = require('../util/web-error');
exports.setup = (mstream) => {
mstream.post('/api/v1/download/m3u', async (req, res) => {
try {
if (!req.body.path) { throw 'Validation Error' }
const pathInfo = vpath.getVPathInfo(req.body.path, req.user);
if (!playlistPathInfo) { throw 'vpath lookup failed'; }
const playlistParentDir = path.dirname(playlistPathInfo.fullPath);
const songs = await m3u.readPlaylistSongs(pathInfo.fullPath);
const archive = archiver('zip');
archive.on('error', function (err) {
winston.error('Download Error', { stack: err });
res.status(500).json({ error: err.message });
});
res.attachment(`${path.basename(req.body.path)}.zip`);
archive.pipe(res);
for (let song of songs) {
const songPath = fe.join(playlistParentDir, song);
archive.file(songPath, { name: fe.basename(song) });
}
archive.finalize();
} catch (err) {
winston.error('Download Error', { stack: err })
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
mstream.post('/api/v1/download/m3u', (req, res) => {
// custom wrap download functions to avoid an error with the archiver module
downloadM3U(req, res).catch(err => {
throw err;
})
});
mstream.post('/api/v1/download/directory', async (req, res) => {
try {
if (!req.body.directory) { throw 'Validation Error' }
async function downloadM3U(req, res) {
if (!req.body.path) { throw new WebError('Validation Error', 403); }
const pathInfo = vpath.getVPathInfo(req.body.path, req.user);
const playlistParentDir = path.dirname(pathInfo.fullPath);
const songs = await m3u.readPlaylistSongs(pathInfo.fullPath);
const archive = archiver('zip');
archive.on('error', function (err) {
winston.error('Download Error', { stack: err });
res.status(500).json({ error: err.message });
});
const pathInfo = vpath.getVPathInfo(req.body.directory, req.user);
if (!pathInfo) { return res.status(500).json({ error: "Could not find file" }); }
if (!(await fs.stat(pathInfo.fullPath)).isDirectory()) { throw 'Not A Directory'; }
const archive = archiver('zip');
archive.on('error', (err) => {
winston.error('Download Error', { stack: err })
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
});
res.attachment('mstream-directory.zip');
archive.pipe(res);
archive.directory(pathInfo.fullPath, false);
archive.finalize();
} catch (err) {
winston.error('Download Error', { stack: err })
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
res.attachment(`${path.basename(req.body.path)}.zip`);
archive.pipe(res);
for (let song of songs) {
const songPath = path.join(playlistParentDir, song);
archive.file(songPath, { name: path.basename(song) });
}
archive.file(pathInfo.fullPath, { name: path.basename(pathInfo.fullPath) });
archive.finalize();
}
mstream.post('/api/v1/download/directory', (req, res) => {
downloadDir(req, res).catch(err => {
throw err;
})
});
async function downloadDir(req, res) {
if (!req.body.directory) { throw new WebError('Validation Error', 403); }
const pathInfo = vpath.getVPathInfo(req.body.directory, req.user);
if (!(await fs.stat(pathInfo.fullPath)).isDirectory()) { throw new Error('Not A Directory'); }
const archive = archiver('zip');
archive.on('error', (err) => {
winston.error('Download Error', { stack: err })
res.status(500).json({ error: 'Download Error' });
});
res.attachment('mstream-directory.zip');
archive.pipe(res);
archive.directory(pathInfo.basePath, false);
archive.finalize();
}
mstream.get('/api/v1/download/shared', (req, res) => {
try {
if (!req.sharedPlaylistId) { throw 'Missing Playlist Id'; }
const fileArray = shared.lookupPlaylist(req.sharedPlaylistId).playlist;
download(req, res, fileArray);
} catch (err) {
winston.error('Download Error', { stack: err })
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
if (!req.sharedPlaylistId) { throw new WebError('Missing Playlist Id', 403); }
const fileArray = shared.lookupPlaylist(req.sharedPlaylistId).playlist;
download(req, res, fileArray).catch(err => {
throw err;
});
});
mstream.post('/api/v1/download/zip', (req, res) => {
try {
const fileArray = JSON.parse(req.body.fileArray);
download(req, res, fileArray);
} catch (err) {
winston.error('Download Error', { stack: err })
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
const fileArray = JSON.parse(req.body.fileArray);
download(req, res, fileArray).catch(err => {
throw err;
});
});
async function download(req, res, fileArray) {
@ -96,10 +93,11 @@ exports.setup = (mstream) => {
archive.pipe(res);
for(const file of fileArray) {
const pathInfo = vpath.getVPathInfo(file, req.user);
if (!pathInfo) { continue; }
try { await fs.access(pathInfo.fullPath)} catch (err) { return; }
archive.file(pathInfo.fullPath, { name: path.basename(file) });
try {
const pathInfo = vpath.getVPathInfo(file, req.user);
await fs.access(pathInfo.fullPath);
archive.file(pathInfo.fullPath, { name: path.basename(file) });
} catch (err) { continue; }
}
archive.finalize();

View File

@ -8,6 +8,7 @@ const URL = require('url');
const winston = require('winston');
const sync = require('../state/syncthing');
const config = require('../state/config');
const { joiValidate } = require('../util/validation');
// const admin = require('../util/admin');
exports.setup = (mstream) => {
@ -44,29 +45,24 @@ exports.setup = (mstream) => {
// });
mstream.post('/api/v1/federation/invite/accept', async (req, res) => {
try {
const schema = Joi.object({
url: Joi.string().uri().required(),
vpaths: Joi.array().items(Joi.string()).required(),
invite: Joi.string().required(),
accessAll: Joi.boolean().required()
});
await schema.validateAsync(req.body);
}catch (err) {
return res.status(500).json({ error: 'Validation Error' });
}
const schema = Joi.object({
url: Joi.string().uri().required(),
vpaths: Joi.array().items(Joi.string()).required(),
invite: Joi.string().required(),
accessAll: Joi.boolean().required()
});
joiValidate(schema, req.body);
try {
const newURL = new URL(req.body.url);
newURL.pathname = '/federation/invite/exchange';
const newURL = new URL(req.body.url);
newURL.pathname = '/federation/invite/exchange';
const result = await axios({
method: 'post',
url: newURL.toString(),
headers: { 'accept': 'application/json' },
responseType: 'json',
data: { token: req.body.invite, federationId: sync.getId() }
});
const result = await axios({
method: 'post',
url: newURL.toString(),
headers: { 'accept': 'application/json' },
responseType: 'json',
data: { token: req.body.invite, federationId: sync.getId() }
});
// Add Device
@ -139,22 +135,15 @@ exports.setup = (mstream) => {
// // Save config file
// fs.writeFileSync(config.configFile, JSON.stringify(loadJson, null, 2), 'utf8');
res.json({});
}catch (err) {
return res.status(403).json({ error: err.message });
}
res.json({});
});
mstream.post('/api/v1/federation/invite/generate', async (req, res) => {
try {
const schema = Joi.object({
vpaths: Joi.array().items(Joi.string()),
url: Joi.string().optional()
});
await schema.validateAsync(req.body);
}catch (err) {
return res.status(500).json({ error: 'Validation Error' });
}
const schema = Joi.object({
vpaths: Joi.array().items(Joi.string()),
url: Joi.string().optional()
});
joiValidate(schema, req.body);
const vPaths = {};
req.body.vpaths.forEach(p => {
@ -210,24 +199,14 @@ exports.setup = (mstream) => {
});
mstream.all('/api/v1/syncthing-proxy/*', (req, res) => {
try {
// Add the auth token as a cookie so all contents of the iframe use it
if (req.token) { res.cookie('x-access-token', req.token); }
apiProxy.web(req, res, {target: 'http://' + sync.getUiAddress(), changeOrigin: true});
} catch (err) {
winston.error('Syncthing Proxy Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
// Add the auth token as a cookie so all contents of the iframe use it
if (req.token) { res.cookie('x-access-token', req.token); }
apiProxy.web(req, res, {target: 'http://' + sync.getUiAddress(), changeOrigin: true});
});
mstream.all('/api/v1/syncthing-proxy/', (req, res) => {
try {
// Add the auth token as a cookie so all contents of the iframe use it
if (req.token) { res.cookie('x-access-token', req.token); }
apiProxy.web(req, res, {target: 'http://' + sync.getUiAddress(), changeOrigin: true});
} catch (err) {
winston.error('Syncthing Proxy Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
// Add the auth token as a cookie so all contents of the iframe use it
if (req.token) { res.cookie('x-access-token', req.token); }
apiProxy.web(req, res, {target: 'http://' + sync.getUiAddress(), changeOrigin: true});
});
}

View File

@ -1,7 +1,7 @@
const path = require('path');
const fs = require('fs').promises;
const fsOld = require('fs');
const Busboy = require("busboy");
const busboy = require("busboy");
const Joi = require('joi');
const mkdirp = require('make-dir');
const winston = require('winston');
@ -9,69 +9,61 @@ const fileExplorer = require('../util/file-explorer');
const vpath = require('../util/vpath');
const m3u = require('../util/m3u');
const config = require('../state/config');
const { joiValidate } = require('../util/validation');
const WebError = require('../util/web-error');
exports.setup = (mstream) => {
mstream.post("/api/v1/file-explorer", async (req, res) => {
try {
var reqData;
const schema = Joi.object({
directory: Joi.string().allow("").required(),
sort: Joi.boolean().default(true),
pullMetadata: Joi.boolean().default(false)
});
const { value } = joiValidate(schema, req.body);
const schema = Joi.object({
directory: Joi.string().allow("").required(),
sort: Joi.boolean().default(true)
});
reqData = await schema.validateAsync(req.body);
}catch (err) {
return res.status(500).json({ error: 'Validation Error' });
// Convenience functions to get the most useful directory
if (value.directory === "~") {
if (req.user.vpaths.length !== 1) {
value.directory = "";
} else {
value.directory = `/${req.user.vpaths[0]}`;
}
}
try {
// Convenience functions to get the most useful directory
if (reqData.directory === "~") {
if (req.user.vpaths.length !== 1) {
reqData.directory = "";
} else {
reqData.directory = `/${req.user.vpaths[0]}`;
}
// Return vpaths if no path is given
if (value.directory === "" || value.directory === "/") {
const directories = [];
for (let dir of req.user.vpaths) {
directories.push({ name: dir });
}
// Return vpaths if no path is given
if (reqData.directory === "" || reqData.directory === "/") {
const directories = [];
for (let dir of req.user.vpaths) {
directories.push({ name: dir });
}
return res.json({ path: "/", directories: directories, files: [] });
}
// Get vPath Info
const pathInfo = vpath.getVPathInfo(reqData.directory, req.user);
if (!pathInfo) { throw 'Failed to find vPath'; }
// Do not allow browsing outside the directory
if (pathInfo.fullPath.substring(0, pathInfo.basePath.length) !== pathInfo.basePath) {
winston.warn(`user '${req.user.username}' attempted to access a directory they don't have access to: ${pathInfo.fullPath}`)
throw 'Access to directory not allowed';
}
// get directory contents
const folderContents = await fileExplorer.getDirectoryContents(pathInfo.fullPath, config.program.supportedAudioFiles, reqData.sort);
// Format directory string for return value
let returnDirectory = path.join(pathInfo.vpath, pathInfo.relativePath);
returnDirectory = returnDirectory.replace(/\\/g, "/"); // Formatting for windows paths
// Make sure we have a slash at the beginning & end
if (returnDirectory.slice(1) !== "/") { returnDirectory = "/" + returnDirectory; }
if (returnDirectory.slice(-1) !== "/") { returnDirectory += "/"; }
res.json({
path: returnDirectory,
files: folderContents.files,
directories: folderContents.directories
});
} catch (err) {
res.status(500).json({ error: "Failed to get directory contents" });
return res.json({ path: "/", directories: directories, files: [] });
}
// Get vPath Info
const pathInfo = vpath.getVPathInfo(value.directory, req.user);
// Do not allow browsing outside the directory
if (pathInfo.fullPath.substring(0, pathInfo.basePath.length) !== pathInfo.basePath) {
winston.warn(`user '${req.user.username}' attempted to access a directory they don't have access to: ${pathInfo.fullPath}`)
throw new Error('Access to directory not allowed');
}
// get directory contents
const folderContents = await fileExplorer.getDirectoryContents(pathInfo.fullPath, config.program.supportedAudioFiles, value.sort, value.pullMetadata, value.directory, req.user);
// Format directory string for return value
let returnDirectory = path.join(pathInfo.vpath, pathInfo.relativePath);
returnDirectory = returnDirectory.replace(/\\/g, "/"); // Formatting for windows paths
// Make sure we have a slash at the beginning & end
if (returnDirectory.slice(1) !== "/") { returnDirectory = "/" + returnDirectory; }
if (returnDirectory.slice(-1) !== "/") { returnDirectory += "/"; }
res.json({
path: returnDirectory,
files: folderContents.files,
directories: folderContents.directories
});
});
async function recursiveFileScan(directory, fileList, relativePath, vPath) {
@ -93,70 +85,53 @@ exports.setup = (mstream) => {
}
mstream.post("/api/v1/file-explorer/recursive", async (req, res) => {
try {
const schema = Joi.object({ directory: Joi.string().required() });
await schema.validateAsync(req.body);
}catch (err) {
return res.status(500).json({ error: 'Validation Error' });
const schema = Joi.object({ directory: Joi.string().required() });
joiValidate(schema, req.body);
// Get vPath Info
const pathInfo = vpath.getVPathInfo(req.body.directory, req.user);
// Do not allow browsing outside the directory
if (pathInfo.fullPath.substring(0, pathInfo.basePath.length) !== pathInfo.basePath) {
winston.warn(`user '${req.user.username}' attempted to access a directory they don't have access to: ${pathInfo.fullPath}`)
throw new Error('Access to directory not allowed');
}
try {
// Get vPath Info
const pathInfo = vpath.getVPathInfo(req.body.directory, req.user);
if (!pathInfo) { throw 'Failed to find vPath'; }
// Do not allow browsing outside the directory
if (pathInfo.fullPath.substring(0, pathInfo.basePath.length) !== pathInfo.basePath) {
winston.warn(`user '${req.user.username}' attempted to access a directory they don't have access to: ${pathInfo.fullPath}`)
throw 'Access to directory not allowed';
}
res.json(await recursiveFileScan(pathInfo.fullPath, [], pathInfo.relativePath, pathInfo.vpath));
} catch (err) {
console.log(err)
res.status(500).json({ error: "Failed to get directory contents" });
}
res.json(await recursiveFileScan(pathInfo.fullPath, [], pathInfo.relativePath, pathInfo.vpath));
});
mstream.post('/api/v1/file-explorer/upload', (req, res) => {
try {
if (config.program.noUpload === true) { throw 'Uploading Disabled'; }
if (!req.headers['data-location']) { throw 'No Location Provided'; }
if (config.program.noUpload === true) { throw new WebError('Uploading Disabled'); }
if (!req.headers['data-location']) { throw new WebError('No Location Provided', 403); }
const pathInfo = vpath.getVPathInfo(decodeURI(req.headers['data-location']), req.user);
if (!pathInfo) { throw 'Location could not be parsed'; }
const pathInfo = vpath.getVPathInfo(decodeURI(req.headers['data-location']), req.user);
mkdirp.sync(pathInfo.fullPath);
mkdirp.sync(pathInfo.fullPath);
const bb = busboy({ headers: req.headers });
bb.on('file', (fieldname, file, info) => {
const { filename } = info;
const saveTo = path.join(pathInfo.fullPath, filename);
winston.info(`Uploading from ${req.user.username} to: ${saveTo}`);
file.pipe(fsOld.createWriteStream(saveTo));
});
const busboy = new Busboy({ headers: req.headers });
busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
const saveTo = path.join(pathInfo.fullPath, filename);
winston.info(`Uploading from ${req.user.username} to: ${saveTo}`);
file.pipe(fsOld.createWriteStream(saveTo));
});
busboy.on('finish', () => { res.json({}); });
req.pipe(busboy);
} catch (err) {
winston.error('Upload Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
bb.on('close', () => { res.json({}); });
req.pipe(bb);
});
mstream.post("/api/v1/file-explorer/m3u", async (req, res) => {
try {
const pathInfo = vpath.getVPathInfo(req.body.path, req.user);
if (!pathInfo) { throw 'vpath lookup failed'; }
const playlistParentDir = path.dirname(req.body.path);
const songs = await m3u.readPlaylistSongs(pathInfo.fullPath);
res.json({
files: songs.map(function (song) {
return { type: getFileType(song), name: fe.basename(song), path: fe.join(playlistParentDir, song).replace(/\\/g, '/') }
})
});
} catch (error) {
winston.error('Upload Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
const pathInfo = vpath.getVPathInfo(req.body.path, req.user);
const playlistParentDir = path.dirname(req.body.path);
const songs = await m3u.readPlaylistSongs(pathInfo.fullPath);
res.json({
files: songs.map((song) => {
return {
type: fileExplorer.getFileType(song),
name: path.basename(song),
path: path.join(playlistParentDir, song).replace(/\\/g, '/')
};
})
});
});
}

View File

@ -1,7 +1,7 @@
const winston = require('winston');
const Joi = require('joi');
const config = require('../state/config');
const db = require('../db/manager');
const { joiValidate } = require('../util/validation');
exports.setup = (mstream) => {
// TODO: This is a legacy endpoint that should be improved
@ -11,126 +11,140 @@ exports.setup = (mstream) => {
transcode = {
defaultCodec: config.program.transcode.defaultCodec,
defaultBitrate: config.program.transcode.defaultBitrate,
defaultAlgorithm: config.program.transcode.algorithm
}
}
res.json({
const returnThis = {
vpaths: req.user.vpaths,
playlists: getPlaylists(req.user.username),
transcode
transcode,
vpathMetaData: {}
};
req.user.vpaths.forEach(p => {
if (config.program.folders[p]) {
returnThis.vpathMetaData[p] = {
type: config.program.folders[p].type
};
}
});
res.json(returnThis);
});
mstream.post('/api/v1/playlist/delete', async (req, res) => {
try {
const schema = Joi.object({ playlistname: Joi.string().required() });
await schema.validateAsync(req.body);
}catch (err) {
return res.status(500).json({ error: 'Validation Error' });
}
mstream.post('/api/v1/playlist/delete', (req, res) => {
const schema = Joi.object({ playlistname: Joi.string().required() });
joiValidate(schema, req.body);
try {
if (!db.getPlaylistCollection()) { throw 'DB Error'; }
if (!db.getPlaylistCollection()) { throw new Error('DB Error'); }
db.getPlaylistCollection().findAndRemove({
'$and': [
{ 'user': { '$eq': req.user.username }},
{ 'name': { '$eq': req.body.playlistname }}
]
});
res.json({});
} catch (err) {
winston.error('Db Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
userDataDb.saveDatabase(err => {
if (err) { winston.error('Playlist Save Error', { stack: err }); }
db.getPlaylistCollection().findAndRemove({
'$and': [
{ 'user': { '$eq': req.user.username }},
{ 'name': { '$eq': req.body.playlistname }}
]
});
db.saveUserDB();
res.json({});
});
mstream.post('/api/v1/playlist/add-song', async (req, res) => {
try {
const schema = Joi.object({
song: Joi.string().required(),
playlist: Joi.string().required()
});
await schema.validateAsync(req.body);
}catch (err) {
return res.status(500).json({ error: 'Validation Error' });
mstream.post('/api/v1/playlist/add-song', (req, res) => {
const schema = Joi.object({
song: Joi.string().required(),
playlist: Joi.string().required()
});
joiValidate(schema, req.body);
if (!db.getPlaylistCollection()) { throw new Error('No DB'); }
db.getPlaylistCollection().insert({
name: req.body.playlist,
filepath: req.body.song,
user: req.user.username
});
db.saveUserDB();
res.json({});
});
mstream.post('/api/v1/playlist/remove-song', (req, res) => {
const schema = Joi.object({ lokiid: Joi.number().integer().required() });
joiValidate(schema, req.body);
if (!db.getPlaylistCollection()) { throw new Error('No DB'); }
const result = db.getPlaylistCollection().get(req.body.lokiid);
if (result.user !== req.user.username) {
throw new Error(`User ${req.user.username} tried accessing a resource they don't have access to. Playlist Loki ID: ${req.body.lokiid}`);
}
try {
if (!db.getPlaylistCollection()) { throw 'No DB'; }
db.getPlaylistCollection().remove(result);
db.saveUserDB();
res.json({});
});
mstream.post('/api/v1/playlist/new', (req, res) => {
const schema = Joi.object({ title: Joi.string().required() });
joiValidate(schema, req.body);
const results = db.getPlaylistCollection().findOne({
'$and': [
{ 'user': { '$eq': req.user.username } },
{ 'name': { '$eq': req.body.title } }
]
});
if (results !== null) {
return res.status(400).json({ error: 'Playlist Already Exists' });
}
// insert null entry
db.getPlaylistCollection().insert({
name: req.body.title,
filepath: null,
user: req.user.username,
live: false
});
db.saveUserDB();
res.json({});
});
mstream.post('/api/v1/playlist/save', (req, res) => {
const schema = Joi.object({
title: Joi.string().required(),
songs: Joi.array().items(Joi.string()),
live: Joi.boolean().optional()
});
joiValidate(schema, req.body);
// Delete existing playlist
db.getPlaylistCollection().findAndRemove({
'$and': [
{ 'user': { '$eq': req.user.username } },
{ 'name': { '$eq': req.body.title } }
]
});
for (const song of req.body.songs) {
db.getPlaylistCollection().insert({
name: req.body.playlist,
filepath: req.body.song,
name: req.body.title,
filepath: song,
user: req.user.username
});
res.json({ });
db.saveUserDB();
}catch (err) {
winston.error('DB Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
});
mstream.post('/api/v1/playlist/remove-song', async (req, res) => {
try {
const schema = Joi.object({ lokiid: Joi.number().integer().required() });
await schema.validateAsync(req.body);
}catch (err) {
return res.status(500).json({ error: 'Validation Error' });
}
try {
if (!db.getPlaylistCollection()) { throw 'No DB'; }
db.getPlaylistCollection().findAndRemove({ '$loki': req.body.lokiid });
res.json({});
db.saveUserDB();
}catch (err) {
winston.error('Db Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
});
mstream.post('/api/v1/playlist/save', async (req, res) => {
try {
const schema = Joi.object({
title: Joi.string().required(),
songs: Joi.array().items(Joi.string())
});
await schema.validateAsync(req.body);
}catch (err) {
return res.status(500).json({ error: 'Validation Error' });
}
try {
// Delete existing playlist
db.getPlaylistCollection().findAndRemove({
'$and': [
{ 'user': { '$eq': req.user.username } },
{ 'name': { '$eq': req.body.title } }
]
});
for (const song of req.body.songs) {
db.getPlaylistCollection().insert({
name: req.body.title,
filepath: song,
user: req.user.username
});
}
// insert null entry
db.getPlaylistCollection().insert({
name: req.body.title,
filepath: null,
user: req.user.username,
live: typeof req.body.live === 'boolean' ? req.body.live : false
});
res.json({});
db.saveUserDB();
}catch (err) {
winston.error('Db Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
db.saveUserDB();
res.json({});
});
mstream.get('/api/v1/playlist/getall', (req, res) => {
@ -140,13 +154,9 @@ exports.setup = (mstream) => {
function getPlaylists(username) {
const playlists = [];
const results = db.getPlaylistCollection().find({ 'user': { '$eq': username } });
const store = {};
const results = db.getPlaylistCollection().find({ 'user': { '$eq': username }, 'filepath': { '$eq': null } });
for (let row of results) {
if (!store[row.name]) {
playlists.push({ name: row.name });
store[row.name] = true;
}
playlists.push({ name: row.name });
}
return playlists;
}

View File

@ -7,6 +7,7 @@ const jwt = require('jsonwebtoken');
const WebSocketServer = require('ws').Server;
const winston = require('winston');
const config = require('../state/config');
const { joiValidate } = require('../util/validation');
// list of currently connected clients (users)
const clients = {};
@ -27,12 +28,12 @@ exports.setupAfterAuth = (mstream, server) => {
let decoded;
if (config.program.users && Object.keys(config.program.users).length !== 0) {
const token = url.parse(info.req.url, true).query.token;
if (!token) { throw 'Token Not Found'; }
if (!token) { throw new Error('Token Not Found'); }
decoded = jwt.verify(token, config.program.secret);
}
info.req.code = url.parse(info.req.url, true).query.code;
if (info.req.code in clients) { throw 'Code In Use'; }
if (info.req.code in clients) { throw new Error('Code In Use'); }
info.req.jwt = jwt.sign({
username: decoded !== undefined ? decoded.username : 'mstream-user',
@ -66,37 +67,27 @@ exports.setupAfterAuth = (mstream, server) => {
});
mstream.post('/api/v1/jukebox/push-to-client', async (req, res) => {
try {
const schema = Joi.object({
code: Joi.string().required(),
command: Joi.string().required(),
file: Joi.string().optional()
});
await schema.validateAsync(req.body);
}catch (err) {
console.log(err)
return res.status(500).json({ error: 'Validation Error' });
mstream.post('/api/v1/jukebox/push-to-client', (req, res) => {
const schema = Joi.object({
code: Joi.string().required(),
command: Joi.string().required(),
file: Joi.string().optional()
});
joiValidate(schema, req.body);
if (!(req.body.code in clients)) {
throw new Error('Code Not Found');
}
try {
if (!(req.body.code in clients)) {
throw 'Code Not Found';
}
if (allowedCommands.indexOf(req.body.command) === -1) {
throw 'Command Not Recognized';
}
// Push commands to client
clients[req.body.code].send(JSON.stringify({ command: req.body.command, file: req.body.file ? req.body.file : '' }));
// Send confirmation back to user
res.json({ });
} catch(err) {
winston.error('Jukebox Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
if (allowedCommands.indexOf(req.body.command) === -1) {
throw new Error('Command Not Recognized');
}
// Push commands to client
clients[req.body.code].send(JSON.stringify({ command: req.body.command, file: req.body.file ? req.body.file : '' }));
// Send confirmation back to user
res.json({ });
});
}
@ -114,22 +105,17 @@ exports.setupBeforeAuth = (mstream) => {
});
mstream.get('/remote/:remoteId', async (req, res) => {
try {
const clientCode = req.params.remoteId;
if (!(clientCode in clients) || !(clientCode in codeTokenMap)) {
throw 'Token Not Found';
}
let sharePage = await fs.readFile(path.join(config.program.webAppDirectory, 'remote/index.html'), 'utf-8');
sharePage = sharePage.replace(/\.\.\//g, '../../');
sharePage = sharePage.replace(
'<script></script>',
`<script>var remoteProperties = ${JSON.stringify({ code: clientCode, error: false, token: codeTokenMap[clientCode] })}</script>`
);
res.send(sharePage);
} catch (err) {
winston.error('Jukebox Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
const clientCode = req.params.remoteId;
if (!(clientCode in clients) || !(clientCode in codeTokenMap)) {
throw new Error('Token Not Found');
}
let sharePage = await fs.readFile(path.join(config.program.webAppDirectory, 'remote/index.html'), 'utf-8');
sharePage = sharePage.replace(/\.\.\//g, '../../');
sharePage = sharePage.replace(
'<script></script>',
`<script>var remoteProperties = ${JSON.stringify({ code: clientCode, error: false, token: codeTokenMap[clientCode] })}</script>`
);
res.send(sharePage);
});
}

View File

@ -8,67 +8,52 @@ exports.setup = (mstream) => {
next();
});
mstream.post('/api/v1/scanner/get-file', async (req, res) => {
try {
const dbObj = { '$and': [
{ 'filepath': { '$eq': req.body.filepath } },
{ 'vpath': { '$eq': req.body.vpath } }
]};
const dbFileInfo = db.getFileCollection().findOne(dbObj);
mstream.post('/api/v1/scanner/get-file', (req, res) => {
const dbObj = { '$and': [
{ 'filepath': { '$eq': req.body.filepath } },
{ 'vpath': { '$eq': req.body.vpath } }
]};
const dbFileInfo = db.getFileCollection().findOne(dbObj);
// return empty response if nothing was found
if (!dbFileInfo) {
return res.json({});
}
// if the file was edited, remove it from the DB
// TODO: we need a way to handle metadata (like ratings) for modified files
else if (req.body.modTime !== dbFileInfo.modified) {
db.getFileCollection().findAndRemove(dbObj);
return res.json({});
}
// update the record with the new scan ID
// This lets us clear out old files wit ha bulk delete at the end of the scan
else {
dbFileInfo.sID = req.body.scanId;
db.getFileCollection().update(dbFileInfo);
}
res.json(dbFileInfo);
} catch (err) {
winston.error('Scanner API Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
// return empty response if nothing was found
if (!dbFileInfo) {
return res.json({});
}
// if the file was edited, remove it from the DB
// TODO: we need a way to handle metadata (like ratings) for modified files
else if (req.body.modTime !== dbFileInfo.modified) {
db.getFileCollection().findAndRemove(dbObj);
return res.json({});
}
// update the record with the new scan ID
// This lets us clear out old files wit ha bulk delete at the end of the scan
else {
dbFileInfo.sID = req.body.scanId;
db.getFileCollection().update(dbFileInfo);
}
res.json(dbFileInfo);
});
mstream.post('/api/v1/scanner/finish-scan', async (req, res) => {
try {
db.getFileCollection().findAndRemove({ '$and': [
{ 'vpath': { '$eq': req.body.vpath } },
{ 'sID': { '$ne': req.body.scanId } }
]});
mstream.post('/api/v1/scanner/finish-scan', (req, res) => {
db.getFileCollection().findAndRemove({ '$and': [
{ 'vpath': { '$eq': req.body.vpath } },
{ 'sID': { '$ne': req.body.scanId } }
]});
db.saveFilesDB();
res.json({});
}catch (err) {
winston.error('Scanner API Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
db.saveFilesDB();
res.json({});
});
let saveCounter = 0;
mstream.post('/api/v1/scanner/add-file', async (req, res) => {
try {
db.getFileCollection().insert(req.body);
res.json({});
mstream.post('/api/v1/scanner/add-file', (req, res) => {
db.getFileCollection().insert(req.body);
res.json({});
saveCounter++;
if(saveCounter > config.program.scanOptions.saveInterval) {
saveCounter = 0;
db.saveFilesDB();
}
}catch (err) {
winston.error('Scanner API Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
saveCounter++;
if(saveCounter > config.program.scanOptions.saveInterval) {
saveCounter = 0;
db.saveFilesDB();
}
});
}

View File

@ -1,9 +1,12 @@
const crypto = require('crypto');
const Joi = require('joi');
const axios = require('axios');
const winston = require('winston');
const config = require('../state/config');
const scribble = require('../state/lastfm');
const db = require('../db/manager');
const { joiValidate } = require('../util/validation');
const { getVPathInfo } = require('../util/vpath');
const Scrobbler = new scribble();
exports.setup = (mstream) => {
@ -16,59 +19,96 @@ exports.setup = (mstream) => {
Scrobbler.addUser(config.program.users[user]['lastfm-user'], config.program.users[user]['lastfm-password']);
}
mstream.post('/api/v1/lastfm/scrobble-by-metadata', async (req, res) => {
try {
const schema = Joi.object({
artist: Joi.string().required(),
album: Joi.string().required(),
track: Joi.string().required(),
});
await schema.validateAsync(req.body);
} catch (err) {
return res.status(500).json({ error: 'Validation Error' });
mstream.post('/api/v1/lastfm/scrobble-by-metadata', (req, res) => {
const schema = Joi.object({
artist: Joi.string().optional().allow(''),
album: Joi.string().optional().allow(''),
track: Joi.string().required(),
});
joiValidate(schema, req.body);
// TODO: update last-played field in DB
if (!req.user['lastfm-user'] || !req.user['lastfm-password']) {
return res.json({ scrobble: false });
}
try {
// TODO: update last-played field in DB
if (!req.user['lastfm-user'] || !req.user['lastfm-password']) {
return res.json({ scrobble: false });
}
Scrobbler.Scrobble(
req.body,
req.user['lastfm-user'],
(post_return_data) => { res.json({}); }
);
});
mstream.post('/api/v1/lastfm/scrobble-by-filepath', (req, res) => {
const schema = Joi.object({
filePath: Joi.string().required(),
});
joiValidate(schema, req.body);
// lookup metadata
const pathInfo = getVPathInfo(req.body.filePath, req.user);
const dbObj = { '$and': [
{ 'filepath': { '$eq': pathInfo.relativePath } },
{ 'vpath': { '$eq': pathInfo.vpath } }
]};
const dbFileInfo = db.getFileCollection().findOne(dbObj);
if (!dbFileInfo) {
return res.json({ scrobble: false });
}
// log play
const result = db.getUserMetadataCollection().findOne({ '$and':[{ 'hash': dbFileInfo.hash}, { 'user': req.user.username }] });
if (!result) {
db.getUserMetadataCollection().insert({
user: req.user.username,
hash: dbFileInfo.hash,
pc: 1,
lp: Date.now()
});
} else {
result.pc = result.pc && typeof result.pc === 'number'
? result.pc + 1 : 1;
result.lp = Date.now();
db.getUserMetadataCollection().update(result);
}
db.saveUserDB();
res.json({});
if (req.user['lastfm-user'] && req.user['lastfm-password']) {
// scrobble on last fm
Scrobbler.Scrobble(
req.body,
{
artist: dbFileInfo.artist,
album: dbFileInfo.album,
track: dbFileInfo.title
},
req.user['lastfm-user'],
(post_return_data) => { res.json({}); }
(post_return_data) => {}
);
}catch (err) {
winston.error('Scrobble Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
});
mstream.post('/api/v1/lastfm/test-login', async (req, res) => {
try {
const schema = Joi.object({
username: Joi.string().required(),
password: Joi.string().required()
});
await schema.validateAsync(req.body);
} catch (err) {
return res.status(500).json({ error: 'Validation Error' });
}
const schema = Joi.object({
username: Joi.string().required(),
password: Joi.string().required()
});
joiValidate(schema, req.body);
try {
const token = crypto.createHash('md5').update(req.body.username + crypto.createHash('md5').update(req.body.password, 'utf8').digest("hex"), 'utf8').digest("hex");
const cryptoString = `api_key${config.program.apiKey}authToken${token}methodauth.getMobileSessionusername${req.body.username}${config.program.apiSecret}`;
const hash = crypto.createHash('md5').update(cryptoString, 'utf8').digest("hex");
const token = crypto.createHash('md5').update(req.body.username + crypto.createHash('md5').update(req.body.password, 'utf8').digest("hex"), 'utf8').digest("hex");
const cryptoString = `api_key${config.program.apiKey}authToken${token}methodauth.getMobileSessionusername${req.body.username}${config.program.apiSecret}`;
const hash = crypto.createHash('md5').update(cryptoString, 'utf8').digest("hex");
await axios({
method: 'GET',
url: `http://ws.audioscrobbler.com/2.0/?method=auth.getMobileSession&username=${req.body.username}&authToken=${token}&api_key=${apiKey1}&api_sig=${hash}`
});
}catch (err) {
winston.error('Scrobble Error', { stack: err });
res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' });
}
await axios({
method: 'GET',
url: `http://ws.audioscrobbler.com/2.0/?method=auth.getMobileSession&username=${req.body.username}&authToken=${token}&api_key=${apiKey1}&api_sig=${hash}`
});
res.json({});
});
}

View File

@ -6,10 +6,12 @@ const fs = require('fs').promises;
const Joi = require('joi');
const config = require('../state/config');
const db = require('../db/manager');
const { joiValidate } = require('../util/validation');
const WebError = require('../util/web-error');
function lookupShared(playlistId) {
const playlistItem = db.getShareCollection().findOne({ 'playlistId': playlistId });
if (!playlistItem) { throw new Error('Playlist Not Found'); }
if (!playlistItem) { throw new WebError('Playlist Not Found'); }
// make sure the token is still good
jwt.verify(playlistItem.token, config.program.secret);
@ -33,70 +35,52 @@ exports.setupBeforeSecurity = async (mstream) => {
return res.redirect(301, req.path.slice(0, (matchEnd[0].length)*-1) + queryString[0]);
}
try {
if (!req.params.playlistId) { throw 'Validation Error' }
let sharePage = await fs.readFile(path.join(config.program.webAppDirectory, 'shared/index.html'), 'utf-8');
sharePage = sharePage.replace(
'<script></script>', `<script>const sharedPlaylist = ${JSON.stringify(lookupShared(req.params.playlistId))}</script>`
);
res.send(sharePage);
} catch (err) {
winston.error('share error', { stack: err })
return res.status(403).json({ error: 'Access Denied' });
}
if (!req.params.playlistId) { throw new WebError('Validation Error', 403); }
let sharePage = await fs.readFile(path.join(config.program.webAppDirectory, 'shared/index.html'), 'utf-8');
sharePage = sharePage.replace(
'<script></script>',
`<script>const sharedPlaylist = ${JSON.stringify(lookupShared(req.params.playlistId))}</script>`
);
res.send(sharePage);
});
mstream.get('/api/v1/shared/:playlistId', (req, res) => {
try {
if (!req.params.playlistId) { throw 'Validation Error' }
res.json(lookupShared(req.params.playlistId));
} catch (err) {
winston.error('share error', { stack: err })
return res.status(403).json({ error: 'Access Denied' });
}
if (!req.params.playlistId) { throw new WebError('Validation Error', 403); }
res.json(lookupShared(req.params.playlistId));
});
}
exports.setupAfterSecurity = async (mstream) => {
mstream.post('/api/v1/share', async (req, res) => {
try {
const schema = Joi.object({
playlist: Joi.array().items(Joi.string()).required(),
time: Joi.number().integer().positive().optional()
});
await schema.validateAsync(req.body);
}catch (err) {
return res.status(500).json({ error: 'Validation Error' });
}
mstream.post('/api/v1/share', (req, res) => {
const schema = Joi.object({
playlist: Joi.array().items(Joi.string()).required(),
time: Joi.number().integer().positive().optional()
});
joiValidate(schema, req.body);
try {
// Setup Token Data
const playlistId = nanoId.nanoid(10);
// Setup Token Data
const playlistId = nanoId.nanoid(10);
const tokenData = {
playlistId: playlistId,
shareToken: true,
username: req.user.username
};
const tokenData = {
playlistId: playlistId,
shareToken: true,
username: req.user.username
};
const jwtOptions = {};
if (req.body.time) { jwtOptions.expiresIn = `${req.body.time}d`; }
const token = jwt.sign(tokenData, config.program.secret, jwtOptions)
const jwtOptions = {};
if (req.body.time) { jwtOptions.expiresIn = `${req.body.time}d`; }
const token = jwt.sign(tokenData, config.program.secret, jwtOptions)
const sharedItem = {
playlistId: playlistId,
playlist: req.body.playlist,
user: req.user.username,
expires: req.body.time ? jwt.verify(token, config.program.secret).exp : null,
token: token
};
const sharedItem = {
playlistId: playlistId,
playlist: req.body.playlist,
user: req.user.username,
expires: req.body.time ? jwt.verify(token, config.program.secret).exp : null,
token: token
};
db.getShareCollection().insert(sharedItem);
db.saveShareDB();
res.json(sharedItem);
}catch (err) {
winston.error('Make shared error', {stack: err})
res.status(500).json({ error: 'Error' });
}
db.getShareCollection().insert(sharedItem);
db.saveShareDB();
res.json(sharedItem);
});
}

View File

@ -4,6 +4,7 @@ const ffmpeg = require("fluent-ffmpeg");
const winston = require('winston');
const vpath = require('../util/vpath');
const config = require('../state/config');
const { Readable } = require('stream');
const platform = ffbinaries.detectPlatform();
@ -13,12 +14,27 @@ const codecMap = {
'aac': { codec: 'aac', contentType: 'audio/aac' }
};
function initHeaders(res, audioTypeId, audioPath) {
const algoSet = new Set(['buffer', 'stream']);
const bitrateSet = new Set(['64k', '128k', '192k', '96k']);
exports.getTransAlgos = () => {
return Array.from(algoSet);
}
exports.getTransBitrates = () => {
return Array.from(bitrateSet);
}
exports.getTransCodecs = () => {
return Object.keys(codecMap);
}
function initHeaders(res, audioTypeId, contentLength) {
const contentType = codecMap[audioTypeId].contentType;
return res.header({
'Accept-Ranges': 'bytes',
'Content-Type': contentType,
// 'Content-Length': stat.size
'Content-Length': contentLength
});
}
@ -74,6 +90,22 @@ exports.downloadedFFmpeg = async () => {
await init();
}
const transCache = {};
function ffmpegIt(pathInfo, codec, bitrate) {
return ffmpeg(pathInfo.fullPath)
.noVideo()
.format(codec)
.audioCodec(codecMap[codec].codec)
.audioBitrate(bitrate)
.on('end', () => {
winston.info('FFmpeg: file has been converted successfully');
})
.on('error', err => {
winston.error('Transcoding Error!', { stack: err });
winston.error(pathInfo.fullPath);
});
}
exports.setup = async mstream => {
if (config.program.transcode.enabled === true) {
init().catch(err => {
@ -90,30 +122,53 @@ exports.setup = async mstream => {
return res.status(500).json({ error: 'transcoding disabled' });
}
const codec = codecMap[req.query.codec] ? req.query.codec : config.program.transcode.defaultCodec;
const algo = algoSet.has(req.query.algo) ? req.query.algo : config.program.transcode.algorithm;
const bitrate = bitrateSet.has(req.query.bitrate) ? req.query.bitrate : config.program.transcode.defaultBitrate;
const pathInfo = vpath.getVPathInfo(req.params[0], req.user);
if (!pathInfo) { return res.json({ "success": false }); }
// Stream audio data
if (req.method === 'GET') {
initHeaders(res, config.program.transcode.defaultCodec, pathInfo.fullPath);
// check cache
if (transCache[`${pathInfo.fullPath}|${bitrate}|${codec}`]) {
const t = transCache[`${pathInfo.fullPath}|${bitrate}|${codec}`].deref();
if (t!== undefined) {
initHeaders(res, codec, t.contentLength);
Readable.from(t.bufs).pipe(res);
return;
}
}
ffmpeg(pathInfo.fullPath)
.noVideo()
.format(config.program.transcode.defaultCodec)
.audioCodec(codecMap[config.program.transcode.defaultCodec].codec)
.audioBitrate(config.program.transcode.defaultBitrate)
.on('end', () => {
// console.log('file has been converted successfully');
})
.on('error', err => {
winston.error('Transcoding Error!', { stack: err });
})
// save to stream
.pipe(res, { end: true });
} else if (req.method === 'HEAD') {
// The HEAD request should return the same headers as the GET request, but not the body
initHeaders(res, config.program.transcode.defaultCodec, pathInfo.fullPath).sendStatus(200);
if (algo === 'stream') {
return ffmpegIt(pathInfo, codec, bitrate).pipe(res);
}
const bufs = [];
let contentLength = 0;
const ffstream = ffmpegIt(pathInfo, codec, bitrate).pipe();
ffstream.on('data', (chunk) => {
bufs.push(chunk);
contentLength += chunk.length;
});
ffstream.on('end', (chunk) => {
// const contentLength = bufs.reduce((sum, buf) => {
// return sum + buf.length;
// }, 0);
initHeaders(res, codec, contentLength);
transCache[`${pathInfo.fullPath}|${bitrate}|${codec}`] = new WeakRef({
contentLength, bufs
});
Readable.from(bufs).pipe(res);
});
// } else if (req.method === 'HEAD') {
// // The HEAD request should return the same headers as the GET request, but not the body
// initHeaders(res, codec, pathInfo.fullPath).sendStatus(200);
} else {
res.sendStatus(405); // Method not allowed
}

View File

@ -0,0 +1,35 @@
const child = require('child_process');
const path = require('path');
const winston = require('winston');
const config = require('../state/config');
let runningTask;
exports.run = () => {
if (runningTask !== undefined) {
return false;
}
const jsonLoad = {
albumArtDirectory: config.program.storage.albumArtDirectory,
};
const forkedScan = child.fork(path.join(__dirname, './image-compress-script.js'), [JSON.stringify(jsonLoad)], { silent: true });
winston.info(`Image Compress Script Started`);
runningTask = forkedScan;
forkedScan.stdout.on('data', (data) => {
winston.info(`Image Compress Message: ${data}`);
});
forkedScan.stderr.on('data', (data) => {
winston.error(`Image Compress Error: ${data}`);
});
forkedScan.on('close', (code) => {
winston.info(`Image compress script completed with code ${code}`);
runningTask = undefined;
});
return true;
}

View File

@ -0,0 +1,53 @@
const Jimp = require('jimp');
const Joi = require('joi');
const fs = require('fs').promises;
const path = require('path');
const mime = require('mime-types')
try {
var loadJson = JSON.parse(process.argv[process.argv.length - 1], 'utf8');
} catch (error) {
console.error(`Warning: failed to parse JSON input`);
process.exit(1);
}
// Validate input
const schema = Joi.object({
albumArtDirectory: Joi.string().required(),
});
const { error, value } = schema.validate(loadJson);
if (error) {
console.error(`Invalid JSON Input`);
console.log(error);
process.exit(1);
}
run();
async function run() {
try {
var files = await fs.readdir(loadJson.albumArtDirectory);
} catch(error) {
console.log(error);
process.exit(1);
}
for (const file of files) {
try {
const filepath = path.join(loadJson.albumArtDirectory, file);
const stat = await fs.stat(filepath);
if (stat.isDirectory()) { continue; }
const mimeType = mime.lookup(path.extname(file));
if (!mimeType.startsWith('image')) { continue; }
if (file.startsWith('zs-') || file.startsWith('zl-') || file.startsWith('zm-')) { continue; }
const img = await Jimp.read(filepath);
await img.scaleToFit(256, 256).write(path.join(loadJson.albumArtDirectory, 'zl-' + file));
await img.scaleToFit(92, 92).write(path.join(loadJson.albumArtDirectory, 'zs-' + file));
} catch (error) {
console.log('error on file: ' + filepath);
console.error(error);
}
}
}

View File

@ -4,6 +4,8 @@ const path = require('path');
const crypto = require('crypto');
const mime = require('mime-types');
const Joi = require('joi');
const Jimp = require('jimp');
const axios = require('axios').create({
httpsAgent: new (require('https')).Agent({
rejectUnauthorized: false
@ -28,6 +30,7 @@ const schema = Joi.object({
albumArtDirectory: Joi.string().required(),
scanId: Joi.string().required(),
isHttps: Joi.boolean().required(),
compressImage: Joi.boolean().required(),
supportedFiles: Joi.object().pattern(
Joi.string(), Joi.boolean()
).required()
@ -59,6 +62,8 @@ async function insertEntries(song) {
"replaygainTrackDb": song.replaygain_track_gain ? song.replaygain_track_gain.dB : null
};
if (song.genre) { data.genre = song.genre };
await axios({
method: 'POST',
url: `http${loadJson.isHttps === true ? 's': ''}://localhost:${loadJson.port}/api/v1/scanner/add-file`,
@ -166,22 +171,32 @@ async function parseFile(thisSong, modified) {
function calculateHash(filepath) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('md5').setEncoding('hex');
const fileStream = fs.createReadStream(filepath);
try {
const hash = crypto.createHash('md5').setEncoding('hex');
const fileStream = fs.createReadStream(filepath);
fileStream.on('end', () => {
hash.end();
fileStream.close();
resolve(hash.read());
});
fileStream.pipe(hash);
fileStream.on('error', (err) => {
reject(err);
});
fileStream.on('end', () => {
hash.end();
fileStream.close();
resolve(hash.read());
});
fileStream.pipe(hash);
}catch(err) {
reject(err);
}
});
}
async function getAlbumArt(songInfo) {
if (loadJson.skipImg === true) { return; }
let originalFileBuffer;
// picture is stored in song metadata
if (songInfo.picture && songInfo.picture[0]) {
// Generate unique name based off hash of album art and metadata
@ -191,10 +206,23 @@ async function getAlbumArt(songInfo) {
if (!fs.existsSync(path.join(loadJson.albumArtDirectory, songInfo.aaFile))) {
// Save file sync
fs.writeFileSync(path.join(loadJson.albumArtDirectory, songInfo.aaFile), songInfo.picture[0].data);
originalFileBuffer = songInfo.picture[0].data;
}
} else {
await checkDirectoryForAlbumArt(songInfo);
originalFileBuffer = await checkDirectoryForAlbumArt(songInfo);
}
if (originalFileBuffer) {
await compressAlbumArt(originalFileBuffer, songInfo.aaFile);
}
}
async function compressAlbumArt(buff, imgName) {
if (loadJson.compressImage === false) { return; }
const img = await Jimp.read(buff);
await img.scaleToFit(256, 256).write(path.join(loadJson.albumArtDirectory, 'zl-' + imgName));
await img.scaleToFit(92, 92).write(path.join(loadJson.albumArtDirectory, 'zs-' + imgName));
}
const mapOfDirectoryAlbumArt = {};
@ -242,6 +270,7 @@ async function checkDirectoryForAlbumArt(songInfo) {
let imageBuffer;
let picFormat;
let newFileFlag = false;
// Search for a named file
for (var i = 0; i < imageArray.length; i++) {
@ -265,9 +294,12 @@ async function checkDirectoryForAlbumArt(songInfo) {
if (!fs.existsSync(path.join(loadJson.albumArtDirectory, songInfo.aaFile))) {
// Save file sync
fs.writeFileSync(path.join(loadJson.albumArtDirectory, songInfo.aaFile), imageBuffer);
newFileFlag = true;
}
mapOfDirectoryAlbumArt[directory] = songInfo.aaFile;
if (newFileFlag === true) { return imageBuffer; }
}
function getFileType(filename) {

309
src/db/scanner.mjs Normal file
View File

@ -0,0 +1,309 @@
import { parseFile } from 'mm-v10';
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import Joi from 'joi';
import { Jimp } from 'jimpv1';
import mime from 'mime-types';
import axios from 'axios';
import https from 'https';
const ax = axios.create({
httpsAgent: new https.Agent({
rejectUnauthorized: false
})
});
try {
var loadJson = JSON.parse(process.argv[process.argv.length - 1], 'utf8');
} catch (error) {
console.error(`Warning: failed to parse JSON input`);
process.exit(1);
}
// Validate input
const schema = Joi.object({
vpath: Joi.string().required(),
directory: Joi.string().required(),
port: Joi.number().port().required(),
token: Joi.string().required(),
pause: Joi.number().required(),
skipImg: Joi.boolean().required(),
albumArtDirectory: Joi.string().required(),
scanId: Joi.string().required(),
isHttps: Joi.boolean().required(),
compressImage: Joi.boolean().required(),
supportedFiles: Joi.object().pattern(
Joi.string(), Joi.boolean()
).required()
});
const { error, value } = schema.validate(loadJson);
if (error) {
console.error(`Invalid JSON Input`);
console.log(error);
process.exit(1);
}
async function insertEntries(song) {
const data = {
"title": song.title ? String(song.title) : null,
"artist": song.artist ? String(song.artist) : null,
"year": song.year ? song.year : null,
"album": song.album ? String(song.album) : null,
"filepath": song.filePath,
"format": song.format,
"track": song.track.no ? song.track.no : null,
"disk": song.disk.no ? song.disk.no : null,
"modified": song.modified,
"hash": song.hash,
"aaFile": song.aaFile ? song.aaFile : null,
"vpath": loadJson.vpath,
"ts": Math.floor(Date.now() / 1000),
"sID": loadJson.scanId,
"replaygainTrackDb": song.replaygain_track_gain ? song.replaygain_track_gain.dB : null
};
if (song.genre) { data.genre = song.genre };
await ax({
method: 'POST',
url: `http${loadJson.isHttps === true ? 's': ''}://localhost:${loadJson.port}/api/v1/scanner/add-file`,
headers: { 'accept': 'application/json', 'x-access-token': loadJson.token },
responseType: 'json',
data: data
});
}
run();
async function run() {
try {
await recursiveScan(loadJson.directory);
await ax({
method: 'POST',
url: `http${loadJson.isHttps === true ? 's': ''}://localhost:${loadJson.port}/api/v1/scanner/finish-scan`,
headers: { 'accept': 'application/json', 'x-access-token': loadJson.token },
responseType: 'json',
data: {
vpath: loadJson.vpath,
scanId: loadJson.scanId
}
});
}catch (err) {
console.error('Scan Failed');
console.error(err.stack)
}
}
async function recursiveScan(dir) {
try {
var files = fs.readdirSync(dir);
} catch (err) {
return;
}
for (const file of files) {
const filepath = path.join(dir, file);
try {
var stat = fs.statSync(filepath);
} catch (error) {
// Bad file, ignore and continue
continue;
}
if (stat.isDirectory()) {
await recursiveScan(filepath);
} else if (stat.isFile()) {
try {
// Make sure this is in our list of allowed files
if (!loadJson.supportedFiles[getFileType(file).toLowerCase()]) {
continue;
}
const dbFileInfo = await ax({
method: 'POST',
url: `http${loadJson.isHttps === true ? 's': ''}://localhost:${loadJson.port}/api/v1/scanner/get-file`,
headers: { 'accept': 'application/json', 'x-access-token': loadJson.token },
responseType: 'json',
data: {
filepath: path.relative(loadJson.directory, filepath),
vpath: loadJson.vpath,
modTime: stat.mtime.getTime(),
scanId: loadJson.scanId
}
});
if (Object.entries(dbFileInfo.data).length === 0) {
const songInfo = await parseMyFile(filepath, stat.mtime.getTime());
await insertEntries(songInfo);
}
} catch (err) {
// console.log(err)
console.error(`Warning: failed to add file ${filepath} to database: ${err.message}`);
}
// pause
if (loadJson.pause) { await timeout(loadJson.pause); }
}
}
}
function timeout(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function parseMyFile(thisSong, modified) {
let songInfo;
try {
songInfo = (await parseFile(thisSong, { skipCovers: loadJson.skipImg })).common;
} catch (err) {
console.error(`Warning: metadata parse error on ${thisSong}: ${err.message}`);
songInfo = {track: { no: null, of: null }, disk: { no: null, of: null }};
}
songInfo.modified = modified;
songInfo.filePath = path.relative(loadJson.directory, thisSong);
songInfo.format = getFileType(thisSong);
songInfo.hash = await calculateHash(thisSong);
await getAlbumArt(songInfo);
return songInfo;
}
function calculateHash(filepath) {
return new Promise((resolve, reject) => {
try {
const hash = crypto.createHash('md5').setEncoding('hex');
const fileStream = fs.createReadStream(filepath);
fileStream.on('error', (err) => {
reject(err);
});
fileStream.on('end', () => {
hash.end();
fileStream.close();
resolve(hash.read());
});
fileStream.pipe(hash);
}catch(err) {
reject(err);
}
});
}
async function getAlbumArt(songInfo) {
if (loadJson.skipImg === true) { return; }
let originalFileBuffer;
// picture is stored in song metadata
if (songInfo.picture && songInfo.picture[0]) {
// Generate unique name based off hash of album art and metadata
const picHashString = crypto.createHash('md5').update(songInfo.picture[0].data.toString('utf-8')).digest('hex');
songInfo.aaFile = picHashString + '.' + mime.extension(songInfo.picture[0].format);
// Check image-cache folder for filename and save if doesn't exist
if (!fs.existsSync(path.join(loadJson.albumArtDirectory, songInfo.aaFile))) {
// Save file sync
fs.writeFileSync(path.join(loadJson.albumArtDirectory, songInfo.aaFile), songInfo.picture[0].data);
originalFileBuffer = songInfo.picture[0].data;
}
} else {
originalFileBuffer = await checkDirectoryForAlbumArt(songInfo);
}
if (originalFileBuffer) {
await compressAlbumArt(originalFileBuffer, songInfo.aaFile);
}
}
async function compressAlbumArt(buff, imgName) {
if (loadJson.compressImage === false) { return; }
const img = await Jimp.fromBuffer(buff);
await img.scaleToFit({w:256, h:256}).write(path.join(loadJson.albumArtDirectory, 'zl-' + imgName));
await img.scaleToFit({w:92, h:92}).write(path.join(loadJson.albumArtDirectory, 'zs-' + imgName));
}
const mapOfDirectoryAlbumArt = {};
async function checkDirectoryForAlbumArt(songInfo) {
const directory = path.join(loadJson.directory, path.dirname(songInfo.filePath));
// album art has already been found
if (mapOfDirectoryAlbumArt[directory]) {
return songInfo.aaFile = mapOfDirectoryAlbumArt[directory];
}
// directory was already scanned and nothing was found
if (mapOfDirectoryAlbumArt[directory] === false) { return; }
const imageArray = [];
try {
var files = fs.readdirSync(directory);
} catch (err) {
return;
}
for (const file of files) {
const filepath = path.join(directory, file);
try {
var stat = fs.statSync(filepath);
} catch (error) {
// Bad file, ignore and continue
continue;
}
if (!stat.isFile()) {
continue;
}
if (["png", "jpg"].indexOf(getFileType(file)) === -1) {
continue;
}
imageArray.push(file);
}
if (imageArray.length === 0) {
return mapOfDirectoryAlbumArt[directory] = false;
}
let imageBuffer;
let picFormat;
let newFileFlag = false;
// Search for a named file
for (var i = 0; i < imageArray.length; i++) {
const imgMod = imageArray[i].toLowerCase();
if (imgMod === 'folder.jpg' || imgMod === 'cover.jpg' || imgMod === 'album.jpg' || imgMod === 'folder.png' || imgMod === 'cover.png' || imgMod === 'album.png') {
imageBuffer = fs.readFileSync(path.join(directory, imageArray[i]));
picFormat = getFileType(imageArray[i]);
break;
}
}
// default to first file if none are named
if (!imageBuffer) {
imageBuffer = fs.readFileSync(path.join(directory, imageArray[0]));
picFormat = getFileType(imageArray[0]);
}
const picHashString = crypto.createHash('md5').update(imageBuffer.toString('utf8')).digest('hex');
songInfo.aaFile = picHashString + '.' + picFormat;
// Check image-cache folder for filename and save if doesn't exist
if (!fs.existsSync(path.join(loadJson.albumArtDirectory, songInfo.aaFile))) {
// Save file sync
fs.writeFileSync(path.join(loadJson.albumArtDirectory, songInfo.aaFile), imageBuffer);
newFileFlag = true;
}
mapOfDirectoryAlbumArt[directory] = songInfo.aaFile;
if (newFileFlag === true) { return imageBuffer; }
}
function getFileType(filename) {
return filename.split(".").pop();
}

View File

@ -19,10 +19,6 @@ function addScanTask(vpath) {
}
}
function removeTask(taskId) {
}
function scanAll() {
Object.keys(config.program.folders).forEach((vpath) => {
addScanTask(vpath);
@ -50,10 +46,13 @@ function runScan(scanObj) {
pause: config.program.scanOptions.pause,
supportedFiles: config.program.supportedAudioFiles,
scanId: scanObj.id,
isHttps: config.getIsHttps()
isHttps: config.getIsHttps(),
compressImage: config.program.scanOptions.compressImage
};
const forkedScan = child.fork(path.join(__dirname, './scanner.js'), [JSON.stringify(jsonLoad)], { silent: true });
winston.info('Using new file scanner: ' + config.program.scanOptions.newScan);
const scanFile = config.program.scanOptions.newScan ? 'scanner.mjs' : 'scanner.js';
const forkedScan = child.fork(path.join(__dirname, `./${scanFile}`), [JSON.stringify(jsonLoad)], { silent: true });
winston.info(`File scan started on ${jsonLoad.directory}`);
runningTasks.add(forkedScan);
vpathLimiter.add(scanObj.vpath);

View File

@ -2,8 +2,11 @@ const winston = require('winston');
const express = require('express');
const fs = require('fs');
const path = require('path');
const bodyParser = require('body-parser');
const Joi = require('joi');
const cookieParser = require('cookie-parser');
const jwt = require('jsonwebtoken');
require('./util/async-error');
const dbApi = require('./api/db');
const playlistApi = require('./api/playlist');
@ -21,6 +24,7 @@ const dbManager = require('./db/manager');
const syncthing = require('./state/syncthing');
const federationApi = require('./api/federation');
const scannerApi = require('./api/scanner');
const WebError = require('./util/web-error');
let mstream;
let server;
@ -60,8 +64,8 @@ exports.serveIt = async configFile => {
// Magic Middleware Things
mstream.use(cookieParser());
mstream.use(bodyParser.json());
mstream.use(bodyParser.urlencoded({ extended: true }));
mstream.use(express.json({ limit: config.program.maxRequestSize }));
mstream.use(express.urlencoded({ extended: true }));
mstream.use((req, res, next) => { // CORS
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
@ -79,7 +83,7 @@ exports.serveIt = async configFile => {
const matchEnd = req.path.match(/(\/)+$/g);
const queryString = req.url.match(/(\?.*)/g) === null ? '' : req.url.match(/(\?.*)/g);
// redirect to a more sane URL
return res.redirect(301, req.path.slice(0, (matchEnd[0].length - 1)*-1) + queryString);
return res.redirect(302, req.path.slice(0, (matchEnd[0].length - 1)*-1) + queryString);
}
next();
});
@ -87,7 +91,16 @@ exports.serveIt = async configFile => {
// Block access to admin page if necessary
mstream.get('/admin', (req, res, next) => {
if (config.program.lockAdmin === true) { return res.send('<p>Admin Page Disabled</p>'); }
next();
if (Object.keys(config.program.users).length === 0){
return next();
}
try {
jwt.verify(req.cookies['x-access-token'], config.program.secret);
next();
} catch(err) {
return res.redirect(302, '/login');
}
});
mstream.get('/admin/index.html', (req, res, next) => {
@ -95,6 +108,32 @@ exports.serveIt = async configFile => {
next();
});
mstream.get('/', (req, res, next) => {
if (Object.keys(config.program.users).length === 0){
return next();
}
try {
jwt.verify(req.cookies['x-access-token'], config.program.secret);
next();
} catch(err) {
return res.redirect(302, '/login');
}
});
mstream.get('/login', (req, res, next) => {
if (Object.keys(config.program.users).length === 0){
return res.redirect(302, '..');
}
try {
jwt.verify(req.cookies['x-access-token'], config.program.secret);
return res.redirect(302, '..');
} catch(err) {
next();
}
});
// Give access to public folder
mstream.use('/', express.static(config.program.webAppDirectory));
@ -119,11 +158,18 @@ exports.serveIt = async configFile => {
federationApi.setup(mstream);
// Versioned APIs
mstream.get('/api/', (req, res) => res.json({ "version": "0.1.0", "supportedVersions": ["1"] }));
mstream.get('/api/v1', (req, res) => res.json({ "version": "0.1.0" }));
mstream.get('/api/', (req, res) => res.json({ "server": require('../package.json').version, "apiVersions": ["1"] }));
// album art folder
mstream.use('/album-art', express.static(config.program.storage.albumArtDirectory));
mstream.get('/album-art/:file', (req, res) => {
if (!req.params.file) { throw new WebError('Missing Error', 404); }
if (req.query.compress && fs.existsSync(path.join(config.program.storage.albumArtDirectory, `z${req.query.compress}-${req.params.file}`))) {
return res.sendFile(path.join(config.program.storage.albumArtDirectory, `z${req.query.compress}-${req.params.file}`));
}
res.sendFile(path.join(config.program.storage.albumArtDirectory, req.params.file));
});
// TODO: determine if user has access to the exact file
// mstream.all('/media/*', (req, res, next) => {
@ -134,6 +180,22 @@ exports.serveIt = async configFile => {
mstream.use('/media/' + key + '/', express.static(config.program.folders[key].root));
});
// error handling
mstream.use((error, req, res, next) => {
winston.error(`Server error on route ${req.originalUrl}`, { stack: error });
// Check for validation error
if (error instanceof Joi.ValidationError) {
return res.status(403).json({ error: error.message });
}
if (error instanceof WebError) {
return res.status(error.status).json({ error: error.message });
}
res.status(500).json({ error: 'Server Error' });
});
// Start the server!
server.on('request', mstream);
server.listen(config.program.port, config.program.address, () => {

View File

@ -2,6 +2,7 @@ const fs = require("fs").promises;
const path = require('path');
const Joi = require('joi');
const winston = require('winston');
const { getTransAlgos, getTransCodecs, getTransBitrates } = require('../api/transcode');
const storageJoi = Joi.object({
albumArtDirectory: Joi.string().default(path.join(__dirname, '../../image-cache')),
@ -16,7 +17,9 @@ const scanOptions = Joi.object({
saveInterval: Joi.number().default(250),
pause: Joi.number().min(0).default(0),
bootScanDelay: Joi.number().default(3),
maxConcurrentTasks: Joi.number().integer().min(1).default(1)
maxConcurrentTasks: Joi.number().integer().min(1).default(1),
compressImage: Joi.boolean().default(true),
newScan: Joi.boolean().default(false)
});
const dbOptions = Joi.object({
@ -24,10 +27,11 @@ const dbOptions = Joi.object({
});
const transcodeOptions = Joi.object({
algorithm: Joi.string().valid(...getTransAlgos()).default('stream'),
enabled: Joi.boolean().default(false),
ffmpegDirectory: Joi.string().default(path.join(__dirname, '../../bin/ffmpeg')),
defaultCodec: Joi.string().valid('mp3', 'opus', 'aac').default('opus'),
defaultBitrate: Joi.string().valid('64k', '128k', '192k', '96k').default('96k')
defaultCodec: Joi.string().valid(...getTransCodecs()).default('opus'),
defaultBitrate: Joi.string().valid(...getTransBitrates()).default('96k')
});
const rpnOptions = Joi.object({
@ -51,13 +55,13 @@ const federationOptions = Joi.object({
});
const schema = Joi.object({
address: Joi.string().ip({ cidr: 'forbidden' }).default('0.0.0.0'),
address: Joi.string().ip({ cidr: 'forbidden' }).default('::'),
port: Joi.number().default(3000),
supportedAudioFiles: Joi.object().pattern(
Joi.string(), Joi.boolean()
).default({
"mp3": true, "flac": true, "wav": true,
"ogg": true, "aac": true, "m4a": true,
"ogg": true, "aac": true, "m4a": true, "m4b": true,
"opus": true, "m3u": false
}),
lastFM: lastFMOptions.default(lastFMOptions.validate({}).value),
@ -70,11 +74,13 @@ const schema = Joi.object({
rpn: rpnOptions.default(rpnOptions.validate({}).value),
transcode: transcodeOptions.default(transcodeOptions.validate({}).value),
secret: Joi.string().optional(),
maxRequestSize: Joi.string().pattern(/[0-9]+(KB|MB)/i).default('1MB'),
db: dbOptions.default(dbOptions.validate({}).value),
folders: Joi.object().pattern(
Joi.string(),
Joi.object({
root: Joi.string().required()
root: Joi.string().required(),
type: Joi.string().valid('music', 'audio-books').default('music'),
})
).default({}),
users: Joi.object().pattern(

View File

@ -4,13 +4,14 @@ const nanoid = require('nanoid')
const winston = require('winston');
const path = require('path');
const { spawn } = require('child_process');
const parser = require('fast-xml-parser');
const { XMLParser } = require("fast-xml-parser");
const axios = require('axios');
const https = require('https');
const kill = require('tree-kill');
const killQueue = require('./kill-list');
const config = require('./config');
const parser = new XMLParser();
const platform = os.platform();
const osMap = {
"win32": "syncthing.exe",

View File

@ -1,4 +1,6 @@
const fs = require("fs").promises;
const path = require("path");
const child = require('child_process');
const express = require('express');
const auth = require('./auth');
const config = require('../state/config');
@ -16,7 +18,7 @@ exports.saveFile = async (saveData, file) => {
return await fs.writeFile(file, JSON.stringify(saveData, null, 2), 'utf8')
}
exports.addDirectory = async (directory, vpath, autoAccess, mstream) => {
exports.addDirectory = async (directory, vpath, autoAccess, isAudioBooks, mstream) => {
// confirm directory is real
const stat = await fs.stat(directory);
if (!stat.isDirectory()) { throw `${directory} is not a directory` };
@ -28,6 +30,7 @@ exports.addDirectory = async (directory, vpath, autoAccess, mstream) => {
// Once the file save is complete, the new user will be added
const memClone = JSON.parse(JSON.stringify(config.program.folders));
memClone[vpath] = { root: directory };
if (isAudioBooks) { memClone[vpath].type = 'audio-books'; }
// add directory to config file
const loadConfig = await this.loadFile(config.configFile);
@ -42,7 +45,7 @@ exports.addDirectory = async (directory, vpath, autoAccess, mstream) => {
await this.saveFile(loadConfig, config.configFile);
// add directory to program
config.program.folders[vpath] = { root: directory };
config.program.folders[vpath] = memClone[vpath];
if (autoAccess === true) {
Object.values(config.program.users).forEach(user => {
@ -185,6 +188,17 @@ exports.editPort = async (port) => {
mStreamServer.reboot();
}
exports.editMaxRequestSize = async (maxRequestSize) => {
if (config.program.maxRequestSize === maxRequestSize) { return; }
const loadConfig = await this.loadFile(config.configFile);
loadConfig.maxRequestSize = maxRequestSize;
await this.saveFile(loadConfig, config.configFile);
// reboot server
mStreamServer.reboot();
}
exports.editUpload = async (val) => {
const loadConfig = await this.loadFile(config.configFile);
loadConfig.noUpload = val;
@ -240,6 +254,15 @@ exports.editSkipImg = async (val) => {
config.program.scanOptions.skipImg = val;
}
exports.editNewScan = async (val) => {
const loadConfig = await this.loadFile(config.configFile);
if (!loadConfig.scanOptions) { loadConfig.scanOptions = {}; }
loadConfig.scanOptions.newScan = val;
await this.saveFile(loadConfig, config.configFile);
config.program.scanOptions.newScan = val;
}
exports.editPause = async (val) => {
const loadConfig = await this.loadFile(config.configFile);
if (!loadConfig.scanOptions) { loadConfig.scanOptions = {}; }
@ -267,6 +290,15 @@ exports.editMaxConcurrentTasks = async (val) => {
config.program.scanOptions.maxConcurrentTasks = val;
}
exports.editCompressImages = async (val) => {
const loadConfig = await this.loadFile(config.configFile);
if (!loadConfig.scanOptions) { loadConfig.scanOptions = {}; }
loadConfig.scanOptions.compressImage = val;
await this.saveFile(loadConfig, config.configFile);
config.program.scanOptions.compressImage = val;
}
exports.editWriteLogs = async (val) => {
const loadConfig = await this.loadFile(config.configFile);
loadConfig.writeLogs = val;
@ -308,6 +340,15 @@ exports.editDefaultBitrate = async (val) => {
config.program.transcode.defaultBitrate = val;
}
exports.editDefaultAlgorithm = async (val) => {
const loadConfig = await this.loadFile(config.configFile);
if (!loadConfig.transcode) { loadConfig.transcode = {}; }
loadConfig.transcode.algorithm = val;
await this.saveFile(loadConfig, config.configFile);
config.program.transcode.algorithm = val;
}
exports.lockAdminApi = async (val) => {
const loadConfig = await this.loadFile(config.configFile);
loadConfig.lockAdmin = val;
@ -317,11 +358,44 @@ exports.lockAdminApi = async (val) => {
}
exports.enableFederation = async (val) => {
const memClone = JSON.parse(JSON.stringify(config.program.federation));
memClone.enabled = val;
const loadConfig = await this.loadFile(config.configFile);
loadConfig.federation.enabled = val;
loadConfig.federation = memClone;
await this.saveFile(loadConfig, config.configFile);
config.program.federation.enabled = val;
syncthing.setup();
}
exports.removeSSL = async () => {
const loadConfig = await this.loadFile(config.configFile);
delete loadConfig.ssl;
await this.saveFile(loadConfig, config.configFile);
delete config.program.ssl;
mStreamServer.reboot();
}
function testSSL(jsonLoad) {
return new Promise((resolve, reject) => {
child.fork(path.join(__dirname, './ssl-test.js'), [JSON.stringify(jsonLoad)], { silent: true }).on('close', (code) => {
if (code !== 0) {
return reject('SSL Failure');
}
resolve();
});
});
}
exports.setSSL = async (cert, key) => {
const sslObj = { key, cert };
await testSSL(sslObj);
const loadConfig = await this.loadFile(config.configFile);
loadConfig.ssl = sslObj;
await this.saveFile(loadConfig, config.configFile);
config.program.ssl = sslObj;
mStreamServer.reboot();
}

30
src/util/async-error.js Normal file
View File

@ -0,0 +1,30 @@
'use strict';
const Layer = require('express/lib/router/layer');
const noop = () => {};
Object.defineProperty(Layer.prototype, "handle", {
enumerable: true,
get: function() { return this.__handle; },
set: function(fn) {
if (isAsync(fn)) {
fn = wrapAsync(fn);
}
this.__handle = fn;
}
});
function isAsync(fn) {
const type = Object.toString.call(fn.constructor);
return type.indexOf('AsyncFunction') !== -1;
};
function wrapAsync(fn) {
return (req, res, next = noop) => {
fn(req, res, next)
.catch((err) => {
next(err);
});
}
};

View File

@ -21,7 +21,7 @@ exports.hashPassword = password => {
exports.authenticateUser = (password, salt, givenPassword) => {
return new Promise((resolve, reject) => {
crypto.pbkdf2(givenPassword, salt, ITERATIONS, HASH_BYTES, ALGORITHM, (err, verifyHash) => {
if (err) { reject('Unknown Authentication Error'); }
if (err) { return reject('Unknown Authentication Error'); }
if (verifyHash.toString(ENCODING) !== password) {
return reject('Authentication Error: Passwords do not match');
}

View File

@ -1,11 +1,12 @@
const fs = require("fs").promises;
const path = require("path");
const dbApi = require('../api/db');
exports.getFileType = (pathString) => {
return path.extname(pathString).substr(1);
}
exports.getDirectoryContents = async (directory, fileTypeFilter, sort) => {
exports.getDirectoryContents = async (directory, fileTypeFilter, sort, pm, metaDir, user) => {
const rt = { directories: [], files: [] };
for (const file of await fs.readdir(directory)) {
try {
@ -21,10 +22,16 @@ exports.getDirectoryContents = async (directory, fileTypeFilter, sort) => {
// Handle Files
const extension = this.getFileType(file).toLowerCase();
if (fileTypeFilter && extension in fileTypeFilter) {
rt.files.push({
const fileInfo = {
type: extension,
name: file
});
};
if (pm) {
fileInfo.metadata = dbApi.pullMetaData(path.join(metaDir, file).replace(/\\/g, '/'), user);
}
rt.files.push(fileInfo);
}
}

View File

@ -2,7 +2,7 @@ const fs = require("fs").promises;
const m3u8Parser = require('m3u8-parser');
exports.readPlaylistSongs = async (filePath) => {
const fileContents = await fs.readFile(filePath).toString();
const fileContents = (await fs.readFile(filePath)).toString();
const parser = new m3u8Parser.Parser();
parser.push(fileContents);

22
src/util/ssl-test.js Normal file
View File

@ -0,0 +1,22 @@
const fs = require('fs');
try {
var loadJson = JSON.parse(process.argv[process.argv.length - 1], 'utf8');
} catch (error) {
console.error(`Warning: failed to parse JSON input`);
process.exit(1);
}
// check if files exist
if (!fs.existsSync(loadJson.cert) || !fs.existsSync(loadJson.key)) {
process.exit(1);
}
try {
require('https').createServer({
key: fs.readFileSync(loadJson.key),
cert: fs.readFileSync(loadJson.cert)
});
} catch (error) {
process.exit(1);
}

15
src/util/validation.js Normal file
View File

@ -0,0 +1,15 @@
'use strict';
require('joi');
const joiValidate = (joiSchema, validateThis, throwErr) => {
const { error, value } = joiSchema.validate(validateThis);
// Defaults to throwing an error
if (error !== undefined && throwErr !== false) {
throw error;
}
return { error, value };
}
module.exports = { joiValidate };

View File

@ -2,7 +2,7 @@ const path = require('path');
const config = require('../state/config');
exports.getVPathInfo = (url, user) => {
if (!config.program) { throw 'Not Configured'; }
if (!config.program) { throw new Error('Not Configured'); }
// remove leading slashes
if (url.charAt(0) === '/') {
@ -13,7 +13,7 @@ exports.getVPathInfo = (url, user) => {
const vpath = url.split('/').shift();
// Verify user has access to this vpath
if (user && !user.vpaths.includes(vpath)) {
return false;
throw new Error(`User does not have access to path ${vpath}`);
}
const baseDir = config.program.folders[vpath].root;

17
src/util/web-error.js Normal file
View File

@ -0,0 +1,17 @@
'use strict';
class WebError extends Error {
constructor (message, code) {
super(message)
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name
if(!Number.isInteger(code) || code < 400 || code > 599) {
code = 500;
};
this.status = code;
}
}
module.exports = WebError;

View File

@ -63,4 +63,15 @@ a {
.flow-root{
display: flow-root;
}
#select-win-drive {
max-width: 55px;
margin-right: 10px;
cursor: pointer;
max-height: 40px;
}
.flex {
display: flex;
}

View File

@ -39,7 +39,6 @@
<link rel="stylesheet" href="index.css">
<script src="../assets/js/api.js"></script>
<script>API.checkAuthAndKickToLogin();</script>
</head>
<body>
@ -101,6 +100,10 @@
<div class="side-nav-header">
<span>Server</span>
</div>
<div class="side-nav-item waves-effect waves-purple" onclick="changeView('info-view', this)">
<svg xmlns="http://www.w3.org/2000/svg" height="28" viewBox="0 -1 24 24" fill="#fff"><path d="M4 2c-1.093 0-2 .907-2 2v18.406l1.719-1.687L6.438 18H20c1.093 0 2-.907 2-2V4c0-1.093-.907-2-2-2H4zm0 2h16v12H5.594l-.313.281L4 17.563V4zm7 2v2h2V6h-2zm0 3v5h2V9h-2z"/></svg>
<span>About</span>
</div>
<div class="side-nav-item waves-effect waves-purple" onclick="changeView('advanced-view', this)">
<svg height="28" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M490.51,294.933v-78.613l-57.766-11.093c-3.883-13.594-9.354-26.549-16.092-38.657l32.701-48.337 l-55.602-55.587l-48.244,32.663c-12.309-6.885-25.492-12.177-39.338-16.109l-11.043-57.14h-78.614l-11.049,57.194 C191.568,83.21,178.34,88.654,166,95.592l-48.069-32.61l-55.589,55.562l32.726,48.312c-6.765,12.204-12.277,25.253-16.142,38.959 L21.49,216.841v78.613l57.659,11.078c3.917,13.661,9.426,26.676,16.228,38.834l-32.932,48.67l55.601,55.59l48.83-33.061 c12.103,6.719,25.048,11.981,38.629,15.852l11.099,57.524h78.614l11.096-57.531c13.684-3.894,26.717-9.275,38.896-16.063 l48.832,33.101l55.588-55.574l-33.072-48.836c6.805-12.221,12.383-25.298,16.279-39.032L490.51,294.933 M255.975,378.017 c-67.441,0-122.115-54.673-122.115-122.114s54.674-122.115,122.115-122.115c67.441,0,122.115,54.673,122.115,122.115 S323.416,378.017,255.975,378.017" fill="#F56F6C"/><path d="M254.297,378v0.006c0.439,0.006,0.877,0.01,1.316,0.01C255.322,378.014,255.289,378.005,254.297,378 M255.676,133.789c-0.461,0-0.92,0.004-1.379,0.011v0.005C255.313,133.801,255.359,133.791,255.676,133.789 M255.975,133.788 l-0.086,0.001c67.402,0.047,122.113,54.702,122.113,122.114c0,67.405-54.744,122.055-122.135,122.114h0.107 c67.441,0,122.115-54.673,122.115-122.114S323.416,133.788,255.975,133.788 M393.775,62.645l-0.014,0.009l55.592,55.579 l-32.701,48.337c6.738,12.108,12.209,25.063,16.092,38.657l57.766,11.093v78.613v-78.613l-57.766-11.093 c-3.883-13.594-9.268-26.549-16.006-38.657l32.66-48.337L393.775,62.645 M295.127,22.058h-40.83H295.127l11.043,57.14 c13.846,3.933,27.029,9.225,39.338,16.109l0.006-0.003c-12.307-6.884-25.494-12.174-39.338-16.106L295.127,22.058" fill="#F2F2F2"/><path d="M295.127,22.058h-40.83V133.8c0.459-0.007,0.918-0.011,1.379-0.011l0.125-0.001l0.088,0.001l0.086-0.001 c67.441,0,122.115,54.673,122.115,122.115s-54.674,122.114-122.115,122.114h-0.107h-0.109l-0.145-0.001 c-0.439,0-0.877-0.004-1.316-0.01v111.936h40.92l11.096-57.531c13.684-3.894,26.717-9.275,38.896-16.063l48.832,33.101 l55.588-55.574l-33.072-48.836c6.805-12.221,12.383-25.298,16.279-39.032l57.674-11.073v-78.613l-57.766-11.093 c-3.883-13.594-9.354-26.549-16.092-38.657l32.701-48.337l-55.592-55.579l-48.244,32.654l-0.004-0.003l-0.006,0.003 c-12.309-6.885-25.492-12.177-39.338-16.109L295.127,22.058" fill="#E96966"/><path d="M256.072,111.928c-79.505,0-143.957,64.451-143.957,143.956c0,79.503,64.452,143.955,143.957,143.955 c79.504,0,143.955-64.452,143.955-143.955C400.027,176.379,335.576,111.928,256.072,111.928z M256.072,329.569 c-40.696,0-73.687-32.99-73.687-73.686c0-40.697,32.991-73.687,73.687-73.687c40.695,0,73.686,32.989,73.686,73.687 C329.758,296.579,296.768,329.569,256.072,329.569z" fill="#FFF"/></svg>
<span>Settings</span>
@ -117,18 +120,21 @@
<svg xmlns="http://www.w3.org/2000/svg" height="28" viewBox="0 0 58 58"><path d="M36.028 0c-11.046 0-17 3.515-17 6v10.849l-4.335-4.335-.118-.118c-.586-.586-3.943-1-6.547 1.604-2.604 2.603-3.467 7.238-2.881 7.824l.118.118 13.763 13.763V52.5c0 3.038 5.954 5.5 17 5.5s17-2.462 17-5.5V6c0-2.485-5.954-6-17-6z" fill="#745a49"/><path d="M36.028 0C25.89 0 20.056 2.959 19.163 5.368 20.056 8.546 25.89 11 36.028 11 46.166 11 52 8.546 52.893 5.368 52.001 2.959 46.166 0 36.028 0zM15.735 13.556l-1.042-1.042-.118-.118c-.586-.586-3.943-1-6.547 1.604-2.604 2.603-3.467 7.238-2.881 7.824l.118.118.952.952c6.334-1.911 8.696-5.948 9.518-9.338z" fill="#d1a170"/><path d="M46.028 50a1 1 0 00-1 1v6.304a28.434 28.434 0 002-.435V51a1 1 0 00-1-1zM40.028 47a1 1 0 00-1 1v9.928a55.775 55.775 0 002-.124V48a1 1 0 00-1-1zM36.028 54a1 1 0 00-1 1v2.986c.333.005.657.014 1 .014.343 0 .666-.009 1-.014V55a1 1 0 00-1-1zM31.028 49a1 1 0 00-1 1v7.716c.642.065 1.304.122 2 .165V50a1 1 0 00-1-1zM24.028 42a1 1 0 00-1 1v13.251c.601.225 1.268.431 2 .618V43a1 1 0 00-1-1zM23.006 8.924l.022 10.078a1 1 0 001 .998h.001a1 1 0 00.999-1.002l-.02-9.339a17.271 17.271 0 01-2.002-.735zM27.991 10.364l.037 15.638a1 1 0 001 .998h.002a1 1 0 00.998-1.002l-.036-15.34a36.834 36.834 0 01-2.001-.294zM37.028 10.985V16a1 1 0 002 0v-5.083c-.645.034-1.313.056-2 .068zM44.028 10.37V27a1 1 0 002 0V9.94c-.628.159-1.291.304-2 .43zM48.028 9.336V19a1 1 0 002 0V8.435a13.82 13.82 0 01-2 .901z" fill="#3d3028"/></svg>
<span>Logs</span>
</div>
<div class="side-nav-item waves-effect waves-purple" onclick="changeView('info-view', this)">
<svg xmlns="http://www.w3.org/2000/svg" height="28" viewBox="0 -1 24 24" fill="#fff"><path d="M4 2c-1.093 0-2 .907-2 2v18.406l1.719-1.687L6.438 18H20c1.093 0 2-.907 2-2V4c0-1.093-.907-2-2-2H4zm0 2h16v12H5.594l-.313.281L4 17.563V4zm7 2v2h2V6h-2zm0 3v5h2V9h-2z"/></svg>
<span>About</span>
</div>
<div class="side-nav-header">
<!-- <div class="side-nav-header">
<span>Security</span>
</div>
</div> -->
<div class="side-nav-item waves-effect waves-purple" onclick="changeView('lock-view', this)">
<svg xmlns="http://www.w3.org/2000/svg" height="28" viewBox="0 0 460 460"><path d="M360.228,386.747L230,460L99.772,386.747c-27.804-15.639-45.01-45.06-45.01-76.96 V21.905L230,0l175.238,21.905v287.882C405.238,341.687,388.032,371.107,360.228,386.747z" fill="#86c867"/><path d="M335.565,357.061L230,416.442l-105.565-59.38 c-22.538-12.678-36.486-36.526-36.486-62.385V54.762L230,37.006l142.051,17.756v239.915 C372.051,320.535,358.103,344.384,335.565,357.061z" fill="#5e9b3e"/><path d="M230,460l130.228-73.253c27.803-15.64,45.01-45.06,45.01-76.96V21.905 L230,0v37.005l142.051,17.756v239.915c0,25.859-13.948,49.707-36.486,62.385L230,416.442V460z" opacity=".7" fill="#86c867"/><path d="M230,98.571c-30.244,0-54.762,24.518-54.762,54.762 c0,22.454,13.519,41.74,32.857,50.192V222.2h9.701c4.308,0,7.8,3.492,7.8,7.8s-3.492,7.8-7.8,7.8h-9.701v17.257h9.701 c4.308,0,7.8,3.492,7.8,7.8c0,4.308-3.492,7.8-7.8,7.8h-9.701v17.257h9.701c4.308,0,7.8,3.492,7.8,7.8c0,4.308-3.492,7.8-7.8,7.8 h-9.701v25.057L230,339.524l21.905-10.952V203.525c19.338-8.452,32.857-27.739,32.857-50.192 C284.762,123.089,260.244,98.571,230,98.571z M230,157.167c-8.166,0-14.786-6.62-14.786-14.786c0-8.166,6.62-14.786,14.786-14.786 s14.786,6.62,14.786,14.786C244.786,150.547,238.166,157.167,230,157.167z" fill="#ecf0f1"/></svg>
<span>Lock Admin API</span>
</div>
<div class="side-nav-spacer"></div>
<!-- <div class="side-nav-spacer"></div> -->
<div class="side-nav-header">
<span>Navigation</span>
</div>
<div class="side-nav-item waves-effect waves-purple" onclick="API.goToPlayer()">
<svg height="28" width="28" viewBox="0 0 137.3 139.3" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><circle cx="68.5" cy="69.6" r="66.2" fill="#2c8aaa"/><defs><circle cx="68.7" cy="69.6" id="aaa" r="66.2"/></defs><clipPath id="bbb"><use xlink:href="#aaa" overflow="visible"/></clipPath><path opacity=".1" clip-path="url(#bbb)" fill="#070808" d="m104.9 65 32 30.9-2.9 39.9-44.8-1.9-37.1-36.6z"/><path d="m104.3 64.5-48.6-28c-2-1.1-4.5.3-4.5 2.6v56.1c0 2.3 2.5 3.7 4.5 2.6l48.6-28c2-1.2 2-4.1 0-5.3z" fill="#fff"/></svg>
<span>Player</span>
</div>
<div class="side-nav-item waves-effect waves-purple" onclick="API.logout()">
<svg xmlns="http://www.w3.org/2000/svg" height="28" width="28" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path fill="#fff" d="M10.09 15.59L11.5 17l5-5-5-5-1.41 1.41L12.67 11H3v2h9.67l-2.58 2.59zM19 3H5c-1.11 0-2 .9-2 2v4h2V5h14v14H5v-4H3v4c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>
<span>Logout</span>

View File

@ -1,6 +1,8 @@
const ADMINDATA = (() => {
const module = {};
module.version = { val: false };
// Used for handling the file explorer selection
module.sharedSelect = { value: '' };
@ -13,6 +15,7 @@ const ADMINDATA = (() => {
// folders
module.folders = {};
module.foldersUpdated = { ts: 0 };
module.winDrives = [];
// users
module.users = {};
module.usersUpdated = { ts: 0 };
@ -40,7 +43,6 @@ const ADMINDATA = (() => {
method: 'GET',
url: `${API.url()}/api/v1/admin/db/shared`
});
console.log(res.data)
while(module.sharedPlaylists.length !== 0) {
module.sharedPlaylists.pop();
@ -169,6 +171,33 @@ const ADMINDATA = (() => {
module.federationParamsUpdated.ts = Date.now();
}
module.getVersion = async () => {
try {
const res = await API.axios({
method: 'GET',
url: `${API.url()}/api`
});
module.version.val = res.data.server;
}catch (err) {}
}
module.getWinDrives = async () => {
try {
const res = await API.axios({
method: 'GET',
url: `${API.url()}/api/v1/admin/file-explorer/win-drives`
});
module.winDrives.length = 0;
res.data.forEach((d) => {
module.winDrives.push(d);
});
console.log(res.data)
return res;
}catch(err){}
}
return module;
})();
@ -179,6 +208,8 @@ ADMINDATA.getUsers();
ADMINDATA.getDbParams();
ADMINDATA.getServerParams();
ADMINDATA.getFederationParams();
ADMINDATA.getVersion();
ADMINDATA.getWinDrives();
// initialize modal
M.Modal.init(document.querySelectorAll('.modal'), {
@ -231,6 +262,10 @@ const foldersView = Vue.component('folders-view', {
<input id="folder-auto-access" type="checkbox" checked/>
<span>Give Access To All Users</span>
</label></div>
<div class="pad-checkbox"><label>
<input id="folder-is-audiobooks" type="checkbox"/>
<span>Audiobooks & Podcasts</span>
</label></div>
</div>
<button class="btn green waves-effect waves-light col m6 s12" type="submit" :disabled="submitPending === true">
{{submitPending === false ? 'Add Folder' : 'Adding...'}}
@ -314,7 +349,8 @@ const foldersView = Vue.component('folders-view', {
data: {
directory: this.folder.value,
vpath: this.dirName,
autoAccess: document.getElementById('folder-auto-access').checked
autoAccess: document.getElementById('folder-auto-access').checked,
isAudioBooks: document.getElementById('folder-is-audiobooks').checked
}
});
@ -414,7 +450,7 @@ const usersView = Vue.component('users-view', {
<form id="add-user-form" @submit.prevent="addUser">
<div class="row">
<div class="input-field directory-name-field col s12 m6">
<input @blur="maybeResetForm()" pattern="[a-zA-Z0-9-]+" v-model="newUsername" id="new-username" required type="text" class="validate">
<input @blur="maybeResetForm()" v-model="newUsername" id="new-username" required type="text" class="validate">
<label for="new-username">Username</label>
</div>
<div class="input-field directory-name-field col s12 m6">
@ -594,7 +630,7 @@ const usersView = Vue.component('users-view', {
title: 'You will be taken the login page',
position: 'center',
buttons: [['<button>Go!</button>', (instance, toast) => {
API.checkAuthAndKickToLogin();
API.logout();
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
}, true]],
});
@ -665,6 +701,12 @@ const advancedView = Vue.component('advanced-view', {
[<a v-on:click="openModal('edit-port-modal')">edit</a>]
</td>
</tr>
<tr>
<td><b>Max Request Size:</b> {{params.maxRequestSize}}</td>
<td>
[<a v-on:click="openModal('edit-request-size-modal')">edit</a>]
</td>
</tr>
<tr>
<td><b>Address:</b> {{params.address}}</td>
<td>
@ -676,11 +718,90 @@ const advancedView = Vue.component('advanced-view', {
</div>
</div>
</div>
<div class="col s12">
<div class="card">
<div v-if="!params.ssl || !params.ssl.cert">
<div class="card-content">
<span class="card-title">SSL Settings</span>
<a v-on:click="openModal('edit-ssl-modal')" class="waves-effect waves-light btn">Add SSL Certs</a>
</div>
</div>
<div v-else>
<div class="card-content">
<span class="card-title">SSL Settings</span>
<table>
<tbody>
<tr>
<td><b>Cert:</b> {{params.ssl.cert}}</td>
</tr>
<tr>
<td><b>Key:</b> {{params.ssl.key}}</td>
</tr>
</tbody>
</table>
</div>
<div class="card-action">
<a v-on:click="openModal('edit-ssl-modal')" class="waves-effect waves-light btn">Edit SSL</a>
<a v-on:click="removeSSL()" class="waves-effect waves-light btn">Remove SSL</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`,
methods: {
openModal: function(modalView) {
modVM.currentViewModal = modalView;
M.Modal.getInstance(document.getElementById('admin-modal')).open();
},
removeSSL: function() {
iziToast.question({
timeout: 20000,
close: false,
overlayClose: true,
overlay: true,
displayMode: 'once',
id: 'question',
zindex: 99999,
layout: 2,
maxWidth: 600,
title: 'Remove SSL Keys?',
message: 'Your server will need to reboot',
position: 'center',
buttons: [
[`<button><b>Remove SSL</b></button>`, async (instance, toast) => {
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
try {
await API.axios({
method: 'DELETE',
url: `${API.url()}/api/v1/admin/ssl`
});
setTimeout(() => {
window.location.href = window.location.href.replace('https://', 'http://');
}, 4000);
iziToast.success({
title: 'Certs Deleted. You will be redirected shortly',
position: 'topCenter',
timeout: 8500
});
} catch (err) {
iziToast.error({
title: 'Failed to Delete Cert',
position: 'topCenter',
timeout: 3500
});
}
}, true],
['<button>Go Back</button>', (instance, toast) => {
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
}],
]
});
},
openModal: function(modalView) {
modVM.currentViewModal = modalView;
M.Modal.getInstance(document.getElementById('admin-modal')).open();
@ -696,7 +817,7 @@ const advancedView = Vue.component('advanced-view', {
zindex: 99999,
layout: 2,
maxWidth: 600,
title: 'Generate a New Auth Key?',
title: '<b>Generate a New Auth Key?</b>',
message: 'All active login sessions will be invalidated. You will need to login after',
position: 'center',
buttons: [
@ -707,7 +828,7 @@ const advancedView = Vue.component('advanced-view', {
url: `${API.url()}/api/v1/admin/config/secret`,
data: { strength: 128 }
}).then(() => {
API.checkAuthAndKickToLogin();
API.logout();
}).catch(() => {
iziToast.error({
title: 'Failed',
@ -733,7 +854,7 @@ const advancedView = Vue.component('advanced-view', {
zindex: 99999,
layout: 2,
maxWidth: 600,
title: `${this.params.noUpload === false ? 'Disable' : 'Enable'} File Uploading?`,
title: `<b>${this.params.noUpload === false ? 'Disable' : 'Enable'} File Uploading?</b>`,
position: 'center',
buttons: [
[`<button><b>${this.params.noUpload === false ? 'Disable' : 'Enable'}</b></button>`, (instance, toast) => {
@ -820,6 +941,19 @@ const dbView = Vue.component('db-view', {
[<a v-on:click="toggleSkipImg()">edit</a>]
</td>
</tr>
<tr>
<td><b>Compress Images:</b> {{dbParams.compressImage}}</td>
<td>
[<a v-on:click="recompressImages()">re-compress</a>]
[<a v-on:click="toggleCompressImage()">edit</a>]
</td>
</tr>
<tr>
<td><b>Use New File Scanner:</b> {{dbParams.newScan}}</td>
<td>
[<a v-on:click="toggleNewScan()">edit</a>]
</td>
</tr>
<tr>
<td><b>Max Concurrent Scans:</b> {{dbParams.maxConcurrentTasks}}</td>
<td>
@ -877,7 +1011,7 @@ const dbView = Vue.component('db-view', {
<tr v-for="(v, k) in sharedPlaylists">
<th><a target="_blank" v-bind:href="'/shared/'+ v.playlistId">{{v.playlistId}}</a></th>
<th>{{v.user}}</th>
<th>{{v.expires}}</th>
<th>{{new Date(v.expires * 1000).toLocaleString()}}</th>
<th>[<a v-on:click="deletePlaylist(v)">delete</a>]</th>
</tr>
</tbody>
@ -1029,6 +1163,144 @@ const dbView = Vue.component('db-view', {
});
}
},
recompressImages: function() {
iziToast.question({
timeout: 20000,
close: false,
overlayClose: true,
overlay: true,
displayMode: 'once',
id: 'question',
zindex: 99999,
layout: 2,
maxWidth: 600,
title: `<b>Compress All Images?</b>`,
message: 'This process will run in the background',
position: 'center',
buttons: [
[`<button><b>Start</b></button>`, async (instance, toast) => {
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
try {
const res = await API.axios({
method: 'POST',
url: `${API.url()}/api/v1/admin/db/force-compress-images`,
});
if (res.data.started === true) {
iziToast.success({
title: 'Process Started',
position: 'topCenter',
timeout: 3500
});
} else {
iziToast.warning({
title: 'Image Compression In Progress',
position: 'topCenter',
timeout: 3500
});
}
} catch (err) {
iziToast.error({
title: 'Failed',
position: 'topCenter',
timeout: 3500
});
}
}, true],
['<button>Go Back</button>', (instance, toast) => {
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
}],
]
});
},
toggleNewScan: function() {
iziToast.question({
timeout: 20000,
close: false,
overlayClose: true,
overlay: true,
displayMode: 'once',
id: 'question',
zindex: 99999,
layout: 2,
maxWidth: 600,
title: `<b>${this.dbParams.newScan === true ? 'Disable' : 'Enable'} Use New Scanner?</b>`,
position: 'center',
buttons: [
[`<button><b>${this.dbParams.newScan === true ? 'Disable' : 'Enable'}</b></button>`, (instance, toast) => {
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
API.axios({
method: 'POST',
url: `${API.url()}/api/v1/admin/db/params/new-scan`,
data: { newScan: !this.dbParams.newScan }
}).then(() => {
// update fronted data
Vue.set(ADMINDATA.dbParams, 'newScan', !this.dbParams.newScan);
iziToast.success({
title: 'Updated Successfully',
position: 'topCenter',
timeout: 3500
});
}).catch(() => {
iziToast.error({
title: 'Failed',
position: 'topCenter',
timeout: 3500
});
});
}, true],
['<button>Go Back</button>', (instance, toast) => {
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
}],
]
});
},
toggleCompressImage: function() {
iziToast.question({
timeout: 20000,
close: false,
overlayClose: true,
overlay: true,
displayMode: 'once',
id: 'question',
zindex: 99999,
layout: 2,
maxWidth: 600,
title: `<b>${this.dbParams.compressImage === true ? 'Disable' : 'Enable'} Compress Images?</b>`,
position: 'center',
buttons: [
[`<button><b>${this.dbParams.compressImage === true ? 'Disable' : 'Enable'}</b></button>`, (instance, toast) => {
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
API.axios({
method: 'POST',
url: `${API.url()}/api/v1/admin/db/params/compress-image`,
data: { compressImage: !this.dbParams.compressImage }
}).then(() => {
// update fronted data
Vue.set(ADMINDATA.dbParams, 'compressImage', !this.dbParams.compressImage);
iziToast.success({
title: 'Updated Successfully',
position: 'topCenter',
timeout: 3500
});
}).catch(() => {
iziToast.error({
title: 'Failed',
position: 'topCenter',
timeout: 3500
});
});
}, true],
['<button>Go Back</button>', (instance, toast) => {
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
}],
]
});
},
toggleSkipImg: function() {
iziToast.question({
timeout: 20000,
@ -1040,7 +1312,7 @@ const dbView = Vue.component('db-view', {
zindex: 99999,
layout: 2,
maxWidth: 600,
title: `${this.dbParams.skipImg === true ? 'Disable' : 'Enable'} Image Skip?`,
title: `<b>${this.dbParams.skipImg === true ? 'Disable' : 'Enable'} Image Skip?</b>`,
position: 'center',
buttons: [
[`<button><b>${this.dbParams.skipImg === true ? 'Disable' : 'Enable'}</b></button>`, (instance, toast) => {
@ -1214,7 +1486,7 @@ const rpnView = Vue.component('rpn-view', {
const infoView = Vue.component('info-view', {
data() {
return {
version: ADMINDATA.version
};
},
template: `
@ -1226,12 +1498,19 @@ const infoView = Vue.component('info-view', {
<div class="col s12">
<div class="card">
<div class="card-content">
<span class="card-title">Developed & Designed By</span>
<h3>Paul Sori</h3>
<h4><a href="mailto:paul.sori@pm.me">paul.sori@pm.me</a></h4>
<blockquote>
<h5><b>I am currently looking for work!</b> Send me an email if you would like to hire me.</h5>
<h4><b>mStream v{{version.val}}</b></h4>
<h4>Developed By: Paul Sori</h4>
<h5><a href="mailto:paul.sori@pm.me">paul@mstream.io</a></h5>
</blockquote>
<br>
<div>
<iframe src="https://github.com/sponsors/IrosTheBeggar/button" title="Donate" height="35" width="200px" style="border: 0;"></iframe>
</div>
<br>
<a href="https://discord.gg/AM896Rr" target="_blank">
<svg style="max-height:70px;" viewBox="0 0 292 80" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0)"><g clip-path="url(#clip1)" fill="#5865F2"><path d="M61.796 16.494a59.415 59.415 0 00-15.05-4.73 44.128 44.128 0 00-1.928 4.003c-5.612-.844-11.172-.844-16.68 0a42.783 42.783 0 00-1.95-4.002 59.218 59.218 0 00-15.062 4.74C1.6 30.9-.981 44.936.31 58.772c6.317 4.717 12.44 7.583 18.458 9.458a45.906 45.906 0 003.953-6.51 38.872 38.872 0 01-6.225-3.03 30.957 30.957 0 001.526-1.208c12.004 5.615 25.046 5.615 36.906 0 .499.416 1.01.82 1.526 1.208a38.775 38.775 0 01-6.237 3.035 45.704 45.704 0 003.953 6.511c6.025-1.875 12.153-4.74 18.47-9.464 1.515-16.04-2.588-29.947-10.844-42.277zm-37.44 33.767c-3.603 0-6.558-3.363-6.558-7.46 0-4.096 2.892-7.466 6.559-7.466 3.666 0 6.621 3.364 6.558 7.466.006 4.097-2.892 7.46-6.558 7.46zm24.237 0c-3.603 0-6.558-3.363-6.558-7.46 0-4.096 2.892-7.466 6.558-7.466 3.667 0 6.622 3.364 6.558 7.466 0 4.097-2.891 7.46-6.558 7.46zM98.03 26.17h15.663c3.776 0 6.966.604 9.583 1.806 2.61 1.201 4.567 2.877 5.864 5.022 1.296 2.145 1.95 4.6 1.95 7.367 0 2.707-.677 5.163-2.031 7.36-1.354 2.204-3.414 3.944-6.185 5.228-2.771 1.283-6.203 1.928-10.305 1.928h-14.54V26.17zm14.378 21.414c2.542 0 4.499-.65 5.864-1.945 1.366-1.301 2.049-3.071 2.049-5.316 0-2.08-.609-3.739-1.825-4.98-1.216-1.243-3.058-1.87-5.52-1.87h-4.9v14.111h4.332zM154.541 54.846c-2.169-.575-4.126-1.407-5.864-2.503v-6.81c1.314 1.038 3.075 1.893 5.284 2.567 2.209.668 4.344 1.002 6.409 1.002.964 0 1.693-.128 2.186-.386.494-.258.741-.569.741-.926 0-.41-.132-.75-.402-1.026-.27-.275-.792-.504-1.566-.697l-4.82-1.108c-2.76-.656-4.717-1.565-5.881-2.73-1.165-1.161-1.745-2.685-1.745-4.572 0-1.588.505-2.965 1.527-4.143 1.015-1.178 2.461-2.087 4.337-2.725 1.877-.645 4.068-.967 6.587-.967 2.249 0 4.309.246 6.186.738 1.876.492 3.425 1.12 4.659 1.887v6.44c-1.263-.767-2.709-1.37-4.361-1.828a19.138 19.138 0 00-5.084-.674c-2.519 0-3.775.44-3.775 1.313 0 .41.195.715.585.92.39.205 1.107.416 2.146.639l4.016.738c2.623.463 4.579 1.278 5.864 2.438 1.286 1.16 1.928 2.878 1.928 5.152 0 2.49-1.061 4.465-3.19 5.93-2.129 1.465-5.147 2.198-9.06 2.198a26.36 26.36 0 01-6.707-.867zM182.978 53.984c-2.3-1.149-4.039-2.708-5.198-4.677-1.159-1.969-1.744-4.184-1.744-6.645 0-2.462.602-4.665 1.807-6.605 1.205-1.94 2.972-3.464 5.302-4.571 2.329-1.108 5.112-1.659 8.354-1.659 4.016 0 7.35.862 10.001 2.585v7.507c-.935-.656-2.026-1.19-3.271-1.6-1.245-.41-2.576-.615-3.999-.615-2.49 0-4.435.463-5.841 1.395-1.406.931-2.111 2.144-2.111 3.65 0 1.477.682 2.685 2.048 3.634 1.366.944 3.345 1.418 5.944 1.418 1.337 0 2.657-.2 3.959-.592 1.297-.398 2.416-.885 3.351-1.459v7.261c-2.943 1.805-6.357 2.707-10.242 2.707-3.27-.011-6.059-.586-8.36-1.734zM211.518 53.984c-2.318-1.148-4.085-2.72-5.302-4.718-1.216-1.998-1.83-4.225-1.83-6.686 0-2.462.608-4.66 1.83-6.587 1.222-1.928 2.978-3.44 5.285-4.536 2.3-1.096 5.049-1.641 8.233-1.641 3.185 0 5.933.545 8.234 1.64 2.301 1.097 4.057 2.597 5.262 4.513 1.205 1.917 1.807 4.114 1.807 6.605 0 2.461-.602 4.688-1.807 6.687-1.205 1.998-2.967 3.569-5.285 4.717-2.318 1.149-5.055 1.723-8.216 1.723-3.162 0-5.899-.568-8.211-1.717zm12.204-7.279c.976-.996 1.469-2.314 1.469-3.955s-.488-2.948-1.469-3.915c-.975-.973-2.307-1.46-3.993-1.46-1.716 0-3.059.487-4.04 1.46-.975.973-1.463 2.274-1.463 3.915 0 1.64.488 2.96 1.463 3.956.976.996 2.324 1.5 4.04 1.5 1.686-.006 3.018-.504 3.993-1.5zM259.17 31.34v8.86c-1.021-.685-2.341-1.025-3.976-1.025-2.141 0-3.793.662-4.941 1.986-1.153 1.325-1.727 3.388-1.727 6.177v7.548h-9.84V30.888h9.64v7.63c.533-2.79 1.4-4.846 2.593-6.176 1.188-1.325 2.725-1.987 4.596-1.987 1.417 0 2.634.328 3.655.985zM291.864 25.35v29.537h-9.841v-5.374c-.832 2.022-2.094 3.563-3.792 4.618-1.699 1.049-3.799 1.576-6.289 1.576-2.226 0-4.165-.55-5.824-1.658-1.658-1.108-2.937-2.626-3.838-4.554-.895-1.928-1.349-4.108-1.349-6.546-.028-2.514.448-4.77 1.429-6.769.976-1.998 2.358-3.557 4.137-4.676 1.779-1.12 3.81-1.682 6.088-1.682 4.688 0 7.832 2.08 9.438 6.235V25.35h9.841zm-11.309 21.191c1.004-.996 1.503-2.29 1.503-3.873 0-1.53-.488-2.778-1.463-3.733-.976-.956-2.313-1.436-3.994-1.436-1.658 0-2.983.486-3.976 1.46-.993.972-1.486 2.232-1.486 3.79 0 1.56.493 2.831 1.486 3.816.993.984 2.301 1.477 3.936 1.477 1.658-.006 2.989-.504 3.994-1.5zM139.382 33.443c2.709 0 4.906-2.015 4.906-4.5 0-2.486-2.197-4.501-4.906-4.501-2.71 0-4.906 2.015-4.906 4.5 0 2.486 2.196 4.501 4.906 4.501zM134.472 36.544c3.006 1.324 6.736 1.383 9.811 0v18.471h-9.811V36.544z"></path></g></g><defs><clipPath id="clip0"><path fill="#fff" transform="translate(0 11.765)" d="M0 0h292v56.471H0z"></path></clipPath><clipPath id="clip1"><path fill="#fff" transform="translate(0 11.765)" d="M0 0h292v56.471H0z"></path></clipPath></defs></svg>
</a>
</div>
</div>
</div>
@ -1330,6 +1609,12 @@ const transcodeView = Vue.component('transcode-view', {
[<a v-on:click="changeBitrate()">edit</a>]
</td>
</tr>
<tr>
<td><b>Default Algorithm:</b> {{params.algorithm}}</td>
<td>
[<a v-on:click="changeAlgorithm()">edit</a>]
</td>
</tr>
</tbody>
</table>
</div>
@ -1349,7 +1634,7 @@ const transcodeView = Vue.component('transcode-view', {
zindex: 99999,
layout: 2,
maxWidth: 600,
title: `${this.params.enabled === true ? 'Disable' : 'Enable'} Transcoding?`,
title: `<b>${this.params.enabled === true ? 'Disable' : 'Enable'} Transcoding?</b>`,
message: 'Enabling this will download FFmpeg',
position: 'center',
buttons: [
@ -1393,6 +1678,10 @@ const transcodeView = Vue.component('transcode-view', {
modVM.currentViewModal = 'edit-transcode-bitrate-modal';
M.Modal.getInstance(document.getElementById('admin-modal')).open();
},
changeAlgorithm: function() {
modVM.currentViewModal = 'edit-transcode-algorithm-modal';
M.Modal.getInstance(document.getElementById('admin-modal')).open();
},
downloadFFMpeg: async function() {
if (this.downloadPending.val === true) {
return;
@ -1809,7 +2098,7 @@ const logsView = Vue.component('logs-view', {
zindex: 99999,
layout: 2,
maxWidth: 600,
title: `${this.params.writeLogs === true ? 'Disable' : 'Enable'} Writing Logs To Disk?`,
title: `<b>${this.params.writeLogs === true ? 'Disable' : 'Enable'} Writing Logs To Disk?</b>`,
position: 'center',
buttons: [
[`<button><b>${this.params.writeLogs === true ? 'Disable' : 'Enable'}</b></button>`, (instance, toast) => {
@ -1856,7 +2145,7 @@ const lockView = Vue.component('lock-view', {
This will prevent anyone from making configuration changes with the Admin Panel. If you want undo this you will need to:
<br><br>
-- Open the config file<br>
-- Change the value of 'lockAdmin' to 'true'<br>
-- Change the value of 'lockAdmin' to 'false'<br>
-- Reboot mStream
</p>
<br>
@ -1875,7 +2164,7 @@ const lockView = Vue.component('lock-view', {
zindex: 99999,
layout: 2,
maxWidth: 600,
title: 'Disable Admin Panel?',
title: '<b>Disable Admin Panel?</b>',
position: 'center',
buttons: [
[`<button><b>Disable</b></button>`, (instance, toast) => {
@ -1946,6 +2235,7 @@ const fileExplorerModal = Vue.component('file-explorer-modal', {
componentKey: false, // Flip this value to force re-render,
pending: false,
currentDirectory: null,
winDrives: ADMINDATA.winDrives,
contents: []
};
},
@ -1959,11 +2249,16 @@ const fileExplorerModal = Vue.component('file-explorer-modal', {
[<a v-on:click="goToDirectory(currentDirectory)">refresh</a>]
</span>
</div>
<div v-show="currentDirectory === null || pending === true" class="row">
<div v-if="currentDirectory === null || pending === true" class="row">
<svg class="spinner" width="65px" height="65px" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg"><circle class="spinner-path" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33" r="30"></circle></svg>
</div>
<div v-show="currentDirectory !== null" class="row">
<h6>{{currentDirectory}}</h6>
<div v-else="currentDirectory !== null" class="row">
<div class="flex">
<select @change="goToDirectory($event.target.value)" v-if="winDrives.length > 0" id="select-win-drive" class="browser-default">
<option v-for="(value) in winDrives" :selected="currentDirectory.startsWith(value)" :value="value">{{ value }}</option>
</select>
<h6>{{currentDirectory}}</h6>
</div>
[<a v-on:click="selectDirectory(currentDirectory)">Select Current Directory</a>]
<ul class="collection">
<li v-on:click="goToDirectory(currentDirectory, dir.name)" v-for="dir in contents" class="collection-item">
@ -2238,6 +2533,74 @@ const userAccessView = Vue.component('user-access-view', {
}
});
const editRequestSizeModal = Vue.component('edit-request-size-modal', {
data() {
return {
params: ADMINDATA.serverParams,
submitPending: false,
maxRequestSize: ADMINDATA.serverParams.maxRequestSize
};
},
template: `
<form @submit.prevent="updatePort">
<div class="modal-content">
<h4>Change Max Request Size</h4>
<p>Accepts KB or MB</p>
<div class="input-field">
<input v-model="maxRequestSize" id="edit-max-request-size" required type="text">
<label for="edit-port">Edit Max Request Size</label>
</div>
<blockquote>
Requires a reboot.
</blockquote>
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn-flat">Go Back</a>
<button class="btn green waves-effect waves-light" type="submit" :disabled="submitPending === true">
{{submitPending === false ? 'Update' : 'Updating...'}}
</button>
</div>
</form>`,
mounted: function () {
M.updateTextFields();
},
methods: {
updatePort: async function() {
try {
this.submitPending = true;
this.maxRequestSize = this.maxRequestSize.replaceAll(' ', '');
await API.axios({
method: 'POST',
url: `${API.url()}/api/v1/admin/config/max-request-size`,
data: { maxRequestSize: this.maxRequestSize }
});
// update fronted data
Vue.set(ADMINDATA.serverParams, 'maxRequestSize', this.maxRequestSize);
// close & reset the modal
M.Modal.getInstance(document.getElementById('admin-modal')).close();
iziToast.success({
title: 'Success: Allow the server 30 seconds to reboot',
position: 'topCenter',
timeout: 3500
});
} catch(err) {
iziToast.error({
title: 'Failed to Update',
position: 'topCenter',
timeout: 3500
});
}finally {
this.submitPending = false;
}
}
}
});
const editPortModal = Vue.component('edit-port-modal', {
data() {
return {
@ -2280,13 +2643,17 @@ const editPortModal = Vue.component('edit-port-modal', {
});
// update fronted data
Vue.set(ADMINDATA.serverParams, 'port', this.currentPort);
// Vue.set(ADMINDATA.serverParams, 'port', this.currentPort);
// close & reset the modal
M.Modal.getInstance(document.getElementById('admin-modal')).close();
setTimeout(() => {
window.location.href = window.location.href.replace(`:${ADMINDATA.serverParams.port}`, `:${this.currentPort}`);
}, 4000);
iziToast.success({
title: 'Port Updated. Server is rebooting',
title: 'Port Updated. You will be redirected shortly',
position: 'topCenter',
timeout: 3500
});
@ -2319,6 +2686,10 @@ const editAddressModal = Vue.component('edit-address-modal', {
<input v-model="editValue" id="edit-server-address" required type="text">
<label for="edit-server-address">Server Address</label>
</div>
<blockquote>
Requires a Reboot<br>
<b>Don't edit this unless you know what you're doing</b>
</blockquote>
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn-flat">Go Back</a>
@ -2381,6 +2752,9 @@ const editMaxScanModal = Vue.component('edit-max-scans-modal', {
<input v-model="editValue" id="edit-max-scans" required type="number" min="1">
<label for="edit-max-scans">Edit Max Scans</label>
</div>
<blockquote>
<b>Using a value more than '1' is experimental</b>
</blockquote>
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn-flat">Go Back</a>
@ -2676,6 +3050,76 @@ const editScanIntervalView = Vue.component('edit-scan-interval-modal', {
}
});
const editSslModal = Vue.component('edit-ssl-modal', {
data() {
return {
certPath: '',
keyPath: '',
submitPending: false
};
},
template: `
<form @submit.prevent="updateSSL">
<div class="modal-content">
<h4>Set SSL Files</h4>
<div class="input-field">
<input v-model="certPath" id="edit-ssl-cert" required type="text">
<label for="edit-ssl-cert">Cert File Path</label>
</div>
<div class="input-field">
<input v-model="keyPath" id="edit-ssl-key" required type="text">
<label for="edit-ssl-key">Key File Path</label>
</div>
<blockquote>
Requires a Reboot
</blockquote>
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn-flat">Go Back</a>
<button class="btn green waves-effect waves-light" type="submit" :disabled="submitPending === true">
{{submitPending === false ? 'Update' : 'Updating...'}}
</button>
</div>
</form>`,
methods: {
updateSSL: async function() {
try {
this.submitPending = true;
await API.axios({
method: 'POST',
url: `${API.url()}/api/v1/admin/ssl`,
data: { cert: this.certPath, key: this.keyPath }
});
// update fronted data
Vue.set(ADMINDATA.dbParams, 'scanInterval', this.editValue);
// close & reset the modal
M.Modal.getInstance(document.getElementById('admin-modal')).close();
setTimeout(() => {
window.location.href = window.location.href.replace('http://', 'https://');
}, 4000);
iziToast.success({
title: 'Updated Successfully. You will be redirected shortly',
position: 'topCenter',
timeout: 3500
});
} catch(err) {
iziToast.error({
title: 'Update Failed',
position: 'topCenter',
timeout: 3500
});
} finally {
this.submitPending = false;
}
}
}
});
const editTranscodeCodecModal = Vue.component('edit-transcode-codec-modal', {
data() {
return {
@ -2743,6 +3187,75 @@ const editTranscodeCodecModal = Vue.component('edit-transcode-codec-modal', {
}
});
const editTranscodeDefaultAlgorithm = Vue.component('edit-transcode-algorithm-modal', {
data() {
return {
params: ADMINDATA.transcodeParams,
submitPending: false,
editValue: ADMINDATA.transcodeParams.algorithm,
selectInstance: null
};
},
template: `
<form @submit.prevent="updateParam">
<div class="modal-content">
<h4>Set Default Algorithm</h4>
<select v-model="editValue" id="transcode-algorithm-dropdown">
<option value="buffer">Buffer</option>
<option value="stream">Stream</option>
</select>
<blockquote>
<b>Buffer</b> takes longer to load and uses more memory, but it works on everything. <b>Stream</b> starts instantaneously, but it might not work on every device
</blockquote>
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn-flat">Go Back</a>
<button class="btn green waves-effect waves-light" type="submit" :disabled="submitPending === true">
{{submitPending === false ? 'Update' : 'Updating...'}}
</button>
</div>
</form>`,
mounted: function () {
this.selectInstance = M.FormSelect.init(document.querySelectorAll("#transcode-algorithm-dropdown"));
},
beforeDestroy: function() {
this.selectInstance[0].destroy();
},
methods: {
updateParam: async function() {
try {
this.submitPending = true;
await API.axios({
method: 'POST',
url: `${API.url()}/api/v1/admin/transcode/default-algorithm`,
data: { algorithm: this.editValue }
});
// update fronted data
Vue.set(ADMINDATA.transcodeParams, 'algorithm', this.editValue);
// close & reset the modal
M.Modal.getInstance(document.getElementById('admin-modal')).close();
iziToast.success({
title: 'Updated Successfully',
position: 'topCenter',
timeout: 3500
});
} catch(err) {
iziToast.error({
title: 'Update Failed',
position: 'topCenter',
timeout: 3500
});
}finally {
this.submitPending = false;
}
}
}
});
const editTranscodeDefaultBitrate = Vue.component('edit-transcode-bitrate-modal', {
data() {
return {
@ -2931,14 +3444,17 @@ const modVM = new Vue({
'user-access-modal': userAccessView,
'file-explorer-modal': fileExplorerModal,
'edit-port-modal': editPortModal,
'edit-request-size-modal': editRequestSizeModal,
'edit-address-modal': editAddressModal,
'edit-scan-interval-modal': editScanIntervalView,
'edit-save-interval-modal': editSaveIntervalView,
'edit-boot-scan-delay-modal': editBootScanView,
'edit-select-codec-modal': editTranscodeCodecModal,
'edit-transcode-bitrate-modal': editTranscodeDefaultBitrate,
'edit-transcode-algorithm-modal': editTranscodeDefaultAlgorithm,
'edit-pause-modal': editPauseModal,
'edit-max-scan-modal': editMaxScanModal,
'edit-ssl-modal': editSslModal,
'lastfm-modal': lastFMModal,
'federation-generate-invite-modal': federationGenerateInvite,
'null-modal': nullModal

155
webapp/alpha/api.js Normal file
View File

@ -0,0 +1,155 @@
const MSTREAMAPI = (() => {
let mstreamModule = {};
mstreamModule.listOfServers = [];
mstreamModule.currentServer = {
host: "",
username: "",
token: "",
vpaths: []
};
async function req(type, url, dataObject) {
const res = await fetch(url, {
method: type,
headers: {
'Content-Type': 'application/json',
'x-access-token': MSTREAMAPI.currentServer.token
// 'Content-Type': 'application/x-www-form-urlencoded',
},
body: dataObject ? JSON.stringify(dataObject) : undefined
});
if (res.ok !== true) {
throw new Error(res);
}
return await res.json();
}
mstreamModule.dirparser = (directory) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/file-explorer', { directory: directory });
}
mstreamModule.loadFileplaylist = (path) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/file-explorer/m3u', { path });
}
mstreamModule.recursiveScan = (directory) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/file-explorer/recursive', { directory: directory });
}
mstreamModule.savePlaylist = (title, songs, live) => {
const postData = { title: title, songs: songs };
if (live !== undefined) {
postData.live = live;
}
return req('POST', mstreamModule.currentServer.host + 'api/v1/playlist/save', postData);
}
mstreamModule.newPlaylist = (title) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/playlist/new', { title: title });
}
mstreamModule.deletePlaylist = (playlistname) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/playlist/delete', { playlistname: playlistname });
}
mstreamModule.removePlaylistSong = (lokiId) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/playlist/remove-song', { lokiid: lokiId });
}
mstreamModule.loadPlaylist = (playlistname) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/playlist/load', { playlistname: playlistname });
}
mstreamModule.getAllPlaylists = () => {
return req('GET', mstreamModule.currentServer.host + 'api/v1/playlist/getall', false);
}
mstreamModule.addToPlaylist = (playlist, song) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/playlist/add-song', { playlist: playlist, song: song });
}
mstreamModule.search = (postObject) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/db/search', postObject);
}
mstreamModule.artists = (postObject) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/db/artists', postObject);
}
mstreamModule.albums = (postObject) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/db/albums', postObject);
}
mstreamModule.artistAlbums = (postObject) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/artists-albums", postObject);
}
mstreamModule.albumSongs = (postObject) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/album-songs", postObject);
}
mstreamModule.dbStatus = () => {
return req('GET', mstreamModule.currentServer.host + "api/v1/db/status", false);
}
mstreamModule.makeShared = (playlist, shareTimeInDays) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/share", { time: shareTimeInDays, playlist: playlist });
}
mstreamModule.rateSong = (filepath, rating) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/rate-song", { filepath: filepath, rating: rating });
}
mstreamModule.getRated = (postObject) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/rated", postObject);
}
mstreamModule.getRecentlyAdded = (limit, ignoreVPaths) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/recent/added", { limit: limit, ignoreVPaths });
}
mstreamModule.getRecentlyPlayed = (limit, ignoreVPaths) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/stats/recently-played", { limit: limit, ignoreVPaths });
}
mstreamModule.getMostPlayed = (limit, ignoreVPaths) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/stats/most-played", { limit: limit, ignoreVPaths });
}
mstreamModule.lookupMetadata = (filepath) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/metadata", { filepath: filepath });
}
mstreamModule.getRandomSong = (postObject) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/random-songs", postObject);
}
// Scrobble
mstreamModule.scrobbleByMetadata = (artist, album, trackName) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/lastfm/scrobble-by-metadata", { artist: artist, album: album, track: trackName });
}
mstreamModule.scrobbleByFilePath = (filePath) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/lastfm/scrobble-by-filepath", { filePath });
}
// LOGIN
mstreamModule.login = (username, password, url) => {
return req('POST', url ? url + "api/v1/auth/login" : "api/v1/auth/login", { username: username, password: password });
}
mstreamModule.ping = () => {
return req('GET', mstreamModule.currentServer.host + "api/v1/ping", false);
}
mstreamModule.logout = () => {
localStorage.removeItem('token');
Cookies.remove('x-access-token');
document.location.assign(window.location.href + (window.location.href.slice(-1) === '/' ? '' : '/') + 'login');
}
return mstreamModule;
})();

View File

@ -1,180 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>mStream Music</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="apple-touch-icon" sizes="180x180" href="../assets/fav/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="../assets/fav/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="../assets/fav/favicon-16x16.png">
<link rel="manifest" href="../assets/fav/site.webmanifest">
<link rel="mask-icon" href="../assets/fav/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="../assets/fav/favicon.ico">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="../assets/fav/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<!-- Fonts -->
<link href="../assets/fonts/jura.css" rel="stylesheet">
<!-- Style Sheets -->
<link rel="stylesheet" href="spa.css">
<link rel="stylesheet" href="../assets/css/waves.css">
<link rel="stylesheet" href="../assets/css/materialize.css">
<!-- JS -->
<script defer src="spa.js"></script>
<script defer src="index.js"></script>
<script defer src="../assets/js/waves.js"></script>
<script src="vue3.js"></script>
<script src="../assets/js/lib/axios.js"></script>
<script src="../assets/js/api.js"></script>
<!-- <script>API.checkAuthAndKickToLogin();</script> -->
</head>
<body>
<div id="sidenav">
<div class="side-nav-header">
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 612 153" style="enable-background:new 0 0 612 153;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#6684B2;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#6684B2;}
.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#26477B;}
</style>
<g>
<path class="st0" d="M179.9,45.5c-6.2,0-11.5,1.7-15.9,5c-4.4,3.3-6.5,8.1-6.5,14.4c0,4.9,1.3,9.1,3.8,12.4
c2.5,3.4,5.7,5.8,9.3,7.3c3.7,1.5,7.3,2.8,11,3.8c3.7,1,6.8,2.3,9.3,3.9c2.5,1.5,3.8,3.5,3.8,5.8c0,4.8-4.4,7.2-13.1,7.2h-24.1V118
h24.1c17.1,0,25.6-6.7,25.6-20.2c0-1.9-0.2-3.8-0.6-5.8c-0.4-2-1.2-4-2.6-6c-1.3-2.1-3.3-3.7-5.8-4.9c-2.5-1.2-6.4-2.7-11.5-4.5
l-8.8-3.1c-0.7-0.2-1.7-0.7-2.9-1.3c-1.3-0.7-2.2-1.3-2.8-1.9c-0.6-0.6-1.1-1.4-1.6-2.3c-0.5-0.9-0.7-2-0.7-3.2c0-2,1-3.5,2.9-4.6
c1.9-1.1,4.3-1.6,7-1.6h24.6V45.5H179.9z"/>
<path class="st0" d="M226.4,58.3v31c0,10.2,2.5,17.6,7.6,22c5.1,4.4,13,6.6,23.7,6.6v-12.8c-2.7,0-4.9-0.2-6.8-0.4
c-1.8-0.3-3.7-0.9-5.8-1.9c-2-0.9-3.6-2.6-4.7-4.9c-1.1-2.3-1.6-5.2-1.6-8.7V58.3h18.8V45.5h-18.8V31.6L214,58.3H226.4z"/>
<path class="st0" d="M281.1,118V76.8c0-7.2,0.9-12,2.6-14.5c1-1.3,2.2-2.2,3.6-2.8c1.4-0.6,2.6-1,3.6-1.1c1-0.1,2.5-0.1,4.3-0.1
H310V45.5h-12.2c-3.6,0-6.5,0.1-8.6,0.3c-2.1,0.2-4.5,0.9-7.3,2c-2.8,1.1-5.1,2.8-7.1,5c-4,4.4-6,12.4-6,24V118H281.1z"/>
<path class="st0" d="M326.2,53.8c-6.2,7.4-9.3,17-9.3,28.9c0,10.7,3.2,19.4,9.5,26.2s14.7,10.1,25.3,10.1c8.7,0,16.3-2.7,22.7-8.1
L366,102c-3.7,2.1-8.5,3.2-14.3,3.2c-6.5,0-11.8-2.3-15.8-6.9c-4-4.6-6-10.5-6-17.9c0-7,1.9-12.9,5.6-17.9c3.8-5,8.9-7.5,15.5-7.5
c3.3,0,6.1,0.8,8.2,2.4c2.1,1.6,3.2,4,3.2,7.2c0,5-1.2,8.5-3.6,10.6c-2.4,2.1-6.7,3.2-12.9,3.2h-6.7v11.7h5.7
c20.3,0,30.5-8.5,30.5-25.4c0-13.6-7.9-20.7-23.7-21.5C340.9,43,332.4,46.5,326.2,53.8z"/>
<path class="st0" d="M412.3,73.2c-7.4,0-13.6,1.9-18.5,5.7c-4.9,3.8-7.4,9.4-7.4,16.7c0,7.3,2.3,12.9,7,16.7
c4.6,3.8,10.9,5.7,18.8,5.7h31V73.6c0-9.1-2.4-16-7.2-20.8c-4.8-4.8-11.7-7.2-20.7-7.2h-22.9v12.8h22.3c10.9,0,16.4,6.1,16.4,18.2
v28.7h-18.4c-9.1,0-13.6-3.2-13.6-9.8c0-3.3,1.2-5.9,3.6-7.8c2.4-1.8,5.8-2.7,10.2-2.7c5.1,0,9.4,1.4,12.9,4.3V75.3
C420.9,73.9,416.5,73.2,412.3,73.2z"/>
<path class="st0" d="M458.8,118H471V58.3h24.4V118h12.2V58.3h5.7c6.8,0,11.3,0.7,13.5,2c4.3,2.5,6.5,7.7,6.5,15.5V118h12.2V75.7
c0-6-0.6-11.2-1.9-15.5c-1.2-4.3-3.9-7.8-7.9-10.6c-3.9-2.7-9.1-4.1-15.7-4.1h-61.4V118z"/>
<polygon class="st1" points="75,118.5 75,35.5 96,48.5 96,118.5 "/>
<polygon class="st2" points="99,118.5 99,49.5 110.5,56.5 121,49.5 121,118.5 "/>
<polygon class="st1" points="124,118.5 124,48.5 145,35.5 145,118.5 "/>
</g>
</svg>
</div>
<div class="side-nav-header">
<button class="my-waves" onclick="changeView('now-playing-view', this)">Now Playing</button>
</div>
<div class="side-nav-header">
<span>Music</span>
</div>
<div class="side-nav-item select my-waves" onclick="changeView('file-explorer-view', this)">
<?xml version="1.0" encoding="utf-8"?><svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="6 6 40 40" style="enable-background:new 0 0 48 48"><path d="M16.516 20.688C16.266 21.25 12 31.906 12 31.906V17c0-.55.45-1 1-1h1.334l.35-1.052C14.857 14.427 15.45 14 16 14h5c.55 0 1.143.427 1.316.948l.35 1.052H32c.55 0 1 .45 1 1v3H17.5c-.275 0-.734.125-.984.688zM41 21H19c-.55 0-1.167.418-1.371.929l-5.258 13.143c-.204.51.079.928.629.928h22c.55 0 1.167-.418 1.371-.929l5.258-13.143c.204-.51-.079-.928-.629-.928z"/></svg>
<span>File Explorer</span>
</div>
<div class="side-nav-item my-waves">
<?xml version="1.0" encoding="utf-8"?><svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="8 5 38 38" style="enable-background:new 0 0 48 48"><path d="M37.192 23.032c-.847.339-.179-.339-.179-.339s1.422-2.092.406-3.786c-.793-1.321-3.338-1.075-4.42-1.669v14.154c0 .037-.016.07-.022.106-.154 1.504-1.607 3.034-3.696 3.712-2.559.829-5.102.063-5.678-1.711-.574-1.774 1.034-3.887 3.595-4.717.66-.189 2.207-.439 2.801-.193V12.607a.609.609 0 0 1 .608-.607h1.785c.336 0 .608.273.608.607v.549c1.542 1.004 6.18 1.455 6.851 4.139.805 3.225-1.813 5.398-2.659 5.737zM12.5 20H28v-3H12.5c-.275 0-.5.225-.5.5v2c0 .275.225.5.5.5zm0 6H28v-3H12.5c-.275 0-.5.225-.5.5v2c0 .275.225.5.5.5zm10.125 3H12.5c-.275 0-.5.225-.5.5v2c0 .275.225.5.5.5h8.551c.176-1.075.728-2.113 1.574-3z"/></svg>
<span>Playlists</span>
</div>
<div class="side-nav-item my-waves">
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="10 12 28 28" style="enable-background:new 0 0 48 48"><path d="M11 17v14a1 1 0 0 1-1-1V18a1 1 0 0 1 1-1zM37 17v14a1 1 0 0 0 1-1V18a1 1 0 0 0-1-1zM13 16h1v16h-1a1 1 0 0 1-1-1V17a1 1 0 0 1 1-1zM35 16h-1v16h1a1 1 0 0 0 1-1V17a1 1 0 0 0-1-1zM32 15H16c-.55 0-1 .45-1 1v16c0 .55.45 1 1 1h16c.55 0 1-.45 1-1V16c0-.55-.45-1-1-1zm-3 12c0 1.469-2.022 1.71-2.301 1.71-.846 0-1.55-.395-1.752-1.02-.276-.852.461-1.796 1.607-2.218.649-.238 1.446-.155 1.446-.146v-4.407l-6 1.369V28c0 1.813-2.102 2.057-2.417 2.057-.816 0-1.44-.374-1.629-.955-.271-.835.441-1.77 1.603-2.174.646-.225 1.443-.137 1.443-.131v-6.604c0-.245.072-.469.291-.529l7.491-1.854-.039-.01c.218 0 .257.169.257.393V27z"/></svg>
<span>Albums</span>
</div>
<div class="side-nav-item my-waves">
<?xml version="1.0" encoding="utf-8"?><svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><path d="M611.4 34.4c92-46.8 206.7-25.7 276.3 58.1 66.9 80.5 71.4 193.8 18.5 278.4-13.2.1-27-2.3-41.2-6.6-56-17.1-114.7-64.1-161.8-122.1-47.3-58.2-82-126-90.1-184.6-1-8-1.6-15.7-1.7-23.2zM146.8 678c-.7 41.6 19.8 64.6 59 71l431.9-297.1C578.1 413.3 527.3 349.7 501 282.7L146.8 678zm14.1 94.3c-35.9 38.4-46.8 78.1-38.3 107.7 3.5 12.1 10.4 22.5 20.2 30.3 10.4 8.3 24.5 13.8 41.6 15.3 58.5 5.1 146-33.6 248.3-150.9 102.8-118 195.4-173.9 271.3-187.5 45.9-8.2 86.3-1.4 119.7 16.6 33.4 18 59.4 47 76.7 83.1 27 56.4 32.6 130.2 12 205.2l-60-17.2c16.3-59.8 12.6-117.4-7.9-160-11.5-24-28.4-43.1-49.8-54.6-21.5-11.5-48.3-15.8-79.9-10.1-63.2 11.4-143 61-235.4 167-117.5 134.9-224.8 178.7-300 172.2-29.6-2.6-54.8-12.8-74.6-28.4-20.4-16.1-34.7-37.9-41.9-63.2-14.3-50.1.3-113.2 53.1-169.7l44.9 44.2zM871.5 412c-69.8-23.6-139.6-79.4-194.4-146.8-49.9-61.3-88.3-133.3-103.8-200.3-41 51.7-57.7 118-48.6 181.6 25.1 77.2 90.1 157.1 162.7 191.2 60.8 18 127.9 10 184.1-25.7z"/></svg>
<span>Artists</span>
</div>
<div class="side-nav-item my-waves">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/><path d="M0 0h24v24H0z" fill="none"/><path d="M12.5 7H11v6l5.25 3.15.75-1.23-4.5-2.67z"/></svg>
<span>Recent</span>
</div>
<div class="side-nav-item my-waves">
<?xml version="1.0" encoding="iso-8859-1"?><svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="-3 0 62 62" style="enable-background:new 0 0 53.867 53.867"><path style="fill:#efce4a" d="M26.934 1.318l8.322 16.864 18.611 2.705L40.4 34.013l3.179 18.536-16.645-8.751-16.646 8.751 3.179-18.536L0 20.887l18.611-2.705z"/></svg>
<span>Rated</span>
</div>
<div class="side-nav-item my-waves">
<svg viewBox="-150 -50 1224 1174" height="24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M960 832L710.875 582.875C746.438 524.812 768 457.156 768 384 768 171.969 596 0 384 0 171.969 0 0 171.969 0 384c0 212 171.969 384 384 384 73.156 0 140.812-21.562 198.875-57L832 960c17.5 17.5 46.5 17.375 64 0l64-64c17.5-17.5 17.5-46.5 0-64zM384 640c-141.375 0-256-114.625-256-256s114.625-256 256-256 256 114.625 256 256-114.625 256-256 256z"></path></svg>
<span>Search</span>
</div>
<div class="side-nav-header">
<span>System</span>
</div>
<div class="side-nav-item my-waves">
<svg xmlns="http://www.w3.org/2000/svg" fill="#DDD" width="24" height="24" viewBox="0 0 55.334 55.334"><g><circle cx="27.667" cy="27.667" r="3.618"/><path d="M27.667 0C12.387 0 0 12.387 0 27.667s12.387 27.667 27.667 27.667 27.667-12.387 27.667-27.667S42.947 0 27.667 0zM17.118 6.881a23.213 23.213 0 0111.214-2.509c.367.01.619.922.564 2.025l-.282 5.677c-.055 1.103-.289 1.986-.523 1.979a13.577 13.577 0 00-6.027 1.196c-1.007.455-2.212.184-2.774-.767l-2.896-4.897c-.562-.951-.261-2.203.724-2.704zm-1.132 10.414l-4.278-3.742c-.832-.727-.918-1.994-.119-2.756l.057-.053c.802-.76 2.059-.605 2.737.266l3.494 4.484c.679.871.837 1.889.391 2.314-.447.427-1.45.214-2.282-.513zm1.891 10.372c0-5.407 4.383-9.79 9.79-9.79s9.79 4.383 9.79 9.79-4.383 9.79-9.79 9.79-9.79-4.383-9.79-9.79zM38.17 48.476a23.21 23.21 0 01-11.244 2.484c-.409-.013-.692-.929-.632-2.032l.31-5.676c.061-1.103.322-1.981.586-1.972a13.596 13.596 0 005.656-1.01c1.022-.42 2.275-.144 2.877.782l3.101 4.77c.602.925.332 2.155-.654 2.654zm5.449-3.82c-.766.72-2.005.551-2.703-.305l-3.59-4.407c-.698-.856-.876-1.848-.435-2.255.442-.407 1.443-.179 2.274.549l4.28 3.744c.832.727.941 1.954.174 2.674z"/></g></svg>
<span>Auto DJ</span>
</div>
<div class="side-nav-item my-waves">
<svg viewBox="-140 140 1024 768" height="24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M384 960C171.969 960 0 902.625 0 832V704c0-11.125 5.562-21.688 13.562-32C56.375 727.125 205.25 768 384 768s327.625-40.875 370.438-96c8 10.312 13.562 20.875 13.562 32v128c0 70.625-172 128-384 128zm0-256C171.969 704 0 646.625 0 576V448c0-6.781 2.562-13.375 6-19.906C7.938 424 10.5 419.969 13.562 416 56.375 471.094 205.25 512 384 512s327.625-40.906 370.438-96c3.062 3.969 5.625 8 7.562 12.094 3.438 6.531 6 13.125 6 19.906v128c0 70.625-172 128-384 128zm0-256C171.969 448 0 390.656 0 320v-64-64C0 121.344 171.969 64 384 64c212 0 384 57.344 384 128v128c0 70.656-172 128-384 128zm0-320c-141.375 0-256 28.594-256 64s114.625 64 256 64 256-28.594 256-64-114.625-64-256-64z"/></svg>
<span>Database</span>
</div>
<div class="side-nav-item my-waves">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#DDD" width="24" height="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M20.2 5.9l.8-.8C19.6 3.7 17.8 3 16 3s-3.6.7-5 2.1l.8.8C13 4.8 14.5 4.2 16 4.2s3 .6 4.2 1.7zm-.9.8c-.9-.9-2.1-1.4-3.3-1.4s-2.4.5-3.3 1.4l.8.8c.7-.7 1.6-1 2.5-1 .9 0 1.8.3 2.5 1l.8-.8zM19 13h-2V9h-2v4H5c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-4c0-1.1-.9-2-2-2zM8 18H6v-2h2v2zm3.5 0h-2v-2h2v2zm3.5 0h-2v-2h2v2z"/></svg>
<span>Servers</span>
</div>
<div class="side-nav-item my-waves" onclick="window.open('./admin', '_blank');">
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M17,11c0.34,0,0.67,0.04,1,0.09V6.27L10.5,3L3,6.27v4.91c0,4.54,3.2,8.79,7.5,9.82c0.55-0.13,1.08-0.32,1.6-0.55 C11.41,19.47,11,18.28,11,17C11,13.69,13.69,11,17,11z"/><path d="M17,13c-2.21,0-4,1.79-4,4c0,2.21,1.79,4,4,4s4-1.79,4-4C21,14.79,19.21,13,17,13z M17,14.38c0.62,0,1.12,0.51,1.12,1.12 s-0.51,1.12-1.12,1.12s-1.12-0.51-1.12-1.12S16.38,14.38,17,14.38z M17,19.75c-0.93,0-1.74-0.46-2.24-1.17 c0.05-0.72,1.51-1.08,2.24-1.08s2.19,0.36,2.24,1.08C18.74,19.29,17.93,19.75,17,19.75z"/></g></g></svg>
<span>Admin Panel</span>
</div>
<div class="side-nav-item my-waves" onclick="API.logout();">
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><rect fill="none" height="24" width="24"/><path d="M9,19l1.41-1.41L5.83,13H22V11H5.83l4.59-4.59L9,5l-7,7L9,19z"/></svg>
<span>Log Out</span>
</div>
</div>
<div id="sidenav-cover" class="click-through"></div>
<div id="content">
<div id="content-header" class="container">
<h5>File Explorer</h5>
<div id="content-header-spacer"></div>
<svg id="sidenav-button" @click="toggleMenu" class="ham hamRotate180 ham5" viewBox="0 0 100 100" width="80">
<path class="line top" d="m 30,33 h 40 c 0,0 8.5,-0.68551 8.5,10.375 0,8.292653 -6.122707,9.002293 -8.5,6.625 l -11.071429,-11.071429" />
<path class="line middle" d="m 70,50 h -40" />
<path class="line bottom" d="m 30,67 h 40 c 0,0 8.5,0.68551 8.5,-10.375 0,-8.292653 -6.122707,-9.002293 -8.5,-6.625 l -11.071429,11.071429" />
</svg>
</div>
<!-- componentKey is used to force a re-render -->
<component :is="currentViewMain">
</component>
</div>
<div id="nav-bar">
<div id="media-player-spacer"></div>
<div class="progress pointer" ref="progressWrapper">
<div class="determinate" ></div>
</div>
<div id="media-player-button-bar">
<div class="media-player-button" id="previous-button">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="#FFF"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
</div>
<div class="media-player-button" id="play-pause-button">
<svg x="0" y="0" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 26 26" width="28" height="28" style="fill: rgb(255, 255, 255);"><g fill="none" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="sans-serif" font-weight="normal" font-size="12" text-anchor="start" mix-blend-mode="normal"><g><g><path d="M0,26l0,-26l26,0l0,26z" fill="none"/><g fill="#ffffff"><path d="M20.208,11.857l-13.306,-6.597c-0.403,-0.203 -0.884,-0.181 -1.268,0.052c-0.386,0.232 -0.619,0.646 -0.619,1.09l0,13.198c0,0.443 0.233,0.856 0.619,1.089c0.208,0.126 0.444,0.19 0.683,0.19c0.201,0 0.401,-0.046 0.586,-0.138l13.306,-6.599c0.438,-0.218 0.716,-0.658 0.716,-1.143c0,-0.485 -0.279,-0.924 -0.717,-1.142z"/></g></g></g></g></svg>
</div>
<div class="media-player-button" id="next-button">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="#FFF"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
</div>
<div class="media-player-button" id="shuffle-button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="28" height="28" fill="#FFF"><path d="M17 2v3h-2.813c-1.1 0-2.187.588-2.687 1.688L6.594 16.5c-.1.3-.482.5-.782.5H2v2h3.813c1.1 0 2.187-.587 2.687-1.688L13.406 7.5c.1-.3.481-.5.781-.5H17v3l5-4-5-4zM2 5v2h3.813c.3 0 .675.194.875.594l1.718 3.312L9.5 8.687l-1-2C7.9 5.588 6.912 5 5.812 5H2zm9.594 8.094L10.5 15.313l1 2c.5 1 1.488 1.687 2.688 1.687H17v3l5-4-5-4v3h-2.813c-.3 0-.675-.194-.874-.594l-1.72-3.312z"/></svg>
</div>
<div class="media-player-button" id="repeat-button">
<svg class="repeat-button center" xmlns="http://www.w3.org/2000/svg" version="1" viewBox="0 0 24 24" enable-background="new 0 0 24 24" width="28" height="28" fill="#FFF">
<path d="M 17 2 L 17 5 L 6 5 C 4.3 5 3 6.3 3 8 L 3 14.8125 L 5 13.1875 L 5 8 C 5 7.4 5.4 7 6 7 L 17 7 L 17 10 L 22 6 L 17 2 z M 21 9.1875 L 19 10.8125 L 19 16 C 19 16.6 18.6 17 18 17 L 7 17 L 7 14 L 2 18 L 7 22 L 7 19 L 18 19 C 19.7 19 21 17.7 21 16 L 21 9.1875 z"/>
</svg>
</div>
<div class="media-player-button">
<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" style="width:25px;webkit-logical-width:25px;webkit-logical-height:25px;user-select:none;transform-origin:12.5px 12.5px;r:0;perspective-origin:12.5px 12.5px;overflow-y:hidden;overflow-x:hidden;inline-size:25px;height:25px;d:none;block-size:25px;background:0% 0%/auto padding-box border-box" overflow="hidden" display="block" fill="#fff"><g transform="translate(-.938 19.85)" style="y:0;user-select:none;transform:matrix(1,0,0,1,-.9375,19.85);perspective-origin:0 0"><g class="ld" style="y:0;x:0;user-select:none;transform-origin:7.9375px 0;transform:none;r:0;perspective-origin:0 0;line-height:31.4286px;font:400 22px/31.4286px 'Varela Round','century gothic',verdana;d:none"><text style="y:0;user-select:none;r:0;perspective-origin:7.9375px 0;line-height:31.4286px;font:400 22px/31.4286px Arial" white-space="nowrap" display="block">D</text></g></g><g transform="translate(14.938 19.85)" style="y:0;user-select:none;transform:matrix(1,0,0,1,14.9375,19.85);perspective-origin:0 0"><g class="ld" style="y:0;x:0;user-select:none;transform-origin:5.5px 0;transform:none;r:0;perspective-origin:0 0;line-height:31.4286px;font:400 22px/31.4286px 'Varela Round','century gothic',verdana;d:none"><text style="y:0;user-select:none;r:0;perspective-origin:5.5px 0;line-height:31.4286px;font:400 22px/31.4286px Arial" white-space="nowrap" display="block">J</text></g></g></svg>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,119 +0,0 @@
const app = Vue.createApp({
data() { return {
currentViewMain: 'file-explorer-view'
}},
methods: {
toggleMenu(event) {
toggleSideMenu();
}
}
})
const fileExplorerView = app.component('file-explorer-view', {
template: `
<div class="container">
<ul class="collection">
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li> <li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
<li class="collection-item">Test Item</li>
</ul>
</div>`
});
const nowPlayingView = app.component('now-playing-view', {
template: `<div class="container>CURRENTLY PLAYING</div>`
});
const vm = app.mount('#content');
function changeView(viewName, el){
if (vm.currentViewMain === viewName) {
closeSideMenu();
return;
}
vm.currentViewMain = viewName;
const elements = document.querySelectorAll('.side-nav-item'); // or:
elements.forEach(elm => {
elm.classList.remove("select")
});
el.classList.add("select");
// close nav on mobile
closeSideMenu();
}

1940
webapp/alpha/m.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -18,17 +18,15 @@ body {
}
#nav-bar {
border-top: 1px solid #444c56;
flex-basis: 100%;
flex-shrink: 0;
height: 80px;
background-color: #1a1a1a;
display: flex;
justify-content: space-between;
color: #F5F7FA;
display: flex;
flex-direction: column;
z-index: 1000;
}
#media-player-spacer {
@ -36,7 +34,12 @@ body {
}
.progress {
padding-top: 10px
padding-top: 10px;
background-color: #CCC !important;
}
.determinate {
background-color: #fa832b !important;
}
#media-player-button-bar {
@ -70,22 +73,12 @@ body {
cursor: pointer;
}
#sidenav-button {
display: none;
height: 60px;
cursor: pointer;
z-index: 1000000;
position: absolute;
left: calc(100% - 80px);
}
#content {
background-color: #1e2228;
color: #FFF;
height: calc(calc(var(--vh, 1vh) * 100) - 80px);
height: calc(calc(var(--vh, 1vh) * 100) - 20px);
/* height: calc(100vh - 80px); */
overflow-y: auto;
overflow-y: hidden;
overflow-x: hidden;
flex-grow: 1; /* do not grow - initial value: 0 */
flex-shrink: 0; /* do not shrink - initial value: 1 */
@ -93,10 +86,10 @@ body {
}
#content-header {
display: none;
width: 100%;
height: 60px;
height: 0px;
display: flex;
flex-direction: row;
}
@ -146,8 +139,9 @@ body {
flex-basis: auto;
}
#sidenav-button {
display: inline;
#content-header {
display: flex;
height: 60px;
}
}
@ -189,7 +183,7 @@ body {
@media screen and (min-width: 768px) {
#sidenav {
height: calc(calc(var(--vh, 1vh) * 100) - 80px);
height: calc(calc(var(--vh, 1vh) * 100) - 20px);
overflow-y: auto;
flex-grow: 0; /* do not grow - initial value: 0 */
flex-shrink: 0; /* do not shrink - initial value: 1 */
@ -199,8 +193,15 @@ body {
#sidenav-cover {
visibility: hidden;
}
}
.responsive-hide {
display: none;
}
.col-rev {
flex-direction: column-reverse !important;
}
}
/* Animated menu button */
/* Shamelessly stolen from: https://codepen.io/ainalem/pen/LJYRxz*/
@ -303,27 +304,705 @@ body {
font-weight: bold;
}
@media screen and (min-width: 600px) {
.h100-res {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
}
@media screen and (max-width: 600px) {
.container {
width: 100% !important;
}
.col-y {
padding-left: 0px !important;
}
.col-x {
padding-right: 0px !important;
}
}
.pointer {
cursor: pointer;
}
/* Note: maybe move things below this line elsewhere */
.collection-item {
/* background-color: #22272e !important; */
background-color: #2d333b !important;
border-bottom: 1px solid #444c56 !important;
color: #FFF;
padding: 0 !important;
position: relative;
border-left: 1px solid #444c56 !important;
border-right: 1px solid #444c56 !important;
}
.collection {
border: 1px solid #444c56 !important;
border: none !important;
margin: 0 !important;
}
.collection .collection-item:last-child {
border-bottom: none !important;
.collection-item:last-child {
border-bottom: 1px solid #444c56 !important;
}
.collection-item:first-child {
border-top: 1px solid #444c56 !important;
}
#filelist {
overflow-y: auto !important;
margin: 0 !important;
flex-grow: 1;
/* background-color: #1a1a1a; */
}
.flex-x {
display: flex;
flex-flow: column;
}
.flex2 {
display: flex;
}
.h1 {
height: 100%;;
}
.row-x {
margin-bottom: 0px !important;
min-height: 0;
width: 100%;
}
.col-x {
padding-left: 0px !important;
}
.col-y {
padding-right: 1px !important;
}
.col-z {
width: 100%;
max-height: 45px;
overflow-y: hidden;
flex-shrink: 0;
}
.row-y {
margin-bottom: 6px !important;
width: 100%;
}
.aa-card {
margin-left: auto !important;
margin-right: auto !important;
max-width: 180px !important;
margin-bottom: 0 !important;
}
.card-image {
max-height: 180px !important;
}
.data-card {
margin-right: 20px;
}
.card {
background-color: #2d333b !important;
}
.row-mod {
margin-bottom: 0px !important;
}
.pointer {
cursor: pointer;
}
.playing {
background-color: #4a463e !important;
}
.aa-card {
max-width: 220px;
margin-left: auto;
margin-right: auto;
}
.playError {
background-color: lightcoral !important;
}
.aux-button-active{
fill:rgb(102, 132, 178) !important;
color: rgb(102, 132, 178) !important;
}
.playlist-text {
width: calc(100% - 25px);
display: inline-block;
}
.secondary-content {
height: 100%;
}
.progress {
margin-top: 30px;
padding-top: 11px
}
.no-margin {
margin: 0;
}
.button-block {
margin-top: 8px;
display: flex;
margin-right: 10px;
}
.button-block-2 {
margin-top: 4px;
display: flex;
margin-right: 10px;
}
.button-block-1 {
margin-top: 8px;
width: 180px;
display: flex;
margin-left: 10px;
justify-content: right;
}
.volume {
width: 100px;
margin-bottom: 0px;
margin-top: 7px;
margin-right: 6px;
}
.margin-lr {
margin-left: 2px;
margin-right: 2px;
}
.margin-lr2 {
margin-left: 6px;
margin-right: 6px;
}
.progress-wrapper {
padding-top: 10px;
}
.fixed-action-btn {
bottom: auto !important;
}
#mstream-player {
max-width: 1800px;
}
#mstream-player svg {
fill:#FFF;
color: #FFF;
}
.flex {
display: flex;
flex-wrap: wrap;
}
.dirz, .filez, .playlist-item, .playlistz, .albumz, .artistz {
width: 100%;
padding-top: 12px;
padding-bottom: 12px;
padding-left: 10px;
padding-right: 10px;
cursor: pointer;
}
.song-button-box{
position: absolute;
right: 0;
z-index: 100;
top: 0;
}
.folder-image{
height: 20px;
margin-right: 5px;
}
.music-image{
min-height: 20px;
min-width: 20px;
margin-right: 5px;
}
.item-text {
vertical-align: top;
}
.songDropdown:hover, .downloadPlaylistSong:hover, .recursiveAddDir:hover, .addFileplaylist:hover {
background-color: #9E9E9E;
}
.songDropdown, .downloadPlaylistSong, .recursiveAddDir, .addFileplaylist {
height: 14px;
background-color: #B5B5B5;
float: right;
text-align: center;
font-size: 12px;
font-family: "Arial Black", Gadget, sans-serif;
padding-left: 3px;
padding-right: 3px;
cursor: pointer;
}
.songDropdown {
min-width: 42px !important;
line-height: 100% !important;
}
#pop-d, #pop-f {
min-width: 50px;
background-color: #F5F5F5;
border-radius: 3px;
border: 1px solid #b4b4b4;
color: #000;
z-index: 100;
}
.pop-playlist {
padding-left: 7px;
padding-right: 7px;
padding-top: 5px;
padding-bottom: 5px;
border-bottom: 1px solid #9E9E9E;
}
.pop-list-item {
padding-left: 7px;
padding-right: 7px;
padding-top: 5px;
padding-bottom: 5px;
cursor:pointer;
}
.pop-list-item:hover {
background-color: #E6EBFA;
}
.popperMenu:hover, .downloadDir:hover, .downloadFileplaylist:hover, .fileAddToPlaylist:hover {
background-color: #9E9E9E;
}
.popperMenu, .downloadDir, .downloadFileplaylist, .fileAddToPlaylist {
min-width: 28px !important;
height: 14px;
background-color: #B5B5B5;
float: right;
text-align: center;
font-size: 12px;
font-family: "Arial Black", Gadget, sans-serif;
border-bottom-left-radius: 3px;
padding-left: 3px;
padding-right: 3px;
border-right: 1px solid #9E9E9E;
cursor: pointer;
}
.song-button-box svg {
vertical-align: top !important;
padding-top: 1px;
}
.drag-handle{
cursor: move;
float: left;
padding-right: 6px;
}
.removeSong, .deletePlaylist, .removePlaylistSong{
cursor: pointer;
min-width: 28px !important;
height: 14px;
background-color: rgba(255,0,0, .7);
float: right;
text-align: center;
font-size: 12px;
font-family: "Arial Black", Gadget, sans-serif;
opacity: 0.9;
}
.removeSong:hover, .deletePlaylist:hover, .removePlaylistSong:hover{
opacity: 1;
background-color: rgba(255,0,0, .85);
}
.deletePlaylist, .removePlaylistSong {
line-height: 100% !important;
padding-left: 7px;
padding-right: 7px;
border-bottom-left-radius: 3px;
}
.downloadPlaylistSong, .recursiveAddDir, .addFileplaylist {
min-width: 28px;
border-right: 1px solid #9E9E9E;
}
#pop {
background: #1a1a1a;
padding: 3px;
border-radius: 3px;
border: 1px solid #262a33;
z-index: 1000;
}
#mstream-player {
z-index: 1000;
background-color: #1e2228;
}
.header-tab{
z-index: 1000;
display: flex;
padding-left: 6px;
}
.grow {
flex-grow: 1;
}
#playlist-buttons a {
padding:6px;
cursor: pointer;
}
#playlist-buttons {
margin-top: 8px;
}
#playlist-buttons svg {
fill: #fff;
opacity: .7;;
}
#playlist-buttons svg:hover {
opacity: 1;
}
.fixed-action-btn {
z-index: 4000 !important;
}
#rg-pregain-info {
font-weight: 800;
font-size: 16px;
font-family: 'Jura', sans-serif;
width: 34px;
transition: opacity 0.25s;
text-align: right;
}
#rg-status {
transition: opacity 0.25s;
}
.rpg {
margin-right: 10px;
cursor: pointer;
}
.auto-dj {
cursor: pointer;
margin-left: 10px;
}
svg {
fill: #FFF;
}
#directoryName, #search_folders {
flex-grow: 1;
max-width: calc(100% - 72px);
}
#search_folders {
padding-right: 5px;
padding-left: 5px;
}
.super-hide {
display: none !important;
}
#localSearchBar {
max-height: 20px;
color: #FFF;
}
.album-art-box{
height: 50px;
min-width: 50px;
margin-right: 10px;
}
.collection-item {
display: flex;
}
.main-overlay{
position: absolute;
padding:0;
margin:0;
top:0;
left:0;
width: 100%;
height: 100%;
background:rgba(0,0,0,1);
z-index:99999;
cursor: pointer;
}
.show-fade {
opacity: 1;
transition: opacity 400ms;
}
.hide-fade {
pointer-events: none;
opacity: 0;
transition: opacity 400ms;
}
#viz-canvas {
width: 100%;
height: 100%;
}
.trans-input {
max-width: 200px;
}
.browser-panel {
margin: 12px;
}
.collection .collection-item {
line-height: normal !important;
}
.flex-center {
display: flex;
justify-content: center;
}
.scroll-auto {
overflow-y: auto !important;
}
#directory_bar {
min-height: 40px;
align-items: center;
justify-content: center;
}
#directoryName{
padding-bottom: 3px;
padding-left: 5px;
text-overflow: ellipsis;
overflow: hidden;
max-height: 100%;
}
.newPlaylistButton {
margin: 0 auto;
display: table !important;
line-height: inherit !important;
}
.recently-added-input {
max-width: 80px;
color: #FFF;
}
.hystmodal__window {
background-color: #2d333b;
color: #FFF;
}
#new_playlist_name, #share_time, #playlist_name, #search-term {
color: #FFF;
}
#new_playlist, #save_playlist {
margin-top: 20px;
}
#share-textarea {
margin-top: 20px;
min-height: 60px;
color: #FFF;
}
.switch label .lever {
height: 18px !important;
width: 46px !important;
}
.switch label input[type="checkbox"]:not(:checked) + .lever {
background-color: #888 !important;
}
.switch label input[type="checkbox"]:checked + .lever::after {
background-color: #26a69a !important;
}
.switch label .lever::before, .switch label .lever::after {
width: 24px !important;
height: 24px !important;
}
.switch label input[type="checkbox"]:checked + .lever::before, .switch label input[type="checkbox"]:checked + .lever::after {
left: 24px !important;
}
.switch label {
font-size: 1.2rem;
}
.column-reverse {
flex-direction: column-reverse;
}
.song-button-box svg {
fill: #000;
}
.fileplaylistz {
padding: 10px; }
.upload-progress-bar {
background-color: rgba(0,0,0,0);
height: 1px;
}
.upload-progress-inner {
background-color: skyblue;
box-shadow: 0px 0px 4px 2px rgba(135, 206, 235,0.91);
height: 100%;
position: relative;
z-index:1000;
}
.m-tab {
font-size: 22px;
width:50%;
cursor: pointer;
height: 40px;
background-color: #111;
margin-top: 5px;
}
.selected-tab {
border-bottom: 1px solid #FFF;
}
#nav-bar div {
padding-left: 4px;
}
#autodj-ratings{
max-width: 120px;
}
select {
background-color: #2d333b !important;
color: #FFF;
cursor: pointer;
}
.custom-card-img img {
max-height: 140px;
}
.card-mod {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.card-mod .card-content {
padding: 6px !important;
}
.card-meta {
padding-left: 12px;
}
.flex-end {
justify-content: flex-end;
}
.block {
display: block;
}
#flip-me {
overflow: hidden;
}
#editServer input {
color: #FFF;
}
.pad-6 {
padding-left: 6px;
padding-right: 6px;
}
.mobile-links {
display: flex;
justify-content: space-between;
}
.mobile-links a {
width: 100%;
max-width: 380px;
}
.mobile-links img {
width: 100%;
}
.no-margin p {
margin: 0;
}
.make-white {
color: #FFF;
}

View File

@ -2,10 +2,7 @@ document.getElementById("sidenav-cover").addEventListener("click", () => {
toggleSideMenu();
});
console.log('EFEWSEFWEF')
function toggleSideMenu() {
console.log('gferg')
document.getElementById("sidenav-cover").classList.toggle("click-through");
// Handles initial state rendered on page load
@ -37,3 +34,31 @@ document.documentElement.style.setProperty('--vh', `${window.innerHeight/100}px`
window.addEventListener("resize", () => {
document.documentElement.style.setProperty('--vh', `${window.innerHeight/100}px`);
});
function changeView(fn, el){
const elements = document.querySelectorAll('.side-nav-item'); // or:
elements.forEach(elm => {
elm.classList.remove("select")
});
el.classList.add("select");
// close nav on mobile
closeSideMenu();
fn();
}
function toggleThing(el, bool) {
document.querySelectorAll('.m-tab').forEach(elm => {
elm.classList.remove("selected-tab")
});
el.classList.add("selected-tab");
if (bool === false) {
document.getElementById('browser').classList.add('hide-on-small-only');
}else {
document.getElementById('browser').classList.remove('hide-on-small-only');
}
}

604
webapp/alpha/vp.js Normal file
View File

@ -0,0 +1,604 @@
const VUEPLAYERCORE = (() => {
const mstreamModule = {};
mstreamModule.livePlaylist = {
name: false
};
mstreamModule.altLayout = {
'moveMeta': false,
'audioBookCtrls': false,
'flipPlayer': false
};
try {
const altLayout = JSON.parse(localStorage.getItem('altLayout'));
mstreamModule.altLayout.flipPlayer = typeof altLayout.flipPlayer === 'boolean' ? altLayout.flipPlayer : false;
mstreamModule.altLayout.audioBookCtrls = typeof altLayout.audioBookCtrls === 'boolean' ? altLayout.audioBookCtrls : false;
mstreamModule.altLayout.moveMeta = typeof altLayout.moveMeta === 'boolean' ? altLayout.moveMeta : false;
if (altLayout.flipPlayer === true) {
document.getElementById('content').classList.add('col-rev');
document.getElementById('flip-me').classList.add('col-rev');
}
} catch (e) {}
const replayGainPreGainSettings = [
-15.0,
-10.0,
-6.0,
0.0
];
var replayGainInfoTimeout;
// Hide rating popover on click
document.onmouseup = (e) => {
if(!e.target.classList.contains('pop-c')){
document.getElementById("pop").style.visibility = "hidden";
currentPopperSongIndex = false;
}
if(!e.target.classList.contains('pop-d')){
document.getElementById("pop-d").style.visibility = "hidden";
cpsi = false;
}
if(!e.target.classList.contains('pop-f')){
document.getElementById("pop-f").style.visibility = "hidden";
}
}
new Vue({
el: '#speed-modal',
data: {
stats: MSTREAMPLAYER.playerStats
},
computed: {
widthcss: function () {
const percentage = ((this.stats.playbackRate / 3.75) * 100) - 6.75;
return `width:calc(${percentage}%)`;
},
},
methods: {
changeSpeed: function() {
const rect = this.$refs.progressWrapper.getBoundingClientRect();
const x = event.clientX - rect.left; //x position within the element.
const percentage = x / rect.width;
MSTREAMPLAYER.changePlaybackRate(percentage * 3.75 + 0.25);
},
changeSpeed2: function(speed) {
MSTREAMPLAYER.changePlaybackRate(speed);
}
}
});
// star rating popper
var currentPopperSongIndex2;
var currentPopperSongIndex;
var currentPopperSong;
const showClearLink = { val: false };
// add to playlist popper
mstreamModule.playlists = [];
var cpsi;
var cps;
new Vue({
el: '#playlist',
data: {
playlist: MSTREAMPLAYER.playlist,
playlists: mstreamModule.playlists,
showClear: showClearLink,
altLayout: mstreamModule.altLayout,
meta: MSTREAMPLAYER.playerStats.metadata,
livePlaylist: mstreamModule.livePlaylist
},
computed: {
albumArtPath: function () {
if (!this.meta['album-art']) {
return 'assets/img/default.png';
}
return MSTREAMAPI.currentServer.host + `album-art/${this.meta['album-art']}?compress=l&token=${MSTREAMPLAYER.getCurrentSong().authToken}`;
}
},
methods: {
getSongInfo: function() {
openMetadataModal(MSTREAMPLAYER.getCurrentSong().metadata, MSTREAMPLAYER.getCurrentSong().rawFilePath);
},
gsi2: function() {
openMetadataModal(cps.metadata, cps.rawFilePath);
},
goToArtist: function() {
const el = document.createElement('DIV');
el.setAttribute('data-artist', this.meta.artist);
getArtistz(el);
},
goToAlbum: function() {
const el = document.createElement('DIV');
el.setAttribute('data-album', this.meta.album);
el.setAttribute('data-year', this.meta.year);
getAlbumsOnClick(el);
},
checkMove: function (event) {
document.getElementById("pop").style.visibility = "hidden";
MSTREAMPLAYER.resetPositionCache();
if (mstreamModule.livePlaylist.name) {
const songs = [];
for (let i = 0; i < MSTREAMPLAYER.playlist.length; i++) {
songs.push(MSTREAMPLAYER.playlist[i].filepath);
}
MSTREAMAPI.savePlaylist(mstreamModule.livePlaylist.name,songs, true);
}
},
clearRating: async function () {
try {
await MSTREAMAPI.rateSong(currentPopperSong.rawFilePath, null);
MSTREAMPLAYER.editSongMetadata('rating', null, currentPopperSongIndex2);
} catch(err) {
iziToast.error({
title: 'Failed to set rating',
position: 'topCenter',
timeout: 3500
});
}
},
},
});
// Template for playlist items
Vue.component('playlist-item', {
template: `
<li class="noselect collection-item" v-bind:class="{ playing: (this.index === positionCache.val), playError: (this.songError && this.songError === true) }" >
<div v-on:click="goToSong($event)" class="playlist-item">
<span onclick="event.stopPropagation()" class="drag-handle">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="24" height="24"><path fill="#FFF" d="M4 7v2h24V7Zm0 8v2h24v-2Zm0 8v2h24v-2Z"/></svg>
</span>
<span class="song-area">{{ comtext }}</span>
<div onclick="event.stopPropagation()" class="song-button-box">
<span v-on:click="removeSong($event)" class="removeSong">
<svg width="12" height="12" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" xml:space="preserve"><path d="M507.8 392 371.7 256l136-136c5.6-5.6 5.6-14.8 0-20.4L412.4 4.2c-5.6-5.6-14.8-5.6-20.4 0l-136 136-136-136c-5.4-5.4-15-5.4-20.4 0L4.3 99.5c-2.7 2.7-4.2 6.4-4.2 10.2s1.5 7.5 4.2 10.2l136 136L4.2 392c-2.7 2.7-4.2 6.4-4.2 10.2 0 3.8 1.5 7.5 4.2 10.2l95.3 95.3c2.7 2.7 6.4 4.2 10.2 4.2 3.8 0 7.5-1.5 10.2-4.2l136.1-136 136.1 136c2.8 2.8 6.5 4.2 10.2 4.2 3.7 0 7.4-1.4 10.2-4.2l95.3-95.3c5.6-5.6 5.6-14.7 0-20.4z"/></svg>
</span>
<span v-on:click="createPopper($event)" class="songDropdown pop-c">
{{ratingNumber}}
<svg class="pop-c" width="12" height="12" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 53.867 53.867"><path class="pop-c" d="m26.934 1.318 8.322 16.864 18.611 2.705L40.4 34.013l3.179 18.536-16.645-8.751-16.646 8.751 3.179-18.536L0 20.887l18.611-2.705z" fill="#efce4a"/></svg>
</span>
<span class="downloadPlaylistSong" v-on:click="downloadSong($event)">
<svg width="12" height="12" viewBox="0 0 2048 2048" xmlns="http://www.w3.org/2000/svg"><path d="M1803 960q0 53-37 90l-651 652q-39 37-91 37-53 0-90-37l-651-652q-38-36-38-90 0-53 38-91l74-75q39-37 91-37 53 0 90 37l294 294v-704q0-52 38-90t90-38h128q52 0 90 38t38 90v704l294-294q37-37 90-37 52 0 91 37l75 75q37 39 37 91z"/></svg>
</span>
<span v-on:click="createPopper2($event)" class="popperMenu pop-d">
<svg class="pop-d" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 292.362 292.362"><path class="pop-d" d="M286.935 69.377c-3.614-3.617-7.898-5.424-12.848-5.424H18.274c-4.952 0-9.233 1.807-12.85 5.424C1.807 72.998 0 77.279 0 82.228c0 4.948 1.807 9.229 5.424 12.847l127.907 127.907c3.621 3.617 7.902 5.428 12.85 5.428s9.233-1.811 12.847-5.428L286.935 95.074c3.613-3.617 5.427-7.898 5.427-12.847 0-4.948-1.814-9.229-5.427-12.85z"/></svg>
</span>
</div>
</div>
</li>`,
props: ['index', 'song'],
// We need the positionCache to track the currently playing song
data: function () {
return {
positionCache: MSTREAMPLAYER.positionCache
}
},
// Methods used by playlist item events
methods: {
goToSong: function (event) {
MSTREAMPLAYER.goToSongAtPosition(this.index);
},
removeSong: function (event) {
MSTREAMPLAYER.removeSongAtPosition(this.index, false);
if (mstreamModule.livePlaylist.name) {
const songs = [];
for (let i = 0; i < MSTREAMPLAYER.playlist.length; i++) {
songs.push(MSTREAMPLAYER.playlist[i].filepath);
}
MSTREAMAPI.savePlaylist(mstreamModule.livePlaylist.name,songs, true);
}
},
downloadSong: function (event) {
const link = document.createElement("a");
link.download = '';
link.href = this.song.url;
link.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true, view: window}));
},
createPopper: function (event) {
if (currentPopperSongIndex === this.index) {
currentPopperSongIndex = false;
document.getElementById("pop").style.visibility = "hidden";
return;
}
var ref = event.target;
currentPopperSongIndex = this.index;
currentPopperSongIndex2 = this.index;
currentPopperSong = this.song;
showClearLink.val = false;
if (typeof MSTREAMPLAYER.playlist[currentPopperSongIndex2].metadata.rating === 'number'){
showClearLink.val = true
}
myRater.setRating(this.song.metadata.rating / 2);
const pop = document.getElementById('pop');
Popper.createPopper(ref, pop, {
placement: 'bottom-end',
onFirstUpdate: function (data) {
document.getElementById("pop").style.visibility = "visible";
},
modifiers: [
{
name: 'flip',
options: {
boundariesElement: 'scrollParent',
},
},
{
name: 'preventOverflow',
options: {
boundariesElement: 'scrollParent',
},
},
]
});
},
createPopper2: function (event) {
if (cpsi === this.index) {
cpsi = false;
document.getElementById("pop-d").style.visibility = "hidden";
return;
}
var ref = event.target;
cpsi = this.index;
cps = this.song;
const pop = document.getElementById('pop-d');
Popper.createPopper(ref, pop, {
placement: 'bottom-end',
onFirstUpdate: function (data) {
document.getElementById("pop-d").style.visibility = "visible";
},
modifiers: [
{
name: 'flip',
options: {
boundariesElement: 'scrollParent',
},
},
{
name: 'preventOverflow',
options: {
boundariesElement: 'scrollParent',
},
},
]
});
},
},
computed: {
comtext: function () {
let returnThis = this.song.filepath.split('/').pop();
if (this.song.metadata.title) {
returnThis = this.song.metadata.title;
if (this.song.metadata.artist) {
returnThis = this.song.metadata.artist + ' - ' + returnThis;
}
}
return returnThis;
},
songError: function () {
return this.song.error;
},
ratingNumber: function () {
if (!this.song.metadata.rating) {
return '';
}
var returnThis = this.song.metadata.rating / 2;
if (!Number.isInteger(returnThis)) {
returnThis = returnThis.toFixed(1);
}
return returnThis;
}
}
});
Vue.component('popper-playlist-item', {
template: '<div class="pop-list-item" v-on:click="addToPlaylist($event)">&#8226; {{playlistName}}</div>',
props: ['index', 'playlist'],
methods: {
addToPlaylist: async function(event) {
try {
await MSTREAMAPI.addToPlaylist(this.playlist.name, cps.filepath);
iziToast.success({
title: 'Song Added!',
position: 'topCenter',
timeout: 3500
});
}catch(err) {
iziToast.error({
title: 'Failed to add song',
position: 'topCenter',
timeout: 3500
});
}
}
},
computed: {
playlistName: function () {
return this.playlist.name;
}
}
});
new Vue({
el: '#mstream-player',
data: {
playerStats: MSTREAMPLAYER.playerStats,
playlist: MSTREAMPLAYER.playlist,
positionCache: MSTREAMPLAYER.positionCache,
meta: MSTREAMPLAYER.playerStats.metadata,
lastVol: 100,
replayGainToggle: false,
altLayout: mstreamModule.altLayout
},
created: function () {
if (typeof(Storage) !== "undefined") {
const localVol = localStorage.getItem("volume");
if (localVol !== null && !isNaN(localVol)) {
MSTREAMPLAYER.changeVolume(parseInt(localVol));
}
MSTREAMPLAYER.setReplayGainActive(localStorage.getItem("replayGain") == "true");
const rgPregain = Number(localStorage.getItem("replayGainPreGainDb"));
MSTREAMPLAYER.setReplayGainPreGainDb(rgPregain === NaN ? 0 : rgPregain);
}
},
computed: {
playbackRate: function() {
const rate = Number(this.playerStats.playbackRate);
return rate.toFixed(2) + 'x'
},
currentTime: function() {
if (!this.playerStats.duration) { return ''; }
const minutes = Math.floor(this.playerStats.currentTime / 60);
const secondsToCalc = Math.floor(this.playerStats.currentTime % 60) + '';
const currentText = minutes + ':' + (secondsToCalc.length < 2 ? '0' + secondsToCalc : secondsToCalc);
return currentText;
},
durationTime: function() {
if (!this.playerStats.duration) { return '0:00'; }
const minutes = Math.floor(this.playerStats.duration / 60);
const secondsToCalc = Math.floor(this.playerStats.duration % 60) + '';
const currentText = minutes + ':' + (secondsToCalc.length < 2 ? '0' + secondsToCalc : secondsToCalc);
return currentText;
},
widthcss: function () {
if (this.playerStats.duration === 0) {
return "width:0";
}
const percentage = (this.playerStats.currentTime / this.playerStats.duration) * 100;
return `width:${percentage}%`;
},
volWidthCss: function () {
return `width: ${this.playerStats.volume}%`;
},
albumArtPath: function () {
if (!this.meta['album-art']) {
return 'assets/img/default.png';
}
return MSTREAMAPI.currentServer.host + `album-art/${this.meta['album-art']}?compress=l&token=${MSTREAMPLAYER.getCurrentSong().authToken}`;
}
},
methods: {
getSongInfo: function() {
openMetadataModal(MSTREAMPLAYER.getCurrentSong().metadata, MSTREAMPLAYER.getCurrentSong().rawFilePath);
},
changeVol: function(event) {
const rect = this.$refs.volumeWrapper.getBoundingClientRect();
const x = event.clientX - rect.left; //x position within the element.
let percentage = (x / rect.width) * 100;
if (percentage > 100) { percentage = 100; } // It's possible to 'drag' the progress bar to get over 100 percent
if (percentage < 0) { percentage = 0; } // It's possible to 'drag' the progress bar to get over 100 percent
MSTREAMPLAYER.changeVolume(percentage);
if (typeof(Storage) !== "undefined") {
localStorage.setItem("volume", percentage);
}
},
seekTo: function(event) {
const rect = this.$refs.progressWrapper.getBoundingClientRect();
const x = event.clientX - rect.left; //x position within the element.
const percentage = (x / rect.width) * 100;
MSTREAMPLAYER.seekByPercentage(percentage);
},
playPause: function() {
MSTREAMPLAYER.playPause();
},
previousSong: function() {
MSTREAMPLAYER.previousSong();
},
nextSong: function() {
MSTREAMPLAYER.nextSong();
},
toggleRepeat: function () {
MSTREAMPLAYER.toggleRepeat();
},
toggleShuffle: function () {
MSTREAMPLAYER.toggleShuffle();
},
toggleAutoDJ: function () {
MSTREAMPLAYER.toggleAutoDJ();
},
goToArtist: function() {
const el = document.createElement('DIV');
el.setAttribute('data-artist', this.meta.artist);
getArtistz(el);
},
goToAlbum: function() {
const el = document.createElement('DIV');
el.setAttribute('data-album', this.meta.album);
el.setAttribute('data-year', this.meta.year);
getAlbumsOnClick(el);
},
goForward: function(seconds) {
MSTREAMPLAYER.goForwardSeek(seconds);
},
goBack: function(seconds) {
MSTREAMPLAYER.goBackSeek(seconds);
},
fadeOverlay: function () {
VIZ.toggleDom();
},
toggleMute: function () {
if (this.playerStats.volume === 0) {
MSTREAMPLAYER.changeVolume(this.lastVol);
} else {
this.lastVol = this.playerStats.volume;
MSTREAMPLAYER.changeVolume(0);
}
},
toggleReplayGain: function () {
// With a series of clicks, allow the user to first activate ReplayGain, then progress through a list of
// settings for the desired level of pre-gain, and then finally disable ReplayGain again.
if (replayGainInfoTimeout) { clearTimeout(replayGainInfoTimeout); }
if (!this.playerStats.replayGain) {
MSTREAMPLAYER.setReplayGainPreGainDb(replayGainPreGainSettings[0]);
MSTREAMPLAYER.setReplayGainActive(true);
} else {
const settingsIdx = replayGainPreGainSettings.indexOf(this.playerStats.replayGainPreGainDb);
if (settingsIdx == -1 || settingsIdx >= replayGainPreGainSettings.length - 1) {
MSTREAMPLAYER.setReplayGainActive(false);
this.replayGainToggle = false;
} else {
MSTREAMPLAYER.setReplayGainPreGainDb(replayGainPreGainSettings[settingsIdx + 1]);
}
}
if (this.playerStats.replayGain) {
this.replayGainToggle = true;
replayGainInfoTimeout = setTimeout(() => {
this.replayGainToggle = false;
}, 1000);
}
if (typeof(Storage) !== "undefined") {
localStorage.setItem("replayGain", this.playerStats.replayGain);
localStorage.setItem("replayGainPreGainDb", this.playerStats.replayGainPreGainDb);
}
},
}
});
// Change spacebar behavior to Play/Pause
window.addEventListener("keydown", (event) => {
// Use default behavior if user is in a form
const element = event.target.tagName.toLowerCase();
if (element === 'input' || element === 'textarea') {
return;
}
// Check the key
switch (event.key) {
case " ": //SpaceBar
event.preventDefault();
MSTREAMPLAYER.playPause();
break;
}
}, false);
const myRater = raterJs({
element: document.querySelector(".my-rating"),
step: .5,
starSize: 22,
rateCallback: async (rating, done) => {
try {
await MSTREAMAPI.rateSong(currentPopperSong.rawFilePath, parseInt(rating * 2));
MSTREAMPLAYER.editSongMetadata('rating', parseInt(rating * 2), currentPopperSongIndex2);
}catch(err) {
iziToast.error({
title: 'Failed to set rating',
position: 'topCenter',
timeout: 3500
});
}
done();
}
});
mstreamModule.addSongWizard = async (filepath, metadata, lookupMetadata, position, livePlaylist, autoPlayOff) => {
// Escape filepath
const rawFilepath = filepath;
filepath = filepath.replace(/\%/g, "%25");
filepath = filepath.replace(/\#/g, "%23");
filepath = filepath.replace(/\?/g, "%3F");
if (filepath.charAt(0) === '/') {
filepath = filepath.substr(1);
}
let defaultPathString = 'media/';
if (MSTREAMPLAYER.transcodeOptions.serverEnabled && MSTREAMPLAYER.transcodeOptions.frontendEnabled) {
defaultPathString = 'transcode/';
}
let url = MSTREAMAPI.currentServer.host + defaultPathString + filepath + '?';
if (MSTREAMAPI.currentServer.token) {
url = url + 'token=' + MSTREAMAPI.currentServer.token;
}
const newSong = {
url: url,
rawFilePath: rawFilepath,
filepath: filepath,
metadata: metadata,
authToken: MSTREAMAPI.currentServer.token
};
if (position) {
MSTREAMPLAYER.insertSongAt(newSong, position, true);
if (mstreamModule.livePlaylist.name) {
const songs = [];
for (let i = 0; i < MSTREAMPLAYER.playlist.length; i++) {
songs.push(MSTREAMPLAYER.playlist[i].filepath);
}
MSTREAMAPI.savePlaylist(mstreamModule.livePlaylist.name,songs, true);
}
} else {
MSTREAMPLAYER.addSong(newSong, autoPlayOff);
if (mstreamModule.livePlaylist.name && livePlaylist !== false) {
await MSTREAMAPI.addToPlaylist(mstreamModule.livePlaylist.name, newSong.filepath);
}
}
// perform lookup
if (lookupMetadata === true) {
const response = await MSTREAMAPI.lookupMetadata(rawFilepath);
if (response.metadata) {
newSong.metadata = response.metadata;
MSTREAMPLAYER.resetCurrentMetadata();
}
}
};
mstreamModule.clearQueue = async() => {
MSTREAMPLAYER.clearPlaylist();
if (mstreamModule.livePlaylist.name) {
const songs = [];
for (let i = 0; i < MSTREAMPLAYER.playlist.length; i++) {
songs.push(MSTREAMPLAYER.playlist[i].filepath);
}
MSTREAMAPI.savePlaylist(mstreamModule.livePlaylist.name,songs, true);
}
}
return mstreamModule;
})()

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
img[data-lazy-src]{will-change:contents}

View File

@ -15,12 +15,6 @@ body {
clear: both;
}
#save_playlist_form{
padding-left: 15px;
padding-right: 15px;
padding-top: 25px;
}
input[type="submit"], input[type="button"] {
background-color: rgb(147, 151, 205);
border-radius: 2px;
@ -94,16 +88,6 @@ background-color: #E6EBFA !important;
.scrollBoxHeight3 {
height: calc(100% - 55px); } }
.album-art-left-container{
display: none;
}
@media (min-width: 644px) {
.album-art-left-container {
display: none; } }
@media (max-device-width: 643px) {
.album-art-left-container {
display: block;} }
.playerControls{
height: 90px;
}
@ -336,7 +320,7 @@ h3 {
text-overflow:ellipsis;
width:calc(100% - 95px);
margin-bottom: 5px;
margin-bottom: 0px;
display: flex;
}
@ -431,24 +415,6 @@ h3 {
z-index:99999;
}
.login-overlay{
position:fixed;
padding:0;
margin:0;
top:0;
left:0;
width: 100%;
height: 100%;
background:rgba(255,255,255,0.9);
z-index:9;
}
.super-hide{
display: none !important;
}
.login-icon{
max-height: 200px;
padding-top: 50px;
@ -457,18 +423,15 @@ h3 {
margin: auto;
}
.mstream-player{
position:fixed;
bottom:0;
}
.share-textarea, #fed-textarea, #federation-invitation-code{
#share-textarea, #fed-textarea {
height: 8em;
}
#share_time, #federation-invite-time{
#share_time {
width: 4em;
float:left;
}
@ -540,7 +503,8 @@ h3 {
}
.music-image{
height: 18px;
min-height: 18px;
min-width: 18px;
margin-right: 5px;
}
@ -567,8 +531,10 @@ h3 {
font-size: 17px;
line-height: 100%;
}
.album-art-box{
height: 50px;
min-width: 50px;
margin-right: 10px;
}
@ -759,12 +725,6 @@ ul.left-nav-menu li.selected svg {
font-family: 'Jura', sans-serif;
}
.login-overlay label {
font-family: 'Jura', sans-serif;
font-size: 16px !important;
font-weight: bold
}
.mobile-links {
display: flex;
justify-content: space-between;
@ -802,22 +762,6 @@ ul.left-nav-menu li.selected svg {
display: none !important;
}
#federation-invite-checkbox-area {
padding-left: 10px;
padding-right: 10px;
padding-bottom: 10px;
}
.federation-invite-thing{
width: calc(100% - 70px);
display: inline-block;
}
#federation-invite-selection-panel {
overflow-y: scroll;
max-height: 300px;
}
input[name="autodj-folders"] {
cursor: pointer;
}
@ -851,4 +795,22 @@ input[name="autodj-folders"] {
border-radius: 0 5px 5px 0;
padding: 0;
}
.show-fade {
opacity: 1;
transition: opacity 400ms;
}
.hide-fade {
pointer-events: none;
opacity: 0;
transition: opacity 400ms;
}
.flex {
display: flex;
}
.metadata-panel-text span {
cursor: pointer;
}

View File

@ -0,0 +1,21 @@
/* https://github.com/AddMoreScripts/hystModal */
.hystmodal__opened,.hystmodal__shadow{position:fixed;right:0;left:0;overflow:hidden}.hystmodal__shadow{border:none;display:block;width:100%;top:0;bottom:0;pointer-events:none;z-index:5000;opacity:0;transition:opacity .15s ease;background-color:#000}.hystmodal__shadow--show{pointer-events:auto;opacity:.6}.hystmodal{position:fixed;top:0;bottom:0;right:0;left:0;overflow:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch;opacity:1;pointer-events:none;display:flex;flex-flow:column nowrap;justify-content:flex-start;z-index:5000;visibility:hidden}.hystmodal--active{opacity:1}.hystmodal--active,.hystmodal--moved{pointer-events:auto;visibility:visible}.hystmodal__wrap{flex-shrink:0;flex-grow:0;width:100%;min-height:100%;margin:auto;display:flex;flex-flow:column nowrap;align-items:center;justify-content:center}.hystmodal__window{margin:50px 0;box-sizing:border-box;flex-shrink:0;flex-grow:0;width:600px;max-width:100%;overflow:visible;transition:transform .2s ease 0s,opacity .2s ease 0s;transform:scale(.9);opacity:0}.hystmodal--active .hystmodal__window{transform:scale(1);opacity:1}.hystmodal__close{position:absolute;z-index:10;top:0;right:-40px;display:block;width:30px;height:30px;background-color:transparent;background-position:50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' stroke='%23fff' stroke-linecap='square' stroke-miterlimit='50' stroke-width='2' d='M22 2L2 22'/%3E%3Cpath fill='none' stroke='%23fff' stroke-linecap='square' stroke-miterlimit='50' stroke-width='2' d='M2 2l20 20'/%3E%3C/svg%3E");background-size:100% 100%;border:none;font-size:0;cursor:pointer;outline:none}.hystmodal__close:focus{outline:2px dotted #afb3b9;outline-offset:2px}@media (max-width:767px){.hystmodal__close{top:10px;right:10px;width:24px;height:24px;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' stroke='%23111' stroke-linecap='square' stroke-miterlimit='50' stroke-width='2' d='M22 2L2 22'/%3E%3Cpath fill='none' stroke='%23111' stroke-linecap='square' stroke-miterlimit='50' stroke-width='2' d='M2 2l20 20'/%3E%3C/svg%3E")}.hystmodal__window{margin:0}}
.hystmodal__window{
position: relative;
overflow: visible;
border-radius: 4px;
padding: 30px 30px;
}
.hystmodal--active {
z-index: 5001;
}
.hystmodal__wrap {
z-index: 5001;
}
.hystmodal--active .hystmodal__window {
z-index: 5002;
}

View File

@ -102,18 +102,6 @@
right: 0;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #f9f9f9;
min-width: 140px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
padding-top: 6px;
padding-bottom: 6px;
z-index: 1;
right: 48px;
top: 0px;
}
.songDropdown:hover, .downloadPlaylistSong:hover, .recursiveAddDir:hover, .addFileplaylist:hover {
background-color: #9E9E9E;
@ -140,11 +128,11 @@
min-width: 38px !important;
}
.popperMenu:hover, .downloadDir:hover, .downloadFileplaylist:hover {
.popperMenu:hover, .downloadDir:hover, .downloadFileplaylist:hover, .fileAddToPlaylist:hover {
background-color: #9E9E9E;
}
.popperMenu, .downloadDir, .downloadFileplaylist {
.popperMenu, .downloadDir, .downloadFileplaylist, .fileAddToPlaylist {
min-width: 28px !important;
height: 14px;
background-color: #B5B5B5;
@ -159,7 +147,7 @@
cursor: pointer;
}
#pop-d {
#pop-d, #pop-f {
min-width: 50px;
background-color: #F5F5F5;
@ -296,18 +284,6 @@
margin-right: 5px;
}
.remote-button{
height: 100%;
width:38px;
background-color: #333333;
float:left;
position: relative;
overflow:hidden;
cursor:pointer;
z-index:9;
}
.player-button{
height: 100%;
width:40px;
@ -379,14 +355,10 @@ fill: rgb(255, 255, 255);
@media (max-width: 450px) {
.volume-bar {
display: none;}
.remote-button {
display: none;}
}
@media (max-device-width: 451px) {
.volume-bar {
display: none;}
.remote-button {
display: none;}
}
.volume-slider{

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -4,20 +4,12 @@ const API = (() => {
// initialize with a default server
module.servers = [{
name: "default",
url: window.location.origin,
url: '..', // This is some hacky bullshit to get relative URLs working
token: localStorage.getItem('token')
}];
module.selectedServer = 0;
module.addServer = (name, url, username, password) => {
module.servers.push({
name: name,
url: url,
token: null
})
}
module.name = () => {
return module.servers[module.selectedServer].name;
}
@ -30,39 +22,19 @@ const API = (() => {
return module.servers[module.selectedServer].url;
}
module.checkAuthAndKickToLogin = async () => {
// Send request to server
try {
await axios({
method: 'GET',
url: `${module.url()}/api/`,
headers: { 'x-access-token': module.token() }
});
} catch (err) {
window.location.replace(`/login?redirect=${encodeURIComponent(window.location.pathname)}`);
}
}
module.logout = () => {
localStorage.removeItem('token');
Cookies.remove('x-access-token');
window.location.replace(`/login`);
document.location.assign(window.location.href.replace('/admin', '') + (window.location.href.slice(-1) === '/' ? '' : '/') + 'login');
}
module.goToPlayer = () => {
window.location.assign(window.location.href.replace('/admin', ''));
}
module.axios = axios.create({
baseURL: module.url(),
headers: { 'x-access-token': module.token() }
});
// TODO: We also need a way to save servers
module.changeDefaultServer = (serverIndex) => {
// TODO: Throw Error?
if (!module.servers[serverIndex]) { return false; }
module.selectedServer = serverIndex;
// TODO: update module.axios to use the token for that server
}
return module;
})();

View File

@ -9,28 +9,44 @@ var MSTREAMAPI = (function () {
vpaths: []
}
$.ajaxPrefilter(function (options) {
options.beforeSend = function (xhr) {
xhr.setRequestHeader('x-access-token', MSTREAMAPI.currentServer.token);
}
});
// $.ajaxPrefilter(function (options) {
// options.beforeSend = function (xhr) {
// xhr.setRequestHeader('x-access-token', MSTREAMAPI.currentServer.token);
// }
// });
function makeRequest(url, type, dataObject, callback) {
var request = $.ajax({
url: url,
type: type,
contentType: "application/json",
dataType: "json",
data: JSON.stringify(dataObject)
});
fetch(url, {
method: type,
headers: {
'Content-Type': 'application/json',
'x-access-token': MSTREAMAPI.currentServer.token
// 'Content-Type': 'application/x-www-form-urlencoded',
},
body: dataObject ? JSON.stringify(dataObject) : undefined
}).then(async res => {
if (res.ok === true) {
return callback(await res.json(), false);
}
callback(res, true);
}).catch(err => {
callback(null, err);
})
// var request = $.ajax({
// url: url,
// type: type,
// contentType: "application/json",
// dataType: "json",
// data: JSON.stringify(dataObject)
// });
request.done(function (response) {
callback(response, false);
});
// request.done(function (response) {
// callback(response, false);
// });
request.fail(function (jqXHR, textStatus) {
callback(textStatus, jqXHR);
});
// request.fail(function (jqXHR, textStatus) {
// callback(textStatus, jqXHR);
// });
}
function makePOSTRequest(url, dataObject, callback) {
@ -57,6 +73,10 @@ var MSTREAMAPI = (function () {
makePOSTRequest('api/v1/playlist/save', { title: title, songs: songs }, callback);
}
mstreamModule.newPlaylist = function (title, callback) {
makePOSTRequest('api/v1/playlist/new', { title: title }, callback);
}
mstreamModule.deletePlaylist = function (playlistname, callback) {
makePOSTRequest('api/v1/playlist/delete', { playlistname: playlistname }, callback);
}
@ -93,8 +113,8 @@ var MSTREAMAPI = (function () {
makePOSTRequest("api/v1/db/artists-albums", { artist: artist }, callback);
}
mstreamModule.albumSongs = function (album, artist, callback) {
makePOSTRequest("api/v1/db/album-songs", { album: album, artist: artist }, callback);
mstreamModule.albumSongs = function (album, artist, year, callback) {
makePOSTRequest("api/v1/db/album-songs", { album, artist, year }, callback);
}
mstreamModule.dbStatus = function (callback) {
@ -152,11 +172,12 @@ var MSTREAMAPI = (function () {
bitrate: '128k',
codec: 'mp3'
};
mstreamModule.addSongWizard = function (filepath, metadata, lookupMetadata) {
mstreamModule.addSongWizard = function (filepath, metadata, lookupMetadata, position) {
// Escape filepath
var rawFilepath = filepath;
filepath = filepath.replace(/\%/g, "%25");
filepath = filepath.replace(/\#/g, "%23");
filepath = filepath.replace(/\?/g, "%3F");
if (filepath.charAt(0) === '/') {
filepath = filepath.substr(1);
}
@ -171,13 +192,18 @@ var MSTREAMAPI = (function () {
url = url + '?token=' + mstreamModule.currentServer.token;
}
var newSong = {
const newSong = {
url: url,
rawFilePath: rawFilepath,
filepath: filepath,
metadata: metadata
};
MSTREAMPLAYER.addSong(newSong);
if (position) {
MSTREAMPLAYER.insertSongAt(newSong, position, true);
} else {
MSTREAMPLAYER.addSong(newSong);
}
// perform lookup
if (lookupMetadata === true) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
// https://github.com/mfranzke/loading-attribute-polyfill
// v2.0.1
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e||self).loadingAttributePolyfill=t()}(this,function(){var e,t="loading"in HTMLImageElement.prototype,r="loading"in HTMLIFrameElement.prototype,o="onscroll"in window;function a(e){var t,r,o=[];"picture"===e.parentNode.tagName.toLowerCase()&&((r=(t=e.parentNode).querySelector("source[data-lazy-remove]"))&&t.removeChild(r),o=Array.prototype.slice.call(e.parentNode.querySelectorAll("source"))),o.push(e),o.forEach(function(e){e.hasAttribute("data-lazy-srcset")&&(e.setAttribute("srcset",e.getAttribute("data-lazy-srcset")),e.removeAttribute("data-lazy-srcset"))}),e.setAttribute("src",e.getAttribute("data-lazy-src")),e.removeAttribute("data-lazy-src")}function n(a){var n=document.createElement("div");for(n.innerHTML=function(a){var n=a.textContent||a.innerHTML,i="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 "+((n.match(/width=['"](\d+)['"]/)||!1)[1]||1)+" "+((n.match(/height=['"](\d+)['"]/)||!1)[1]||1)+"%27%3E%3C/svg%3E";return(/<img/gim.test(n)&&!t||/<iframe/gim.test(n)&&!r)&&o&&(n=void 0===e?n.replace(/(?:\r\n|\r|\n|\t| )src=/g,' lazyload="1" src='):(n=n.replace("<source",'<source srcset="'+i+'" data-lazy-remove="true"></source>\n<source')).replace(/(?:\r\n|\r|\n|\t| )srcset=/g," data-lazy-srcset=").replace(/(?:\r\n|\r|\n|\t| )src=/g,' src="'+i+'" data-lazy-src=')),n}(a);n.firstChild;){var i=n.firstChild;if(o&&void 0!==e&&i.tagName&&(("img"===i.tagName.toLowerCase()||"picture"===i.tagName.toLowerCase())&&!t||"iframe"===i.tagName.toLowerCase()&&!r)){var c="picture"===i.tagName.toLowerCase()?n.querySelector("img"):i;e.observe(c)}a.parentNode.insertBefore(i,a)}a.parentNode.removeChild(a)}window.NodeList&&!NodeList.prototype.forEach&&(NodeList.prototype.forEach=Array.prototype.forEach),"IntersectionObserver"in window&&(e=new IntersectionObserver(function(e,t){e.forEach(function(e){if(0!==e.intersectionRatio){var r=e.target;t.unobserve(r),a(r)}})},{rootMargin:"0px 0px 256px 0px",threshold:.01}));var i=function(){document.querySelectorAll("noscript.loading-lazy").forEach(function(e){return n(e)}),void 0!==window.matchMedia&&window.matchMedia("print").addListener(function(e){e.matches&&document.querySelectorAll('img[loading="lazy"][data-lazy-src],iframe[loading="lazy"][data-lazy-src]').forEach(function(e){a(e)})})};return/comp|inter/.test(document.readyState)?i():"addEventListener"in document?document.addEventListener("DOMContentLoaded",function(){i()}):document.attachEvent("onreadystatechange",function(){"complete"===document.readyState&&i()}),{prepareElement:n}});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,445 @@
// https://github.com/fredolss/rater-js
// v1.0.1
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.raterJs = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
/*! rater-js. [c] 2018 Fredrik Olsson. MIT License */
var css = require('./style.css');
module.exports = function (options) {
//private fields
var showToolTip = true;
if (typeof options.element === "undefined" || options.element === null) {
throw new Error("element required");
}
if (typeof options.showToolTip !== "undefined") {
showToolTip = !!options.showToolTip;
}
if (typeof options.step !== "undefined") {
if (options.step <= 0 || options.step > 1) {
throw new Error("step must be a number between 0 and 1");
}
}
var elem = options.element;
var reverse = options.reverse;
var stars = options.max || 5;
var starSize = options.starSize || 16;
var step = options.step || 1;
var onHover = options.onHover;
var onLeave = options.onLeave;
var rating = null;
var myRating;
elem.classList.add("star-rating");
var div = document.createElement("div");
div.classList.add("star-value");
if (reverse) {
div.classList.add("rtl");
}
div.style.backgroundSize = starSize + "px";
elem.appendChild(div);
elem.style.width = starSize * stars + "px";
elem.style.height = starSize + "px";
elem.style.backgroundSize = starSize + "px";
var callback = options.rateCallback;
var disabled = !!options.readOnly;
var disableText;
var isRating = false;
var isBusyText = options.isBusyText;
var currentRating;
var ratingText;
if (typeof options.disableText !== "undefined") {
disableText = options.disableText;
} else {
disableText = "{rating}/{maxRating}";
}
if (typeof options.ratingText !== "undefined") {
ratingText = options.ratingText;
} else {
ratingText = "{rating}/{maxRating}";
}
if (options.rating) {
setRating(options.rating);
} else {
var dataRating = elem.dataset.rating;
if (dataRating) {
setRating(+dataRating);
}
}
if (!rating) {
elem.querySelector(".star-value").style.width = "0px";
}
if (disabled) {
disable();
} //private methods
function onMouseMove(e) {
onMove(e, false);
}
/**
* Called by eventhandlers when mouse or touch events are triggered
* @param {MouseEvent} e
*/
function onMove(e, isTouch) {
if (disabled === true || isRating === true) {
return;
}
var xCoor = null;
var percent;
var width = elem.offsetWidth;
var parentOffset = elem.getBoundingClientRect();
if (reverse) {
if (isTouch) {
xCoor = e.changedTouches[0].pageX - parentOffset.left;
} else {
xCoor = e.pageX - window.scrollX - parentOffset.left;
}
var relXRtl = width - xCoor;
var valueForDivision = width / 100;
percent = relXRtl / valueForDivision;
} else {
if (isTouch) {
xCoor = e.changedTouches[0].pageX - parentOffset.left;
} else {
xCoor = e.offsetX;
}
percent = xCoor / width * 100;
}
if (percent < 101) {
if (step === 1) {
currentRating = Math.ceil(percent / 100 * stars);
} else {
var rat = percent / 100 * stars;
for (var i = 0;; i += step) {
if (i >= rat) {
currentRating = i;
break;
}
}
} //todo: check why this happens and fix
if (currentRating > stars) {
currentRating = stars;
}
elem.querySelector(".star-value").style.width = currentRating / stars * 100 + "%";
if (showToolTip) {
var toolTip = ratingText.replace("{rating}", currentRating);
toolTip = toolTip.replace("{maxRating}", stars);
elem.setAttribute("title", toolTip);
}
if (typeof onHover === "function") {
onHover(currentRating, rating);
}
}
}
/**
* Called when mouse is released. This function will update the view with the rating.
* @param {MouseEvent} e
*/
function onStarOut(e) {
if (!rating) {
elem.querySelector(".star-value").style.width = "0%";
elem.removeAttribute("data-rating");
} else {
elem.querySelector(".star-value").style.width = rating / stars * 100 + "%";
elem.setAttribute("data-rating", rating);
}
if (typeof onLeave === "function") {
onLeave(currentRating, rating);
}
}
/**
* Called when star is clicked.
* @param {MouseEvent} e
*/
function onStarClick(e) {
if (disabled === true) {
return;
}
if (isRating === true) {
return;
}
if (typeof callback !== "undefined") {
isRating = true;
myRating = currentRating;
if (typeof isBusyText === "undefined") {
elem.removeAttribute("title");
} else {
elem.setAttribute("title", isBusyText);
}
elem.classList.add("is-busy");
callback.call(this, myRating, function () {
if (disabled === false) {
elem.removeAttribute("title");
}
isRating = false;
elem.classList.remove("is-busy");
});
}
}
/**
* Disables the rater so that it's not possible to click the stars.
*/
function disable() {
disabled = true;
elem.classList.add("disabled");
if (showToolTip && !!disableText) {
var toolTip = disableText.replace("{rating}", !!rating ? rating : 0);
toolTip = toolTip.replace("{maxRating}", stars);
elem.setAttribute("title", toolTip);
} else {
elem.removeAttribute("title");
}
}
/**
* Enabled the rater so that it's possible to click the stars.
*/
function enable() {
disabled = false;
elem.removeAttribute("title");
elem.classList.remove("disabled");
}
/**
* Sets the rating
*/
function setRating(value) {
if (typeof value === "undefined") {
throw new Error("Value not set.");
}
if (value === null) {
throw new Error("Value cannot be null.");
}
if (typeof value !== "number") {
throw new Error("Value must be a number.");
}
if (value < 0 || value > stars) {
throw new Error("Value too high. Please set a rating of " + stars + " or below.");
}
rating = value;
elem.querySelector(".star-value").style.width = value / stars * 100 + "%";
elem.setAttribute("data-rating", value);
}
/**
* Gets the rating
*/
function getRating() {
return rating;
}
/**
* Set the rating to a value to inducate it's not rated.
*/
function clear() {
rating = null;
elem.querySelector(".star-value").style.width = "0px";
elem.removeAttribute("title");
}
/**
* Remove event handlers.
*/
function dispose() {
elem.removeEventListener("mousemove", onMouseMove);
elem.removeEventListener("mouseleave", onStarOut);
elem.removeEventListener("click", onStarClick);
elem.removeEventListener("touchmove", handleMove, false);
elem.removeEventListener("touchstart", handleStart, false);
elem.removeEventListener("touchend", handleEnd, false);
elem.removeEventListener("touchcancel", handleCancel, false);
}
elem.addEventListener("mousemove", onMouseMove);
elem.addEventListener("mouseleave", onStarOut);
var module = {
setRating: setRating,
getRating: getRating,
disable: disable,
enable: enable,
clear: clear,
dispose: dispose,
get element() {
return elem;
}
};
/**
* Handles touchmove event.
* @param {TouchEvent} e
*/
function handleMove(e) {
e.preventDefault();
onMove(e, true);
}
/**
* Handles touchstart event.
* @param {TouchEvent} e
*/
function handleStart(e) {
e.preventDefault();
onMove(e, true);
}
/**
* Handles touchend event.
* @param {TouchEvent} e
*/
function handleEnd(evt) {
evt.preventDefault();
onMove(evt, true);
onStarClick.call(module);
}
/**
* Handles touchend event.
* @param {TouchEvent} e
*/
function handleCancel(e) {
e.preventDefault();
onStarOut(e);
}
elem.addEventListener("click", onStarClick.bind(module));
elem.addEventListener("touchmove", handleMove, false);
elem.addEventListener("touchstart", handleStart, false);
elem.addEventListener("touchend", handleEnd, false);
elem.addEventListener("touchcancel", handleCancel, false);
return module;
};
},{"./style.css":2}],2:[function(require,module,exports){
var css = ".star-rating {\n width: 0;\n position: relative;\n display: inline-block;\n background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDguOSIgaGVpZ2h0PSIxMDMuNiIgdmlld0JveD0iMCAwIDEwOC45IDEwMy42Ij48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2UzZTZlNjt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPnN0YXJfMDwvdGl0bGU+PGcgaWQ9IkxheWVyXzIiIGRhdGEtbmFtZT0iTGF5ZXIgMiI+PGcgaWQ9IkxheWVyXzEtMiIgZGF0YS1uYW1lPSJMYXllciAxIj48cG9seWdvbiBjbGFzcz0iY2xzLTEiIHBvaW50cz0iMTA4LjkgMzkuNiA3MS4zIDM0LjEgNTQuNCAwIDM3LjYgMzQuMSAwIDM5LjYgMjcuMiA2Ni4xIDIwLjggMTAzLjYgNTQuNCA4NS45IDg4LjEgMTAzLjYgODEuNyA2Ni4xIDEwOC45IDM5LjYiLz48L2c+PC9nPjwvc3ZnPg0K);\n background-position: 0 0;\n background-repeat: repeat-x;\n cursor: pointer;\n}\n.star-rating .star-value {\n position: absolute;\n height: 100%;\n width: 100%;\n background: url('data:image/svg+xml;base64,PHN2Zw0KCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwOC45IiBoZWlnaHQ9IjEwMy42IiB2aWV3Qm94PSIwIDAgMTA4LjkgMTAzLjYiPg0KCTxkZWZzPg0KCQk8c3R5bGU+LmNscy0xe2ZpbGw6I2YxYzk0Nzt9PC9zdHlsZT4NCgk8L2RlZnM+DQoJPHRpdGxlPnN0YXIxPC90aXRsZT4NCgk8ZyBpZD0iTGF5ZXJfMiIgZGF0YS1uYW1lPSJMYXllciAyIj4NCgkJPGcgaWQ9IkxheWVyXzEtMiIgZGF0YS1uYW1lPSJMYXllciAxIj4NCgkJCTxwb2x5Z29uIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSI1NC40IDAgNzEuMyAzNC4xIDEwOC45IDM5LjYgODEuNyA2Ni4xIDg4LjEgMTAzLjYgNTQuNCA4NS45IDIwLjggMTAzLjYgMjcuMiA2Ni4xIDAgMzkuNiAzNy42IDM0LjEgNTQuNCAwIi8+DQoJCTwvZz4NCgk8L2c+DQo8L3N2Zz4NCg==');\n background-repeat: repeat-x;\n}\n.star-rating.disabled {\n cursor: default;\n}\n.star-rating.is-busy {\n cursor: wait;\n}\n.star-rating .star-value.rtl {\n -moz-transform: scaleX(-1);\n -o-transform: scaleX(-1);\n -webkit-transform: scaleX(-1);\n transform: scaleX(-1);\n filter: FlipH;\n -ms-filter: \"FlipH\";\n right: 0;\n left: auto;\n}\n"; (require("browserify-css").createStyle(css, { "href": "lib\\style.css" }, { "insertAt": "bottom" })); module.exports = css;
},{"browserify-css":3}],3:[function(require,module,exports){
'use strict';
// For more information about browser field, check out the browser field at https://github.com/substack/browserify-handbook#browser-field.
var styleElementsInsertedAtTop = [];
var insertStyleElement = function(styleElement, options) {
var head = document.head || document.getElementsByTagName('head')[0];
var lastStyleElementInsertedAtTop = styleElementsInsertedAtTop[styleElementsInsertedAtTop.length - 1];
options = options || {};
options.insertAt = options.insertAt || 'bottom';
if (options.insertAt === 'top') {
if (!lastStyleElementInsertedAtTop) {
head.insertBefore(styleElement, head.firstChild);
} else if (lastStyleElementInsertedAtTop.nextSibling) {
head.insertBefore(styleElement, lastStyleElementInsertedAtTop.nextSibling);
} else {
head.appendChild(styleElement);
}
styleElementsInsertedAtTop.push(styleElement);
} else if (options.insertAt === 'bottom') {
head.appendChild(styleElement);
} else {
throw new Error('Invalid value for parameter \'insertAt\'. Must be \'top\' or \'bottom\'.');
}
};
module.exports = {
// Create a <link> tag with optional data attributes
createLink: function(href, attributes) {
var head = document.head || document.getElementsByTagName('head')[0];
var link = document.createElement('link');
link.href = href;
link.rel = 'stylesheet';
for (var key in attributes) {
if ( ! attributes.hasOwnProperty(key)) {
continue;
}
var value = attributes[key];
link.setAttribute('data-' + key, value);
}
head.appendChild(link);
},
// Create a <style> tag with optional data attributes
createStyle: function(cssText, attributes, extraOptions) {
extraOptions = extraOptions || {};
var style = document.createElement('style');
style.type = 'text/css';
for (var key in attributes) {
if ( ! attributes.hasOwnProperty(key)) {
continue;
}
var value = attributes[key];
style.setAttribute('data-' + key, value);
}
if (style.sheet) { // for jsdom and IE9+
style.innerHTML = cssText;
style.sheet.cssText = cssText;
insertStyleElement(style, { insertAt: extraOptions.insertAt });
} else if (style.styleSheet) { // for IE8 and below
insertStyleElement(style, { insertAt: extraOptions.insertAt });
style.styleSheet.cssText = cssText;
} else { // for Chrome, Firefox, and Safari
style.appendChild(document.createTextNode(cssText));
insertStyleElement(style, { insertAt: extraOptions.insertAt });
}
}
};
},{}]},{},[1])(1)
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -34,8 +34,16 @@ var JUKEBOX = (function () {
// TODO: Check if websocket has already been created
// open connection
var l = window.location;
var wsLink = ((l.protocol === "https:") ? "wss://" : "ws://") + l.host + '?';
let wsLink = '';
if (MSTREAMAPI.currentServer.host) {
wsLink = MSTREAMAPI.currentServer.host;
wsLink = wsLink.replace('https://', 'wss://');
wsLink = wsLink.replace('http://', 'ws://');
wsLink += '?';
}else {
wsLink = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + '?';
}
if (accessKey) {
wsLink = wsLink + 'token=' + accessKey;
if (code) {
@ -47,10 +55,6 @@ var JUKEBOX = (function () {
}
mstreamModule.connection = new WebSocket(wsLink);
mstreamModule.connection.onopen = function () {
callback();
};
mstreamModule.connection.onclose = function (event) {
iziToast.warning({
title: 'Jukebox Connection Closed',
@ -89,6 +93,7 @@ var JUKEBOX = (function () {
// Handle Code
if(json.code){
mstreamModule.stats.adminCode = json.code;
callback();
}
@ -108,7 +113,7 @@ var JUKEBOX = (function () {
return;
}
if( json.command === 'addSong' && json.file){
MSTREAMAPI.addSongWizard(json.file, {}, true);
VUEPLAYERCORE.addSongWizard(json.file, {}, true);
}
};
}

View File

@ -1,6 +1,17 @@
const MSTREAMPLAYER = (() => {
const mstreamModule = {};
mstreamModule.transcodeOptions = {
serverEnabled: false,
frontendEnabled: false,
defaultBitrate: null,
defaultCodec: null,
defaultAlgo: null,
selectedBitrate: null,
selectedCodec: null,
selectedAlgo: null,
};
// Playlist variables
mstreamModule.positionCache = { val: -1 };
mstreamModule.playlist = [];
@ -29,9 +40,11 @@ const MSTREAMPLAYER = (() => {
// Scrobble function
// This is a placeholder function that the API layer can take hold of to implement the scrobble call
var scrobbleTimer;
mstreamModule.scrobble = function () {
return false;
let scrobbleTimer;
mstreamModule.scrobble = () => {
MSTREAMAPI.scrobbleByFilePath(
mstreamModule.getCurrentSong().rawFilePath,
(response, error) => {});
}
// The audioData looks like this
@ -55,42 +68,29 @@ const MSTREAMPLAYER = (() => {
return addSongToPlaylist(audioData, forceAutoPlayOff);
}
mstreamModule.getRandomSong = (callback) => {
const params = {
ignoreList: autoDjIgnoreArray,
minRating: mstreamModule.minRating,
ignoreVPaths: mstreamModule.ignoreVPaths
};
MSTREAMAPI.getRandomSong(params, function (res, err) {
if (err) {
callback(null, err);
return;
}
// Get first song from array
const firstSong = res.songs[0];
async function autoDJ() {
try {
const params = {
ignoreList: autoDjIgnoreArray,
minRating: mstreamModule.minRating,
ignoreVPaths: Object.keys(mstreamModule.ignoreVPaths).filter((vpath) => {
return mstreamModule.ignoreVPaths[vpath] === true;
})
};
const res = await MSTREAMAPI.getRandomSong(params);
autoDjIgnoreArray = res.ignoreList;
callback(firstSong, null);
});
}
function autoDJ() {
// Call mStream API for random song
mstreamModule.getRandomSong(function (res, err) {
if (err) {
mstreamModule.playerStats.autoDJ = false;
iziToast.warning({
title: 'Auto DJ Failed',
message: err.responseJSON.error ? err.responseJSON.error : '',
position: 'topCenter',
timeout: 3500
});
return;
}
VUEPLAYERCORE.addSongWizard(res.songs[0].filepath, res.songs[0].metadata);
// Add song to playlist
MSTREAMAPI.addSongWizard(res.filepath, res.metadata);
});
}catch (err) {
console.log(err);
iziToast.warning({
title: 'Auto DJ Failed',
position: 'topCenter',
timeout: 3500
});
}
}
function addSongToPlaylist(song, forceAutoPlayOff) {
@ -116,6 +116,22 @@ const MSTREAMPLAYER = (() => {
return true;
}
mstreamModule.insertSongAt = (song, position, playNow) => {
if (!song.url || song.url == false) {
return false;
}
song.error = false;
mstreamModule.playlist.splice(position, 0, song);
if (playNow) {
mstreamModule.positionCache.val = position;
goToSong(mstreamModule.positionCache.val);
}
// TODO: Check cache. Since we use this for play now only, the cache is usually preserved
}
mstreamModule.clearAndPlay = function (song) {
// Clear playlist
@ -146,11 +162,11 @@ const MSTREAMPLAYER = (() => {
// Stop the current song
return goToNextSong();
}
mstreamModule.previousSong = function () {
return goToPreviousSong();
}
mstreamModule.goToSongAtPosition = function (position) {
if (!mstreamModule.playlist[position]) {
return false;
@ -459,7 +475,7 @@ const MSTREAMPLAYER = (() => {
// Scrobble song after 30 seconds
clearTimeout(scrobbleTimer);
scrobbleTimer = setTimeout(function () { mstreamModule.scrobble() }, 30000);
scrobbleTimer = setTimeout(() => { mstreamModule.scrobble() }, 30000);
}
// Should be called whenever the "metadata" field of the current song is changed, or
@ -473,7 +489,8 @@ const MSTREAMPLAYER = (() => {
mstreamModule.playerStats.metadata.year = curSong.metadata && curSong.metadata.year ? curSong.metadata.year : "";
mstreamModule.playerStats.metadata['album-art'] = curSong.metadata && curSong.metadata['album-art'] ? curSong.metadata['album-art'] : "";
mstreamModule.playerStats.metadata['replaygain-track-db'] = curSong.metadata && curSong.metadata['replaygain-track-db'] ? curSong.metadata['replaygain-track-db'] : "";
mstreamModule.playerStats.metadata.filepath = curSong.rawFilePath;
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: mstreamModule.playerStats.metadata.title,
@ -481,7 +498,12 @@ const MSTREAMPLAYER = (() => {
album: mstreamModule.playerStats.metadata.album,
artwork: [] //TODO: Get album art working here
});
}
}
let pageTitle = (mstreamModule.playerStats.metadata.title) ?
mstreamModule.playerStats.metadata.title + ' - ' + mstreamModule.playerStats.metadata.artist : // if metadata exists
(mstreamModule.playerStats.metadata.filepath ? mstreamModule.playerStats.metadata.filepath.split('/').pop() : 'mStream Music');
document.title = pageTitle; // set page title when song is playing
mstreamModule.updateReplayGainFromSong(curSong);
}
@ -552,8 +574,15 @@ const MSTREAMPLAYER = (() => {
if (localPlayer.playerObject.paused === false) {
mstreamModule.playerStats.playing = false;
localPlayer.playerObject.pause();
document.title = "mStream Music"
} else {
localPlayer.playerObject.play();
let pageTitle = (mstreamModule.playerStats.metadata.title) ?
mstreamModule.playerStats.metadata.title + ' - ' + mstreamModule.playerStats.metadata.artist : // if metadata exists
(mstreamModule.playerStats.metadata.filepath ? mstreamModule.playerStats.metadata.filepath.split('/').pop() : 'mStream Music');
document.title = pageTitle; // set page title when song is playing
mstreamModule.playerStats.playing = true;
}
}
@ -595,6 +624,7 @@ const MSTREAMPLAYER = (() => {
currentTime: 0,
playing: false,
shouldLoop: false,
shouldLoopOne: false,
shuffle: false,
volume: 100,
metadata: {
@ -656,7 +686,20 @@ const MSTREAMPLAYER = (() => {
var curP = 'A';
function setMedia(song, player, play) {
player.playerObject.src = song.url;
let url = song.url;
if(mstreamModule.transcodeOptions.serverEnabled === true && mstreamModule.transcodeOptions.frontendEnabled === true) {
if (mstreamModule.transcodeOptions.selectedBitrate !== null) {
url += `&bitrate=${mstreamModule.transcodeOptions.selectedBitrate}`;
}
if (mstreamModule.transcodeOptions.selectedCodec !== null) {
url += `&codec=${mstreamModule.transcodeOptions.selectedCodec}`;
}
if (mstreamModule.transcodeOptions.selectedAlgo !== null) {
url += `&algo=${mstreamModule.transcodeOptions.selectedAlgo}`;
}
}
player.playerObject.src = url;
player.songObject = song;
player.playerObject.load();
player.playerObject.playbackRate = mstreamModule.playerStats.playbackRate;
@ -672,6 +715,9 @@ const MSTREAMPLAYER = (() => {
function callMeOnStreamEnd() {
mstreamModule.playerStats.playing = false;
if (mstreamModule.playerStats.shouldLoopOne === true) {
return goToSong(mstreamModule.positionCache.val);
}
// Go to next song
goToNextSong();
}
@ -721,18 +767,6 @@ const MSTREAMPLAYER = (() => {
lPlayer.playerObject.currentTime = seektime;
}
// var timers = {};
// startTime(100);
// function startTime(interval) {
// if (timers.sliderUpdateInterval) { clearInterval(timers.sliderUpdateInterval); }
// timers.sliderUpdateInterval = setInterval(() => {
// const lPlayer = getCurrentPlayer();
// mstreamModule.playerStats.currentTime = lPlayer.playerObject.currentTime;
// mstreamModule.playerStats.duration = lPlayer.playerObject.duration;
// }, interval);
// }
// Timer for caching. Helps prevent excess caching due to button mashing
var cacheTimer;
function setCachedSong(position) {
@ -752,15 +786,19 @@ const MSTREAMPLAYER = (() => {
// Loop
mstreamModule.setRepeat = (newValue) => {
if (typeof (newValue) !== "boolean") { return; }
if (mstreamModule.playerStats.autoDJ === true) { return; }
mstreamModule.playerStats.shouldLoop = newValue;
}
mstreamModule.toggleRepeat = () => {
if (mstreamModule.playerStats.autoDJ === true) { return; }
mstreamModule.playerStats.shouldLoop = !mstreamModule.playerStats.shouldLoop;
return mstreamModule.playerStats.shouldLoop;
if (mstreamModule.playerStats.shouldLoopOne === true) {
mstreamModule.playerStats.shouldLoop = false;
mstreamModule.playerStats.shouldLoopOne = false;
} else if (mstreamModule.playerStats.shouldLoop === true) {
mstreamModule.playerStats.shouldLoop = false;
mstreamModule.playerStats.shouldLoopOne = true;
} else {
mstreamModule.playerStats.shouldLoop = true;
mstreamModule.playerStats.shouldLoopOne = false;
}
}
// Random Song
@ -823,6 +861,7 @@ const MSTREAMPLAYER = (() => {
// Turn off shuffle & loop
mstreamModule.playerStats.shuffle = false;
mstreamModule.playerStats.shouldLoop = false;
mstreamModule.playerStats.shouldLoopOne = false;
// Add song if necessary
if (mstreamModule.playlist.length === 0 || mstreamModule.positionCache.val === mstreamModule.playlist.length - 1) {

View File

@ -18,17 +18,21 @@ var VUEPLAYER = (function () {
var cps;
// Hide rating popover on click
$(document).mouseup(function (e) {
if (!($(e.target).hasClass("pop-c"))) {
$("#pop").css("visibility", "hidden");
document.onmouseup = (e) => {
if(!e.target.classList.contains('pop-c')){
document.getElementById("pop").style.visibility = "hidden";
currentPopperSongIndex = false;
}
if (!($(e.target).hasClass("pop-d"))) {
$("#pop-d").css("visibility", "hidden");
if(!e.target.classList.contains('pop-d')){
document.getElementById("pop-d").style.visibility = "hidden";
cpsi = false;
}
});
if(!e.target.classList.contains('pop-f')){
document.getElementById("pop-f").style.visibility = "hidden";
}
}
Vue.component('popper-playlist-item', {
template: '<div class="pop-list-item" v-on:click="addToPlaylist($event)">&#8226; {{playlistName}}</div>',
@ -61,22 +65,22 @@ var VUEPLAYER = (function () {
// Template for playlist items
Vue.component('playlist-item', {
template: '\
<div class="noselect playlist-item" v-bind:class="{ playing: (this.index === positionCache.val), playError: (this.songError && this.songError === true) }" >\
<span class="drag-handle"><img src="assets/img/drag-handle.svg"></span><span v-on:click="goToSong($event)" class="song-area">{{ comtext }}</span>\
<div class="song-button-box">\
<span v-on:click="removeSong($event)" class="removeSong">X</span>\
<span v-on:click="createPopper($event)" class="songDropdown pop-c">\
{{ratingNumber}}<img class="star-small pop-c" src="assets/img/star.svg">\
</span>\
<span class="downloadPlaylistSong" v-on:click="downloadSong($event)">\
<svg width="12" height="12" viewBox="0 0 2048 2048" xmlns="http://www.w3.org/2000/svg"><path d="M1803 960q0 53-37 90l-651 652q-39 37-91 37-53 0-90-37l-651-652q-38-36-38-90 0-53 38-91l74-75q39-37 91-37 53 0 90 37l294 294v-704q0-52 38-90t90-38h128q52 0 90 38t38 90v704l294-294q37-37 90-37 52 0 91 37l75 75q37 39 37 91z"/></svg>\
</span>\
<span v-on:click="createPopper2($event)" class="popperMenu pop-d"><?xml version="1.0" encoding="iso-8859-1"?>\
<?xml version="1.0" encoding="iso-8859-1"?><svg class="pop-d" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 292.362 292.362" style="enable-background:new 0 0 292.362 292.362"><path class="pop-d" d="M286.935 69.377c-3.614-3.617-7.898-5.424-12.848-5.424H18.274c-4.952 0-9.233 1.807-12.85 5.424C1.807 72.998 0 77.279 0 82.228c0 4.948 1.807 9.229 5.424 12.847l127.907 127.907c3.621 3.617 7.902 5.428 12.85 5.428s9.233-1.811 12.847-5.428L286.935 95.074c3.613-3.617 5.427-7.898 5.427-12.847 0-4.948-1.814-9.229-5.427-12.85z"/></svg>\
</span>\
</div>\
</div>',
template: `
<div class="noselect playlist-item" v-bind:class="{ playing: (this.index === positionCache.val), playError: (this.songError && this.songError === true) }" >
<span class="drag-handle"><img src="assets/img/drag-handle.svg"></span><span v-on:click="goToSong($event)" class="song-area">{{ comtext }}</span>
<div class="song-button-box">
<span v-on:click="removeSong($event)" class="removeSong">X</span>
<span v-on:click="createPopper($event)" class="songDropdown pop-c">
{{ratingNumber}}<img class="star-small pop-c" src="assets/img/star.svg">
</span>
<span class="downloadPlaylistSong" v-on:click="downloadSong($event)">
<svg width="12" height="12" viewBox="0 0 2048 2048" xmlns="http://www.w3.org/2000/svg"><path d="M1803 960q0 53-37 90l-651 652q-39 37-91 37-53 0-90-37l-651-652q-38-36-38-90 0-53 38-91l74-75q39-37 91-37 53 0 90 37l294 294v-704q0-52 38-90t90-38h128q52 0 90 38t38 90v704l294-294q37-37 90-37 52 0 91 37l75 75q37 39 37 91z"/></svg>
</span>
<span v-on:click="createPopper2($event)" class="popperMenu pop-d">
<svg class="pop-d" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 292.362 292.362"><path class="pop-d" d="M286.935 69.377c-3.614-3.617-7.898-5.424-12.848-5.424H18.274c-4.952 0-9.233 1.807-12.85 5.424C1.807 72.998 0 77.279 0 82.228c0 4.948 1.807 9.229 5.424 12.847l127.907 127.907c3.621 3.617 7.902 5.428 12.85 5.428s9.233-1.811 12.847-5.428L286.935 95.074c3.613-3.617 5.427-7.898 5.427-12.847 0-4.948-1.814-9.229-5.427-12.85z"/></svg>
</span>
</div>
</div>`,
props: ['index', 'song'],
@ -96,13 +100,13 @@ var VUEPLAYER = (function () {
MSTREAMPLAYER.removeSongAtPosition(this.index, false);
},
downloadSong: function (event) {
$("#download-file").attr("href", "/media/" + this.song.filepath + "?token=" + MSTREAMAPI.currentServer.token);
document.getElementById("download-file").href = "/media/" + this.song.filepath + "?token=" + MSTREAMAPI.currentServer.token;
document.getElementById('download-file').click();
},
createPopper: function (event) {
if (currentPopperSongIndex === this.index) {
currentPopperSongIndex = false;
$("#pop").css("visibility", "hidden");
document.getElementById("pop").style.visibility = "hidden";
return;
}
var ref = event.target;
@ -116,28 +120,34 @@ var VUEPLAYER = (function () {
showClearLink.val = true
}
$('.my-rating').starRating('setRating', this.song.metadata.rating / 2);
myRater.setRating(this.song.metadata.rating / 2);
const pop = document.getElementById('pop');
new Popper(ref, pop, {
placement: 'bowrgwr', // Putting jibberish here gives us the behavior we want. It's not a bug, it's a feature
onCreate: function (data) {
$("#pop").css("visibility", "visible");
Popper.createPopper(ref, pop, {
placement: 'bottom-end',
onFirstUpdate: function (data) {
document.getElementById("pop").style.visibility = "visible";
},
modifiers: {
flip: {
boundariesElement: 'scrollParent',
modifiers: [
{
name: 'flip',
options: {
boundariesElement: 'scrollParent',
},
},
preventOverflow: {
boundariesElement: 'scrollParent'
}
}
{
name: 'preventOverflow',
options: {
boundariesElement: 'scrollParent',
},
},
]
});
},
createPopper2: function (event) {
if (cpsi === this.index) {
cpsi = false;
$("#pop-d").css("visibility", "hidden");
document.getElementById("pop-d").style.visibility = "hidden";
return;
}
var ref = event.target;
@ -146,19 +156,25 @@ var VUEPLAYER = (function () {
cps = this.song;
const pop = document.getElementById('pop-d');
new Popper(ref, pop, {
placement: 'bowrgwr', // Putting jibberish here gives us the behavior we want. It's not a bug, it's a feature
onCreate: function (data) {
$("#pop-d").css("visibility", "visible");
Popper.createPopper(ref, pop, {
placement: 'bottom-end',
onFirstUpdate: function (data) {
document.getElementById("pop-d").style.visibility = "visible";
},
modifiers: {
flip: {
boundariesElement: 'scrollParent',
modifiers: [
{
name: 'flip',
options: {
boundariesElement: 'scrollParent',
},
},
preventOverflow: {
boundariesElement: 'scrollParent'
}
}
{
name: 'preventOverflow',
options: {
boundariesElement: 'scrollParent',
},
},
]
});
},
},
@ -204,11 +220,11 @@ var VUEPLAYER = (function () {
methods: {
// checkMove is called when a drag-and-drop action happens
checkMove: function (event) {
$("#pop").css("visibility", "hidden");
document.getElementById("pop").style.visibility = "hidden";
MSTREAMPLAYER.resetPositionCache();
},
clearRating: function () {
MSTREAMAPI.rateSong(currentPopperSong.filepath, null, function (res, err) {
MSTREAMAPI.rateSong(currentPopperSong.rawFilePath, null, function (res, err) {
if(err) {
iziToast.error({
title: 'Failed to set rating',
@ -365,18 +381,12 @@ var VUEPLAYER = (function () {
}
},
fadeOverlay: function () {
if ($('#main-overlay').is(':visible')) {
$('#main-overlay').fadeOut("slow");
this.isViz = false;
} else {
this.isViz = true;
$('#main-overlay').fadeIn("slow", function() {
var isInit = VIZ.initPlayer();
if(isInit === false) {
VIZ.updateSize();
}
});
}
document.getElementById('main-overlay').classList.toggle('hide-fade');
document.getElementById('main-overlay').classList.toggle('show-fade');
this.isViz = !this.isViz;
setTimeout(() => {
VIZ.initPlayer();
}, 1);
},
toggleVolume: function () {
if (this.playerStats.volume === 0) {
@ -397,6 +407,19 @@ var VUEPLAYER = (function () {
data: {
meta: MSTREAMPLAYER.playerStats.metadata
},
methods: {
goToArtist: function() {
const el = document.createElement('DIV');
el.setAttribute('data-artist', this.meta.artist);
getArtistz(el);
},
goToAlbum: function() {
const el = document.createElement('DIV');
el.setAttribute('data-album', this.meta.album);
el.setAttribute('data-year', this.meta.year);
getAlbumsOnClick(el);
},
},
computed: {
albumArtPath: function () {
if (!this.meta['album-art']) {
@ -484,17 +507,12 @@ var VUEPLAYER = (function () {
return false;
}, false);
$(".my-rating").starRating({
const myRater = raterJs({
element: document.querySelector(".my-rating"),
step: .5,
starSize: 22,
disableAfterRate: false,
useGradient: false,
hoverColor: '#26477b',
activeColor: '#6684b2',
ratedColor: '#6684b2',
callback: function (currentRating, $el) {
// make a server call here
MSTREAMAPI.rateSong(currentPopperSong.filepath, parseInt(currentRating * 2), function (res, err) {
rateCallback: (rating, done) => {
MSTREAMAPI.rateSong(currentPopperSong.rawFilePath, parseInt(rating * 2), (res, err) => {
if(err) {
iziToast.error({
title: 'Failed to set rating',
@ -503,9 +521,10 @@ var VUEPLAYER = (function () {
});
return;
}
MSTREAMPLAYER.editSongMetadata('rating', parseInt(currentRating * 2), currentPopperSongIndex2);
MSTREAMPLAYER.editSongMetadata('rating', parseInt(rating * 2), currentPopperSongIndex2);
});
done();
}
});

View File

@ -32,8 +32,8 @@ const VUEPLAYERCORE = (() => {
return "width:0";
}
const percentage = 100 - ((this.playerStats.currentTime / this.playerStats.duration) * 100);
return `width:calc(100% - ${percentage}%)`;
const percentage = ((this.playerStats.currentTime / this.playerStats.duration) * 100);
return `width:${percentage}%`;
},
volWidthCss: function () {
return `width: ${this.playerStats.volume}%`;
@ -42,7 +42,7 @@ const VUEPLAYERCORE = (() => {
if (!this.meta['album-art']) {
return '../assets/img/default.png';
}
return `../album-art/${this.meta['album-art']}?token=${MSTREAMPLAYER.getCurrentSong().authToken}`;
return `../album-art/${this.meta['album-art']}?compress=l&token=${MSTREAMPLAYER.getCurrentSong().authToken}`;
}
},
methods: {
@ -102,7 +102,7 @@ const VUEPLAYERCORE = (() => {
}
},
template: `
<li v-on:click="goToSong($event)" class="pointer collection-item" v-bind:class="{ playing: (this.index === positionCache.val), playError: (this.songError && this.songError === true) }" >
<li v-on:click="goToSong($event)" class="pointer collection-item" v-bind:class="{ playing: (this.index === positionCache.val), playError: (this.songError && this.songError === true) }" >
<div class="playlist-text">{{ comtext }}</div>
<a v-on:click.stop="downloadSong($event)" class="secondary-content">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 12v7H5v-7H3v7c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-7h-2zm-6 .67l2.59-2.58L17 11.5l-5 5-5-5 1.41-1.41L11 12.67V3h2z"/></svg>

View File

@ -1,96 +1,95 @@
var visualizer = null;
var audioContext = new AudioContext();
var vizSettings = {
width: 800,
height: 600,
pixelRatio: window.devicePixelRatio || 1,
textureRatio: 1
}
var cycleInterval = null;
var presets = {};
var presetKeys = [];
var presetIndexHist = [];
var presetIndex = 0;
var presetCycle = true;
var presetCycleLength = 15000;
var presetRandom = true;
var isInit = false;
var renderSource = null;
function startRenderer(source) {
if(source) {
renderSource = source;
}
if(isInit === true && renderSource) {
visualizer.connectAudio(renderSource);
requestAnimationFrame(() => startRenderer());
visualizer.render();
}
}
function connectAudio(sourceNode) {
audioContext.resume();
var gainNode = audioContext.createGain();
var biquadFilter = audioContext.createBiquadFilter();
gainNode.gain.value = 1.25;
sourceNode.connect(gainNode);
gainNode.connect(biquadFilter)
startRenderer(biquadFilter);
// startRenderer(sourceNode);
}
function nextPreset(blendTime = 5.7) {
presetIndexHist.push(presetIndex);
var numPresets = presetKeys.length;
if (presetRandom) {
presetIndex = Math.floor(Math.random() * presetKeys.length);
} else {
presetIndex = (presetIndex + 1) % numPresets;
}
visualizer.loadPreset(presets[presetKeys[presetIndex]], blendTime);
$('#presetSelect').val(presetIndex);
}
function prevPreset(blendTime = 5.7) {
var numPresets = presetKeys.length;
if (presetIndexHist.length > 0) {
presetIndex = presetIndexHist.pop();
} else {
presetIndex = ((presetIndex - 1) + numPresets) % numPresets;
}
visualizer.loadPreset(presets[presetKeys[presetIndex]], blendTime);
$('#presetSelect').val(presetIndex);
}
function restartCycleInterval() {
if (cycleInterval) {
clearInterval(cycleInterval);
cycleInterval = null;
}
if (presetCycle) {
cycleInterval = setInterval(() => nextPreset(2.7), presetCycleLength);
}
}
$(function() {
$('#presetSelect').change((evt) => {
presetIndexHist.push(presetIndex);
presetIndex = parseInt($('#presetSelect').val());
visualizer.loadPreset(presets[presetKeys[presetIndex]], 5.7);
});
$('#presetCycle').change(() => {
presetCycle = $('#presetCycle').is(':checked');
restartCycleInterval();
});
$('#presetCycleLength').change((evt) => {
presetCycleLength = parseInt($('#presetCycleLength').val() * 1000);
restartCycleInterval();
});
});
var VIZ = (function () {
var VIZ = (() => {
let vizModule = {};
var visualizer = null;
var audioContext = new AudioContext();
var vizSettings = {
width: 800,
height: 600,
pixelRatio: window.devicePixelRatio || 1,
textureRatio: 1
}
var cycleInterval = null;
var presets = {};
var presetKeys = [];
var presetIndexHist = [];
var presetIndex = 0;
var presetCycle = true;
var presetCycleLength = 15000;
var presetRandom = true;
var isInit = false;
var renderSource = null;
function startRenderer(source) {
if(source) {
renderSource = source;
}
if(isInit === true && renderSource) {
visualizer.connectAudio(renderSource);
requestAnimationFrame(() => startRenderer());
visualizer.render();
}
}
function connectAudio(sourceNode) {
audioContext.resume();
var gainNode = audioContext.createGain();
var biquadFilter = audioContext.createBiquadFilter();
gainNode.gain.value = 1.25;
sourceNode.connect(gainNode);
gainNode.connect(biquadFilter)
startRenderer(biquadFilter);
// startRenderer(sourceNode);
}
function nextPreset(blendTime = 5.7) {
presetIndexHist.push(presetIndex);
var numPresets = presetKeys.length;
if (presetRandom) {
presetIndex = Math.floor(Math.random() * presetKeys.length);
} else {
presetIndex = (presetIndex + 1) % numPresets;
}
visualizer.loadPreset(presets[presetKeys[presetIndex]], blendTime);
document.getElementById('presetSelect').value = presetIndex;
}
function prevPreset(blendTime = 5.7) {
var numPresets = presetKeys.length;
if (presetIndexHist.length > 0) {
presetIndex = presetIndexHist.pop();
} else {
presetIndex = ((presetIndex - 1) + numPresets) % numPresets;
}
visualizer.loadPreset(presets[presetKeys[presetIndex]], blendTime);
document.getElementById('presetSelect').value = presetIndex;
}
function restartCycleInterval() {
if (cycleInterval) {
clearInterval(cycleInterval);
cycleInterval = null;
}
if (presetCycle) {
cycleInterval = setInterval(() => nextPreset(2.7), presetCycleLength);
}
}
// NOTE: These controls are not accessible to the user currently
// $('#presetSelect').change((evt) => {
// presetIndexHist.push(presetIndex);
// presetIndex = parseInt($('#presetSelect').val());
// visualizer.loadPreset(presets[presetKeys[presetIndex]], 5.7);
// });
// $('#presetCycle').change(() => {
// presetCycle = $('#presetCycle').is(':checked');
// restartCycleInterval();
// });
// $('#presetCycleLength').change((evt) => {
// presetCycleLength = parseInt($('#presetCycleLength').val() * 1000);
// restartCycleInterval();
// });
vizModule.connect = function (audioNode) {
connectAudio(audioNode)
}
@ -109,12 +108,19 @@ var VIZ = (function () {
visualizer.setRendererSize(vizSettings.width, vizSettings.height)
}
$( window ).resize(function() {
function reportWindowSize() {
if (!document.getElementById("viz-canvas").clientWidth || !isInit) {
return;
}
vizModule.updateSize();
});
}
window.onresize = reportWindowSize;
vizModule.toggleDom = () => {
document.getElementById('main-overlay').classList.toggle('hide-fade');
document.getElementById('main-overlay').classList.toggle('show-fade');
VIZ.initPlayer();
}
vizModule.initPlayer = function () {
if(isInit === true) {
@ -148,11 +154,11 @@ var VIZ = (function () {
canvas.width = vizSettings.width;
canvas.height = vizSettings.height;
visualizer = butterchurn.createVisualizer(audioContext, canvas, vizSettings);
visualizer = butterchurn.default.createVisualizer(audioContext, canvas, vizSettings);
nextPreset(0);
cycleInterval = setInterval(() => nextPreset(2.7), presetCycleLength);
startRenderer();
}
return vizModule;
}());
})();

BIN
webapp/build/icon.icns Normal file

Binary file not shown.

BIN
webapp/build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
webapp/build/tray-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,13 @@
const { app, BrowserWindow } = require('electron')
const path = require('path')
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
autoHideMenuBar: true,
backgroundColor: '#1e2228',
width: 1200,
height: 800
})
win.loadFile('./alpha/index.html')
win.loadFile('./index.html')
}
app.whenReady().then(() => {

View File

@ -1,25 +1,4 @@
// Check Token
async function checkToken() {
if (!localStorage.getItem('token')) {
return;
}
try {
await axios({
method: 'GET',
url: `${API.url()}/api/`,
headers: { 'x-access-token': localStorage.getItem('token') }
});
window.location.replace(`/`);
} catch (err) {
// localStorage.removeItem('token');
}
}
checkToken();
document.getElementById("login").addEventListener("submit", async e =>{
document.getElementById("login").addEventListener("submit", async e => {
e.preventDefault();
// Lock Button
@ -37,10 +16,7 @@ document.getElementById("login").addEventListener("submit", async e =>{
localStorage.setItem("token", res.data.token);
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const goTo = urlParams.get('redirect') ? urlParams.get('redirect') : '/';
window.location.replace(goTo);
window.location.assign(window.location.href.replace('/login', ''));
iziToast.success({
title: 'Login Success!',

493
webapp/old.html Normal file
View File

@ -0,0 +1,493 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>mStream Music</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta content="yes" name="mobile-web-app-capable">
<meta content="yes" name="apple-mobile-web-app-capable">
<meta content="black" name="apple-mobile-web-app-status-bar-style">
<link rel="apple-touch-icon" sizes="180x180" href="assets/fav/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="assets/fav/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/fav/favicon-16x16.png">
<link rel="manifest" href="assets/fav/site.webmanifest">
<link rel="mask-icon" href="assets/fav/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="assets/fav/favicon.ico">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="assets/fav/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<meta content="yes" name="apple-mobile-web-app-capable">
<link rel="stylesheet" href="assets/css/foundation.css" />
<link rel="stylesheet" href="assets/css/master.css">
<link rel="stylesheet" href="assets/css/mstream-player.css">
<script defer src="assets/js/lib/cookie.min.js"></script>
<!-- iziToast -->
<script src="assets/js/lib/izi-toast.js"></script>
<link rel="stylesheet" href="assets/css/izi-toast.css">
<!-- Vue JS -->
<script src="assets/js/lib/vue2.js"></script>
<!-- Sortable JS -->
<script src="assets/js/lib/sortable.js"></script>
<!-- https://github.com/SortableJS/Vue.Draggable - v2.14.1 -->
<script src="assets/js/lib/vue-sortable.js"></script>
<!-- Font -->
<link href="assets/fonts/jura.css" rel="stylesheet">
<!-- mStream Media Player -->
<script src="assets/js/mstream.player.js"></script>
<!-- These must be loaded after the player -->
<!-- They add functions onto the MSTREAM object -->
<script src="assets/js/api2.js"></script>
<script src="assets/js/mstream.jukebox.js"></script>
<!-- Star Rating System -->
<script src="assets/js/lib/star-rating.js"></script>
<script src="assets/js/lib/popper.js"></script>
<script async src="assets/js/lib/butterchurn.min.js"></script>
<script async src="assets/js/lib/butterchurn-presets.min.js"></script>
<script async src="assets/js/lib/butterchurn-presets-extra.js"></script>
<script src="assets/js/t.js"></script>
<script src="assets/js/lib/dropzone.js"></script>
<script src="assets/js/lib/hyst-modal.js"></script>
<link rel="stylesheet" href="assets/css/modal.css">
<link rel="stylesheet" href="assets/css/spinner.css">
<script defer src="assets/js/lib/qr.js"></script>
<!-- For mStream SVG Logo -->
<style>.st0,.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#264679}.st1{fill:#6684b2}</style>
</head>
<body>
<!-- Speed Modal -->
<div class="hystmodal" id="speedModal" aria-hidden="true">
<div class="hystmodal__wrap">
<div class="hystmodal__window" role="dialog" aria-modal="true">
<a data-hystclose class="hystmodal__close">Close</a>
<!-- You modal HTML markup -->
<div class="hystmodal__styled">
<div class="h1">Save Playlist</div>
<div id="speed-bar" class="speed-bar">
0.5x
<input v-model="curSpeed" type="range" max="3.5" min=".5" value="1" step=".01">
3.5x
<div>{{curSpeed}}</div>
</div>
</div>
</div>
</div>
</div>
<!-- Save Playlist Modal -->
<div class="hystmodal" id="savePlaylist" aria-hidden="true">
<div class="hystmodal__wrap">
<div class="hystmodal__window" role="dialog" aria-modal="true">
<a data-hystclose class="hystmodal__close">Close</a>
<!-- You modal HTML markup -->
<div class="hystmodal__styled">
<div class="h1">Save Playlist</div>
<form action="javascript:savePlaylist()">
<input id="playlist_name" type="text" required placeholder="Enter your playlist name" pattern="[a-zA-Z0-9 _-]+">
<input id="save_playlist" type="submit" class="" value="Save Playlist">
</form>
</div>
</div>
</div>
</div>
<!-- New Playlist Modal -->
<div class="hystmodal" id="newPlaylist" aria-hidden="true">
<div class="hystmodal__wrap">
<div class="hystmodal__window" role="dialog" aria-modal="true">
<a data-hystclose class="hystmodal__close">Close</a>
<!-- You modal HTML markup -->
<div class="hystmodal__styled">
<div class="h1">New Playlist</div>
<form id="newPlaylistForm" action="javascript:newPlaylist()">
<p>Name must be unique</p>
<input id="new_playlist_name" type="text" required placeholder="Enter your playlist name" pattern="[a-zA-Z0-9 _-]+">
<input id="new_playlist" type="submit" class="" value="Create Playlist">
</form>
</div>
</div>
</div>
</div>
<!-- Share Modal -->
<div class="hystmodal" id="sharePlaylist" aria-hidden="true">
<div class="hystmodal__wrap">
<div class="hystmodal__window" role="dialog" aria-modal="true">
<a data-hystclose class="hystmodal__close">Close</a>
<!-- You modal HTML markup -->
<div class="hystmodal__styled">
<div class="h1">Share Playlist</div>
<form action="javascript:submitShareForm()">
<div class="row collapse">
<div class="small-6 large-6 columns">
<label>Expiration Time</label>
<input id="share_time" value="14" class="form-control" type="text" pattern="[0-9]+">
<span class="share-time-post postfix radius">Days</span>
</div>
</div>
<input id="share_it" type="submit" value="Share It!">
</form>
<textarea id="share-textarea" rows="8" cols="60" placeholder="Your URL will be put here" readonly="readonly"></textarea>
</div>
</div>
</div>
</div>
<!-- Download iFrame -->
<form id="downform" action="" target="frameframe" method="POST"></form>
<iframe id="downframe" src="" width="0" height="0" tabindex="-1" title="empty" hidden name="frameframe"></iframe>
<!-- Download iFrame for GET requests -->
<a href="#" class="hide" download id="download-file" hidden></a>
<div class="off-canvas-wrap">
<div class="inner-wrap">
<!-- Top Nav Bar -->
<nav id="top-nav-bar" class="noselect tab-bar">
<section onclick="toggleMenu();" class="left-small hamburger-button">
<a class="menu-icon" title="Toggle Sidebar">
<span></span>
</a>
</section>
<section class="tab-bar-section">
<svg style='stroke-width: 0px; background-color: #FFF;' fill="#FFF" class="logo nav-logo" alt="mStream" width="181" xmlns="http://www.w3.org/2000/svg" viewBox="0 -5 620 180" style="enable-background:new 0 0 612 153"><style>.st0,.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#264679}.st1{fill:#6684b2}</style><g><path class="st0" d="M179.9 45.5c-6.2 0-11.5 1.7-15.9 5s-6.5 8.1-6.5 14.4c0 4.9 1.3 9.1 3.8 12.4 2.5 3.4 5.7 5.8 9.3 7.3 3.7 1.5 7.3 2.8 11 3.8s6.8 2.3 9.3 3.9c2.5 1.5 3.8 3.5 3.8 5.8 0 4.8-4.4 7.2-13.1 7.2h-24.1V118h24.1c17.1 0 25.6-6.7 25.6-20.2 0-1.9-.2-3.8-.6-5.8-.4-2-1.2-4-2.6-6-1.3-2.1-3.3-3.7-5.8-4.9-2.5-1.2-6.4-2.7-11.5-4.5l-8.8-3.1c-.7-.2-1.7-.7-2.9-1.3-1.3-.7-2.2-1.3-2.8-1.9-.6-.6-1.1-1.4-1.6-2.3-.5-.9-.7-2-.7-3.2 0-2 1-3.5 2.9-4.6 1.9-1.1 4.3-1.6 7-1.6h24.6V45.5h-24.5zM226.4 58.3v31c0 10.2 2.5 17.6 7.6 22 5.1 4.4 13 6.6 23.7 6.6v-12.8c-2.7 0-4.9-.2-6.8-.4-1.8-.3-3.7-.9-5.8-1.9-2-.9-3.6-2.6-4.7-4.9-1.1-2.3-1.6-5.2-1.6-8.7V58.3h18.8V45.5h-18.8V31.6L214 58.3h12.4zM281.1 118V76.8c0-7.2.9-12 2.6-14.5 1-1.3 2.2-2.2 3.6-2.8 1.4-.6 2.6-1 3.6-1.1 1-.1 2.5-.1 4.3-.1H310V45.5h-12.2c-3.6 0-6.5.1-8.6.3-2.1.2-4.5.9-7.3 2s-5.1 2.8-7.1 5c-4 4.4-6 12.4-6 24V118h12.3zM326.2 53.8c-6.2 7.4-9.3 17-9.3 28.9 0 10.7 3.2 19.4 9.5 26.2s14.7 10.1 25.3 10.1c8.7 0 16.3-2.7 22.7-8.1L366 102c-3.7 2.1-8.5 3.2-14.3 3.2-6.5 0-11.8-2.3-15.8-6.9-4-4.6-6-10.5-6-17.9 0-7 1.9-12.9 5.6-17.9 3.8-5 8.9-7.5 15.5-7.5 3.3 0 6.1.8 8.2 2.4 2.1 1.6 3.2 4 3.2 7.2 0 5-1.2 8.5-3.6 10.6-2.4 2.1-6.7 3.2-12.9 3.2h-6.7v11.7h5.7c20.3 0 30.5-8.5 30.5-25.4 0-13.6-7.9-20.7-23.7-21.5-10.8-.2-19.3 3.3-25.5 10.6zM412.3 73.2c-7.4 0-13.6 1.9-18.5 5.7-4.9 3.8-7.4 9.4-7.4 16.7 0 7.3 2.3 12.9 7 16.7 4.6 3.8 10.9 5.7 18.8 5.7h31V73.6c0-9.1-2.4-16-7.2-20.8-4.8-4.8-11.7-7.2-20.7-7.2h-22.9v12.8h22.3c10.9 0 16.4 6.1 16.4 18.2v28.7h-18.4c-9.1 0-13.6-3.2-13.6-9.8 0-3.3 1.2-5.9 3.6-7.8 2.4-1.8 5.8-2.7 10.2-2.7 5.1 0 9.4 1.4 12.9 4.3v-14c-4.9-1.4-9.3-2.1-13.5-2.1zM458.8 118H471V58.3h24.4V118h12.2V58.3h5.7c6.8 0 11.3.7 13.5 2 4.3 2.5 6.5 7.7 6.5 15.5V118h12.2V75.7c0-6-.6-11.2-1.9-15.5-1.2-4.3-3.9-7.8-7.9-10.6-3.9-2.7-9.1-4.1-15.7-4.1h-61.4V118z"/><path class="st1" d="M75 118.5v-83l21 13v70z"/><path style="fill-rule:evenodd;clip-rule:evenodd;fill:#26477b" d="M99 118.5v-69l11.5 7 10.5-7v69z"/><path class="st1" d="M124 118.5v-70l21-13v83z"/></g></svg>
</section>
<div class="audio-book-bar">
<!-- Back 30 -->
<div v-on:click="goBack(30)" class="nav-right-icon">
<svg fill="#FFF" width="28" height="28" viewBox="0 0 487 487" xmlns="http://www.w3.org/2000/svg"><g class="layer"><path d="M261.397 17.983c-88.909 0-167.372 51.302-203.909 129.073L32.072 94.282 0 109.73l52.783 109.565 109.565-52.786-15.451-32.066-57.077 27.491c30.833-65.308 96.818-108.353 171.577-108.353 104.668 0 189.818 85.154 189.818 189.821s-85.15 189.824-189.818 189.824c-61.631 0-119.663-30.109-155.228-80.539l-29.096 20.521c42.241 59.87 111.143 95.613 184.324 95.613 124.286 0 225.407-101.122 225.407-225.419S385.684 17.983 261.397 17.983z"/><text font-family="Sans-serif" font-size="24" font-weight="bold" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-width="0" text-anchor="middle" transform="matrix(12.1089 0 0 8 -1472.63 -1268.31)" x="143.282" y="199.945">30</text></g></svg>
</div>
<div class="nav-right-icon" onclick="openPlaybackModal();">
{{playbackRate}}
</div>
<!-- Forward 30 -->
<div v-on:click="goForward(30)" class="nav-right-icon">
<svg fill="#FFF" width="28" height="28" viewBox="0 0 495 460" xmlns="http://www.w3.org/2000/svg"><g class="layer"><path d="M230.603 4.983c88.909 0 167.372 51.302 203.909 129.073l25.416-52.774L492 96.73l-52.783 109.565-109.565-52.786 15.451-32.066 57.077 27.491C371.347 83.626 305.362 40.581 230.603 40.581c-104.668 0-189.818 85.154-189.818 189.821s85.15 189.824 189.818 189.824c61.631 0 119.663-30.109 155.228-80.539l29.096 20.521c-42.241 59.87-111.143 95.613-184.324 95.613-124.286 0-225.407-101.122-225.407-225.419S106.316 4.983 230.603 4.983z"/><text font-family="Sans-serif" font-size="24" font-weight="bold" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-width="0" text-anchor="middle" transform="matrix(12.1089 0 0 8 -1472.63 -1268.31)" x="140.557" y="198.32">30</text></g></svg>
</div>
</div>
</nav>
<section class="main-section">
<!-- Overlay with Visualizer -->
<div id="main-overlay" class="hide-fade main-overlay">
<div class="hide" id="presetControls">
<div>Preset: <select id="presetSelect"></select></div>
</div>
<canvas id="viz-canvas">
</canvas>
</div>
<!-- Left Nav Menu -->
<div id="responsive-left-nav" class="responsive-left-nav hide-on-small">
<div class="left-nav-menu-header">Music</div>
<ul class="left-nav-menu">
<li onclick="loadFileExplorer(this);">
<?xml version="1.0" encoding="utf-8"?><svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="6 6 40 40" style="enable-background:new 0 0 48 48"><path d="M16.516 20.688C16.266 21.25 12 31.906 12 31.906V17c0-.55.45-1 1-1h1.334l.35-1.052C14.857 14.427 15.45 14 16 14h5c.55 0 1.143.427 1.316.948l.35 1.052H32c.55 0 1 .45 1 1v3H17.5c-.275 0-.734.125-.984.688zM41 21H19c-.55 0-1.167.418-1.371.929l-5.258 13.143c-.204.51.079.928.629.928h22c.55 0 1.167-.418 1.371-.929l5.258-13.143c.204-.51-.079-.928-.629-.928z"/></svg>
File Explorer
</li>
<li onclick="getAllPlaylists(this);">
<?xml version="1.0" encoding="utf-8"?><svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="8 5 38 38" style="enable-background:new 0 0 48 48"><path d="M37.192 23.032c-.847.339-.179-.339-.179-.339s1.422-2.092.406-3.786c-.793-1.321-3.338-1.075-4.42-1.669v14.154c0 .037-.016.07-.022.106-.154 1.504-1.607 3.034-3.696 3.712-2.559.829-5.102.063-5.678-1.711-.574-1.774 1.034-3.887 3.595-4.717.66-.189 2.207-.439 2.801-.193V12.607a.609.609 0 0 1 .608-.607h1.785c.336 0 .608.273.608.607v.549c1.542 1.004 6.18 1.455 6.851 4.139.805 3.225-1.813 5.398-2.659 5.737zM12.5 20H28v-3H12.5c-.275 0-.5.225-.5.5v2c0 .275.225.5.5.5zm0 6H28v-3H12.5c-.275 0-.5.225-.5.5v2c0 .275.225.5.5.5zm10.125 3H12.5c-.275 0-.5.225-.5.5v2c0 .275.225.5.5.5h8.551c.176-1.075.728-2.113 1.574-3z"/></svg>
Playlists
</li>
<li onclick="getAllAlbums(this);" >
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="10 12 28 28" style="enable-background:new 0 0 48 48"><path d="M11 17v14a1 1 0 0 1-1-1V18a1 1 0 0 1 1-1zM37 17v14a1 1 0 0 0 1-1V18a1 1 0 0 0-1-1zM13 16h1v16h-1a1 1 0 0 1-1-1V17a1 1 0 0 1 1-1zM35 16h-1v16h1a1 1 0 0 0 1-1V17a1 1 0 0 0-1-1zM32 15H16c-.55 0-1 .45-1 1v16c0 .55.45 1 1 1h16c.55 0 1-.45 1-1V16c0-.55-.45-1-1-1zm-3 12c0 1.469-2.022 1.71-2.301 1.71-.846 0-1.55-.395-1.752-1.02-.276-.852.461-1.796 1.607-2.218.649-.238 1.446-.155 1.446-.146v-4.407l-6 1.369V28c0 1.813-2.102 2.057-2.417 2.057-.816 0-1.44-.374-1.629-.955-.271-.835.441-1.77 1.603-2.174.646-.225 1.443-.137 1.443-.131v-6.604c0-.245.072-.469.291-.529l7.491-1.854-.039-.01c.218 0 .257.169.257.393V27z"/></svg>
Albums
</li>
<li onclick="getAllArtists(this);">
<?xml version="1.0" encoding="utf-8"?><svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><path d="M611.4 34.4c92-46.8 206.7-25.7 276.3 58.1 66.9 80.5 71.4 193.8 18.5 278.4-13.2.1-27-2.3-41.2-6.6-56-17.1-114.7-64.1-161.8-122.1-47.3-58.2-82-126-90.1-184.6-1-8-1.6-15.7-1.7-23.2zM146.8 678c-.7 41.6 19.8 64.6 59 71l431.9-297.1C578.1 413.3 527.3 349.7 501 282.7L146.8 678zm14.1 94.3c-35.9 38.4-46.8 78.1-38.3 107.7 3.5 12.1 10.4 22.5 20.2 30.3 10.4 8.3 24.5 13.8 41.6 15.3 58.5 5.1 146-33.6 248.3-150.9 102.8-118 195.4-173.9 271.3-187.5 45.9-8.2 86.3-1.4 119.7 16.6 33.4 18 59.4 47 76.7 83.1 27 56.4 32.6 130.2 12 205.2l-60-17.2c16.3-59.8 12.6-117.4-7.9-160-11.5-24-28.4-43.1-49.8-54.6-21.5-11.5-48.3-15.8-79.9-10.1-63.2 11.4-143 61-235.4 167-117.5 134.9-224.8 178.7-300 172.2-29.6-2.6-54.8-12.8-74.6-28.4-20.4-16.1-34.7-37.9-41.9-63.2-14.3-50.1.3-113.2 53.1-169.7l44.9 44.2zM871.5 412c-69.8-23.6-139.6-79.4-194.4-146.8-49.9-61.3-88.3-133.3-103.8-200.3-41 51.7-57.7 118-48.6 181.6 25.1 77.2 90.1 157.1 162.7 191.2 60.8 18 127.9 10 184.1-25.7z"/></svg>
Artists
</li>
<li onclick="getRecentlyAdded(this);">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/><path d="M0 0h24v24H0z" fill="none"/><path d="M12.5 7H11v6l5.25 3.15.75-1.23-4.5-2.67z"/></svg>
Recent
</li>
<li onclick="getRatedSongs(this);">
<?xml version="1.0" encoding="iso-8859-1"?><svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="-3 0 62 62" style="enable-background:new 0 0 53.867 53.867"><path style="fill:#efce4a" d="M26.934 1.318l8.322 16.864 18.611 2.705L40.4 34.013l3.179 18.536-16.645-8.751-16.646 8.751 3.179-18.536L0 20.887l18.611-2.705z"/></svg>
Rated
</li>
<li onclick="setupSearchPanel(undefined, this);">
<svg viewBox="-150 -50 1224 1174" height="24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M960 832L710.875 582.875C746.438 524.812 768 457.156 768 384 768 171.969 596 0 384 0 171.969 0 0 171.969 0 384c0 212 171.969 384 384 384 73.156 0 140.812-21.562 198.875-57L832 960c17.5 17.5 46.5 17.375 64 0l64-64c17.5-17.5 17.5-46.5 0-64zM384 640c-141.375 0-256-114.625-256-256s114.625-256 256-256 256 114.625 256 256-114.625 256-256 256z"></path></svg>
Search
</li>
</ul>
<div class="left-nav-menu-header">Utilities</div>
<ul class="left-nav-menu">
<li onclick="getMobilePanel(this);">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M15.5 1h-8C6.12 1 5 2.12 5 3.5v17C5 21.88 6.12 23 7.5 23h8c1.38 0 2.5-1.12 2.5-2.5v-17C18 2.12 16.88 1 15.5 1zm-4 21c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4.5-4H7V4h9v14z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
Mobile
</li>
<li onclick="autoDjPanel(this);">
<svg xmlns="http://www.w3.org/2000/svg" fill="#DDD" width="24" height="24" viewBox="0 0 55.334 55.334"><g><circle cx="27.667" cy="27.667" r="3.618"/><path d="M27.667 0C12.387 0 0 12.387 0 27.667s12.387 27.667 27.667 27.667 27.667-12.387 27.667-27.667S42.947 0 27.667 0zM17.118 6.881a23.213 23.213 0 0111.214-2.509c.367.01.619.922.564 2.025l-.282 5.677c-.055 1.103-.289 1.986-.523 1.979a13.577 13.577 0 00-6.027 1.196c-1.007.455-2.212.184-2.774-.767l-2.896-4.897c-.562-.951-.261-2.203.724-2.704zm-1.132 10.414l-4.278-3.742c-.832-.727-.918-1.994-.119-2.756l.057-.053c.802-.76 2.059-.605 2.737.266l3.494 4.484c.679.871.837 1.889.391 2.314-.447.427-1.45.214-2.282-.513zm1.891 10.372c0-5.407 4.383-9.79 9.79-9.79s9.79 4.383 9.79 9.79-4.383 9.79-9.79 9.79-9.79-4.383-9.79-9.79zM38.17 48.476a23.21 23.21 0 01-11.244 2.484c-.409-.013-.692-.929-.632-2.032l.31-5.676c.061-1.103.322-1.981.586-1.972a13.596 13.596 0 005.656-1.01c1.022-.42 2.275-.144 2.877.782l3.101 4.77c.602.925.332 2.155-.654 2.654zm5.449-3.82c-.766.72-2.005.551-2.703-.305l-3.59-4.407c-.698-.856-.876-1.848-.435-2.255.442-.407 1.443-.179 2.274.549l4.28 3.744c.832.727.941 1.954.174 2.674z"/></g></svg>
Auto DJ
</li>
<li onclick="setupTranscodePanel(this);">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 48 48"><path id="ffmpeg-logo" fill="none" stroke="#DDD" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M8.5 8.5h9l-9 9v11l20-20h11l-31 31h11l20-20v11l-9 9h9"/></svg>
Transcode
</li>
<li v-bind:class="{ 'aux-button-active-2': jukebox.live }" onclick="setupJukeboxPanel(this);">
<svg v-bind:class="{ 'aux-button-active-2': jukebox.live }" height="24" width="24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 65 65" enable-background="new 0 0 65 65" xml:space="preserve"><g><path d="M36.1,14.4c-1.4-1.4-3.9-1.4-5.3,0L1.1,44.1c-1.4,1.4-1.4,3.8,0,5.3l14.5,14.5c0.7,0.7,1.6,1.1,2.6,1.1 c1,0,1.9-0.4,2.6-1.1l29.7-29.7c1.5-1.4,1.5-3.8,0-5.3L36.1,14.4z M49.5,33.2L19.8,62.9c-0.8,0.8-2.3,0.8-3.2,0L2.1,48.3 c-0.9-0.9-0.9-2.3,0-3.2l29.7-29.7c0.4-0.4,1-0.7,1.6-0.7c0.6,0,1.2,0.2,1.6,0.7L49.5,30C50.4,30.9,50.4,32.3,49.5,33.2z"></path><rect x="32.4" y="19.5" transform="matrix(0.707 0.7072 -0.7072 0.707 24.3992 -18.4628)" width="4.2" height="1.5"></rect><rect x="42.7" y="29.8" transform="matrix(-0.7072 -0.707 0.707 -0.7072 54.8735 83.7577)" width="4.2" height="1.5"></rect><rect x="14.7" y="37.1" transform="matrix(0.7071 0.7071 -0.7071 0.7071 31.7025 -0.8228)" width="4.2" height="1.5"></rect><rect x="25" y="47.4" transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 12.2855 101.3977)" width="4.2" height="1.5"></rect><rect x="19.9" y="42.3" transform="matrix(0.7071 0.7071 -0.7071 0.7071 36.8513 -2.9555)" width="4.2" height="1.5"></rect><rect x="11.7" y="40.2" transform="matrix(0.7071 0.7071 -0.7071 0.7071 32.9629 2.2207)" width="4.2" height="1.5"></rect><rect x="22" y="50.5" transform="matrix(-0.707 -0.7072 0.7072 -0.707 4.9277 104.4377)" width="4.2" height="1.5"></rect><rect x="16.8" y="45.3" transform="matrix(0.7071 0.7071 -0.7071 0.7071 38.1117 8.766792e-002)" width="4.2" height="1.5"></rect><rect x="8.6" y="43.2" transform="matrix(0.7071 0.7071 -0.7071 0.7071 34.2235 5.2638)" width="4.2" height="1.5"></rect><rect x="18.9" y="53.5" transform="matrix(-0.7072 -0.707 0.707 -0.7072 -2.4064 107.4868)" width="4.2" height="1.5"></rect><rect x="13.8" y="48.3" transform="matrix(-0.707 -0.7072 0.7072 -0.707 -7.5673 95.0498)" width="4.2" height="1.5"></rect><rect x="10.8" y="51.4" transform="matrix(-0.7072 -0.707 0.707 -0.7072 -14.9034 98.0997)" width="4.2" height="1.5"></rect><path d="M35.4,38.8c-1.1,1.1-2.4,1.7-3.9,1.9l0.2,1.5c1.8-0.2,3.5-1,4.8-2.3c1.3-1.3,2.1-3,2.3-4.8l-1.5-0.2 C37.1,36.4,36.5,37.8,35.4,38.8z"></path><path d="M31.7,26.2l-0.2,1.5c1.5,0.2,2.8,0.8,3.9,1.9c1.1,1.1,1.7,2.4,1.9,3.9l1.5-0.2c-0.2-1.8-1-3.5-2.3-4.8 C35.2,27.2,33.5,26.4,31.7,26.2z"></path><path d="M26.2,29.6c1.1-1.1,2.4-1.7,3.9-1.9l-0.2-1.5c-1.8,0.2-3.5,1-4.8,2.3c-1.3,1.3-2.1,3-2.3,4.8l1.5,0.2 C24.5,32,25.1,30.6,26.2,29.6z"></path><path d="M24.3,34.9l-1.5,0.2c0.2,1.8,1,3.5,2.3,4.8c1.3,1.3,3,2.1,4.8,2.3l0.2-1.5c-1.5-0.2-2.9-0.8-3.9-1.9 C25.1,37.8,24.5,36.4,24.3,34.9z"></path><path d="M43.4,0v1.5c11.1,0,20.1,9,20.1,20.1H65C65,9.7,55.3,0,43.4,0z"></path><path d="M43.4,7.4v1.5c7,0,12.7,5.7,12.7,12.7h1.5C57.6,13.8,51.2,7.4,43.4,7.4z"></path><path d="M48.6,21.6h1.5c0-3.7-3-6.7-6.7-6.7v1.5C46.3,16.4,48.6,18.7,48.6,21.6z"></path></g></svg>
Jukebox
</li>
</ul>
<div class="left-nav-menu-header">Navigation</div>
<ul class="left-nav-menu">
<a href="admin/" target="__blank">
<li class="admin-panel-button">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path clip-rule="evenodd" d="M0 0h24v24H0z" fill="none"/><path d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z"/></svg>
Server Admin
</li>
</a>
<li onclick="logout();">
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M10.09 15.59L11.5 17l5-5-5-5-1.41 1.41L12.67 11H3v2h9.67l-2.58 2.59zM19 3H5c-1.11 0-2 .9-2 2v4h2V5h14v14H5v-4H3v4c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>
Logout
</li>
</ul>
<div class="db-status-bar">
<p id="scan-status" class="scan-status metadata-panel-text"></p>
<p id="scan-status-files" class="scan-status-files metadata-panel-text"></p>
</div>
</div>
<!-- content goes in here -->
<div class="row ohidden player-body">
<!-- Tabs for Mobile -->
<dl class="tabs show-for-small-only">
<dd id="activate-panel-1" class="active" onclick="activePanel1()">
<a class="panel_one_name" href="javascript:void(0);">Library</a>
</dd>
<dd id="activate-panel-2" onclick="activePanel2()">
<a href="javascript:void(0);">Now Playing</a>
</dd>
</dl>
<div class="tabs-content">
<div class="content active" id="panel1">
<div class="large-12 columns libraryColumn">
<div class="columnHeader">
<div class="libraryHeaderContainer">
<div class="large-12 medium-12 small-12 columns noPaddingLeft">
<h3 class="panel_one_name noselect hide-for-small">&nbsp</h3>
</div>
</div>
<!-- /libraryHeaderContainer -->
<div class="clear flatline"></div>
<div id="directory_bar" class="clear directoryTitle">
<button onclick="onBackButton();" class="backButton noselect tiny">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="12 14 24 24" alt="Back" width="15" height="15"><g><path d="M36 29.131c-2.813-4.089-9-3.979-14.465-2.018l1.232 3.926c.067.217-.058.328-.27.251l-10.17-3.766c-.212-.076-.281-.294-.151-.479l6.196-8.903c.13-.185.296-.162.365.053 0 0 .904 2.852 1.127 3.592C29 19.529 35.471 24.294 36 29.131z"/></g></svg>
</button>
<h4 id="directoryName" class="directoryName"></h4>
<!-- Current Directory -->
<input onkeyup="runLocalSearch(this)" id="search_folders" class="hide" placeholder="Search List">
<a title="Search" onclick="onSearchButtonClick();" class="noselect hover-fill">
<svg viewBox="-150 -150 1224 1174" height="27" width="27" xmlns="http://www.w3.org/2000/svg"><path d="M960 832L710.875 582.875C746.438 524.812 768 457.156 768 384 768 171.969 596 0 384 0 171.969 0 0 171.969 0 384c0 212 171.969 384 384 384 73.156 0 140.812-21.562 198.875-57L832 960c17.5 17.5 46.5 17.375 64 0l64-64c17.5-17.5 17.5-46.5 0-64zM384 640c-141.375 0-256-114.625-256-256s114.625-256 256-256 256 114.625 256 256-114.625 256-256 256z"/></svg>
</a>
<a title="Add All Songs to Playlist" class="noselect add hover-fill" onclick='addAll();'>
<svg xmlns="http://www.w3.org/2000/svg" width="27" height="27" viewBox="8 6 34 34"><g><path d="M36 35.5c0 .275-.225.5-.5.5h-23a.501.501 0 0 1-.5-.5s.019-10.651.033-10.693l3.723-11.316c.088-.269.387-.49.665-.49H27v3h-9.063l-3.032 9H19l2 5h6l2-5h4.095l-1.347-4h2.973l1.246 3.807C35.98 24.849 36 35.5 36 35.5zM34 9c-2.761 0-5 2.016-5 5 0 3.206 2.239 5 5 5 2.762 0 5-2.238 5-5s-2.238-5-5-5zm3 5.5c0 .275-.225.5-.5.5H35v1.5c0 .275-.225.5-.5.5h-1a.501.501 0 0 1-.5-.5V15h-1.5a.501.501 0 0 1-.5-.5v-1c0-.275.225-.5.5-.5H33v-1.5c0-.275.225-.5.5-.5h1c.275 0 .5.225.5.5V13h1.5c.275 0 .5.225.5.5v1z"/></g></svg>
</a>
</div>
</div>
<!-- Upload progress bar-->
<div class="upload-progress-bar">
<div style="width:0%" id="upload-progress-inner" class="upload-progress-inner"></div>
</div>
<div class="clear col scroll noselect" id="filelist">
<div class="filez">Loading...</div>
</div>
<div style="visibility:hidden;" class="pop-f" id="pop-f" role="tooltip">
<div class="pop-f pop-playlist">Add To Playlist:</div>
</div>
</div>
</div>
<!-- Playlist -->
<div class="content" id="panel2">
<div class="large-12 columns playlistColumn" id="playlist_container">
<div class="columnHeader noselect">
<div class="large-6 medium-6 small-12 columns noPaddingLeft">
<h3 class="hide-for-small">Now Playing</h3>
</div>
<div savePlaylist="large-6 medium-6 small-12 columns">
<div class="controls rightControls">
<a title="Save Playlist" onclick="openSaveModal();">
<?xml version="1.0" encoding="iso-8859-1"?><svg width="27" height="27" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 49 49" style="enable-background:new 0 0 49 49"><path d="M27.5 5h6v10h-6z"/><path d="M39.914 0H.5v49h48V8.586L39.914 0zM10.5 2h26v16h-26V2zm29 45h-31V26h31v21z"/><path d="M13.5 32h7a1 1 0 1 0 0-2h-7a1 1 0 1 0 0 2zM13.5 36h10a1 1 0 1 0 0-2h-10a1 1 0 1 0 0 2zM26.5 36c.27 0 .52-.11.71-.29.18-.19.29-.45.29-.71s-.11-.521-.29-.71c-.37-.37-1.04-.37-1.41 0a.996.996 0 0 0-.3.71c0 .27.109.52.29.71.189.18.439.29.71.29z"/></svg>
</a>
<a title="Download Playlist" class="downloadPlaylist" onclick="downloadPlaylist();">
<?xml version="1.0" encoding="utf-8"?><svg width="25" height="25" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><path d="M763.8 839.2c0-10.2-3.7-19-11.2-26.5-7.5-7.5-16.3-11.2-26.5-11.2s-19 3.7-26.5 11.2c-7.5 7.5-11.2 16.3-11.2 26.5s3.7 19 11.2 26.5c7.5 7.5 16.3 11.2 26.5 11.2s19-3.7 26.5-11.2c7.5-7.4 11.2-16.3 11.2-26.5zm150.8 0c0-10.2-3.7-19-11.2-26.5-7.5-7.5-16.3-11.2-26.5-11.2s-19 3.7-26.5 11.2c-7.5 7.5-11.2 16.3-11.2 26.5s3.7 19 11.2 26.5c7.5 7.5 16.3 11.2 26.5 11.2s19-3.7 26.5-11.2c7.5-7.4 11.2-16.3 11.2-26.5zM990 707.3v188.5c0 15.7-5.5 29.1-16.5 40s-24.3 16.5-40 16.5h-867c-15.7 0-29.1-5.5-40-16.5-11-11-16.5-24.3-16.5-40V707.3c0-15.7 5.5-29.1 16.5-40 11-11 24.3-16.5 40-16.5h273.9l79.5 80.1c22.8 22 49.5 33 80.1 33 30.6 0 57.3-11 80.1-33l80.1-80.1h273.3c15.7 0 29.1 5.5 40 16.5 11 11 16.5 24.3 16.5 40zM798.6 372.2c6.7 16.1 3.9 29.8-8.2 41.2L526.5 677.3c-7.1 7.5-15.9 11.2-26.5 11.2s-19.4-3.7-26.5-11.2L209.7 413.4c-12.2-11.4-14.9-25.1-8.2-41.2 6.7-15.3 18.3-23 34.7-23H387V85.4c0-10.2 3.7-19 11.2-26.5 7.5-7.5 16.3-11.2 26.5-11.2h150.8c10.2 0 19 3.7 26.5 11.2 7.5 7.5 11.2 16.3 11.2 26.5v263.8H764c16.3 0 27.9 7.7 34.6 23z"/></svg>
</a>
<a title="Share Playlist" onclick="openShareModal();">
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 50 50"><path d="M 40 0 C 34.536 0 30.079 4.39775 30 9.84375 C 29.999 9.89275 30 9.951 30 10 C 30 13.688 31.99675 16.891 34.96875 18.625 C 36.44575 19.49 38.167 20 40 20 C 45.514 20 50 15.514 50 10 C 50 4.486 45.514 0 40 0 z M 28.0625 10.84375 L 17.84375 15.96875 C 20.22275 18.03275 21.78375 20.99975 21.96875 24.34375 L 32.3125 19.15625 C 29.8965 17.12825 28.3005 14.17675 28.0625 10.84375 z M 10 15 C 4.486 15 0 19.486 0 25 C 0 30.514 4.486 35 10 35 C 12.051 35 13.94325 34.3765 15.53125 33.3125 C 18.21425 31.5205 20 28.471 20 25 C 20 21.411 18.09 18.264 15.25 16.5 C 13.72 15.546 11.931 15 10 15 z M 21.96875 25.65625 C 21.78475 28.99625 20.249 31.9975 17.875 34.0625 L 28.0625 39.15625 C 28.2995 35.82325 29.86925 32.87375 32.28125 30.84375 L 21.96875 25.65625 z M 40 30 C 37.937 30 36.0305 30.64475 34.4375 31.71875 C 31.7705 33.51475 30 36.543 30 40 C 30 40.015 30 40.01725 30 40.03125 C 29.956 40.03425 29.919 40.0595 29.875 40.0625 L 30 40.125 C 30.066 45.583 34.527 50 40 50 C 45.514 50 50 45.514 50 40 C 50 34.486 45.514 30 40 30 z"/></svg>
</a>
<a title="Clear Playlist" onclick="MSTREAMPLAYER.clearPlaylist();">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="11 11 26 26" width="25" height="25" style="enable-background:new 0 0 48 48"><g fill="#C33"><path d="M32.662 15.338c-4.784-4.784-12.54-4.784-17.324 0s-4.784 12.54 0 17.324 12.54 4.784 17.324 0c4.783-4.784 4.784-12.54 0-17.324zm-5.127 14.318a.5.5 0 0 1-.707 0L24 26.828l-2.828 2.828a.5.5 0 0 1-.707 0l-2.121-2.121a.5.5 0 0 1 0-.707L21.172 24l-2.828-2.828a.5.5 0 0 1 0-.707l2.121-2.121a.5.5 0 0 1 .707 0L24 21.172l2.828-2.828a.5.5 0 0 1 .707 0l2.121 2.121a.5.5 0 0 1 0 .707L26.828 24l2.828 2.828a.5.5 0 0 1 0 .707l-2.121 2.121z"/></g></svg>
</a>
</div>
</div>
</div>
<div class="clear flatline"></div>
<!-- This section is tied into the MSTREAMPLAYER module with Vue -->
<!-- see file: mstream.vue-player-controls.js -->
<div class="clear col scroll scrollBoxHeight3">
<draggable :list="playlist" handle=".drag-handle" @end="checkMove" id="playlist">
<div v-for="(song, index) in playlist" is="playlist-item" :key="index" :index="index" :song="song">
</div>
<div style="visibility:hidden;" class="pop-c" id="pop" role="tooltip">
<div class="my-rating pop-c"></div>
<a v-show="showClear.val === true" v-on:click="clearRating">clear</a>
</div>
<div style="visibility:hidden;" class="pop-d" id="pop-d" role="tooltip">
<div class="pop-d pop-playlist">Add To Playlist:</div>
<div v-for="(playlist, index) in playlists" is="popper-playlist-item" :key="index" :index="index" :playlist="playlist"></div>
</div>
</draggable>
</div>
<div id="meta-box" class="meta-box">
<div class="player-spacer"></div>
<div class="meta-spacer"></div>
<div class="meta-album-art noselect">
<img :src="albumArtPath" />
</div>
<div class="meta-info">
<p v-cloak class="metadata-panel-text">
Title: {{ (meta.title) ? meta.title : '' }}
</p>
<p v-cloak class="metadata-panel-text">Artist:
<span v-on:click="goToArtist">{{ (meta.artist) ? meta.artist : '' }}</span>
</p>
<p v-cloak class="metadata-panel-text">Album:
<span v-on:click="goToArtist">{{ (meta.album) ? meta.album : '' }}</span>
</p>
<p v-cloak class="metadata-panel-text">
Year: {{ (meta.year) ? meta.year : '' }}
</p>
<p v-cloak class="metadata-panel-text">
Gain: {{ (meta['replaygain-track-db']) ? String(meta['replaygain-track-db']) + " db" : 'n/a' }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- /row -->
<!-- Media Player
This section is tied into the MSTREAMPLAYER module with Vue
see file: mstream.vue-player-controls.js -->
<div id="mstream-player" class="mstream-player noselect">
<div class="player-spacer"></div>
<div class="player-spacer"></div>
<div class="player-ticker-layer">
<div id="progress-bar" class="progress-bar">
<div class="titlebar">
<div v-cloak id="title-text" class="title-text">{{currentSongText}}</div>
<div v-cloak class="duration-text">{{showTime}}</div>
</div>
<div class="pbar" :style="widthcss"></div>
</div>
</div>
<div class="player-spacer"></div>
<div class="player-button-layer">
<div id="previous-button" class="previous-button">
<img class="noselect previous-image center" src="assets/img/previous-white.svg" title="Previous">
</div>
<div id="play-pause-button" class="play-pause-button">
<img id="play-pause-image" class="noselect play-pause-image center" :src="imgsrc" title="Pause/Play">
</div>
<div id="next-button" class="next-button">
<img class="center noselect" src="assets/img/next-white.svg" title="Next">
</div>
<div id="player-overlay-button" class="player-overlay-button">
<span title="Butterchurn Visualizer">
<svg v-bind:class="{ 'aux-button-active': isViz }" style="cursor:pointer;" v-on:click="fadeOverlay" class="center" height="36" width="36" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M82.405 313.596c0 5.449-4.347 9.76-9.212 9.76H20.751c-5.419 0-9.736-4.311-9.736-9.76v-21.054c0-5.409 4.317-9.761 9.736-9.761h52.442c4.865 0 9.212 4.352 9.212 9.761v21.054zm0 49.759c0 5.411-4.347 9.763-9.212 9.763H20.751c-5.419 0-9.736-4.352-9.736-9.763v-21.05c0-5.459 4.317-9.76 9.736-9.76h52.442c4.865 0 9.212 4.301 9.212 9.76v21.05zm0 49.765c0 5.408-4.347 9.763-9.212 9.763H20.751c-5.419 0-9.736-4.354-9.736-9.763v-21.103c0-5.41 4.317-9.712 9.736-9.712h52.442c4.865 0 9.212 4.302 9.212 9.712v21.103zm164.946 0a9.726 9.726 0 0 1-9.751 9.763h-51.903a9.72 9.72 0 0 1-9.746-9.763v-21.103c0-5.41 4.327-9.712 9.746-9.712H237.6c5.419 0 9.751 4.302 9.751 9.712v21.103zm0-99.524c0 5.449-4.333 9.76-9.751 9.76h-51.903c-5.419 0-9.746-4.311-9.746-9.76v-21.054a9.719 9.719 0 0 1 9.746-9.761H237.6a9.723 9.723 0 0 1 9.751 9.761v21.054zm0-49.711c0 5.399-4.333 9.71-9.751 9.71h-51.903c-5.419 0-9.746-4.311-9.746-9.71v-21.104a9.718 9.718 0 0 1 9.746-9.761H237.6a9.722 9.722 0 0 1 9.751 9.761v21.104zm0-49.762c0 4.874-4.333 9.711-9.751 9.711h-51.903c-5.419 0-9.746-4.837-9.746-9.711v-21.628c0-4.885 4.327-9.186 9.746-9.186H237.6c5.419 0 9.751 4.301 9.751 9.186v21.628zm82.185 49.762c0 5.399-4.299 9.71-9.186 9.71h-52.451c-5.408 0-9.751-4.311-9.751-9.71v-21.104a9.73 9.73 0 0 1 9.751-9.761h52.451c4.887 0 9.186 4.351 9.186 9.761v21.104zm0 49.711c0 5.449-4.299 9.76-9.186 9.76h-52.451a9.704 9.704 0 0 1-9.751-9.76v-21.054a9.73 9.73 0 0 1 9.751-9.761h52.451c4.887 0 9.186 4.352 9.186 9.761v21.054zm0 49.759c0 5.411-4.299 9.763-9.186 9.763h-52.451a9.73 9.73 0 0 1-9.751-9.763v-21.05a9.698 9.698 0 0 1 9.751-9.76h52.451c4.887 0 9.186 4.301 9.186 9.76v21.05zm0 49.765c0 5.408-4.299 9.763-9.186 9.763h-52.451a9.732 9.732 0 0 1-9.751-9.763v-21.103c0-5.41 4.343-9.712 9.751-9.712h52.451c4.887 0 9.186 4.302 9.186 9.712v21.103zm82.752 0a9.72 9.72 0 0 1-9.761 9.763h-52.442c-4.852 0-9.185-4.354-9.185-9.763v-21.103c0-5.41 4.333-9.712 9.185-9.712h52.442c5.439 0 9.761 4.302 9.761 9.712v21.103zm0-49.765a9.719 9.719 0 0 1-9.761 9.763h-52.442c-4.852 0-9.185-4.352-9.185-9.763v-21.05c0-5.459 4.333-9.76 9.185-9.76h52.442c5.439 0 9.761 4.301 9.761 9.76v21.05zm0-49.759c0 5.449-4.321 9.76-9.761 9.76h-52.442c-4.852 0-9.185-4.311-9.185-9.76v-21.054c0-5.409 4.333-9.761 9.185-9.761h52.442a9.719 9.719 0 0 1 9.761 9.761v21.054zm88.698-196.278c0 5.408-4.333 9.187-9.743 9.187h-52.471c-4.855 0-9.188-3.778-9.188-9.187V95.691c0-4.876 4.332-9.227 9.188-9.227h52.471c5.41 0 9.743 4.351 9.743 9.227v21.627zm0 49.723a9.728 9.728 0 0 1-9.743 9.76h-52.471c-4.855 0-9.188-4.361-9.188-9.76v-21.629c0-4.834 4.332-9.187 9.188-9.187h52.471c5.41 0 9.743 4.353 9.743 9.187v21.629zm0 49.761c0 5.398-4.333 9.762-9.743 9.762h-52.471c-4.855 0-9.188-4.363-9.188-9.762V195.75c0-5.461 4.332-9.761 9.188-9.761h52.471c5.41 0 9.743 4.3 9.743 9.761v21.052zm0 49.761c0 5.401-4.333 9.712-9.743 9.712h-52.471c-4.855 0-9.188-4.311-9.188-9.712V245.46c0-5.408 4.332-9.71 9.188-9.71h52.471c5.41 0 9.743 4.302 9.743 9.71v21.103zm0 49.763c0 5.398-4.333 9.761-9.743 9.761h-52.471c-4.855 0-9.188-4.362-9.188-9.761v-21.104c0-5.409 4.332-9.711 9.188-9.711h52.471c5.41 0 9.743 4.302 9.743 9.711v21.104zm0 50.287c0 4.874-4.333 9.185-9.743 9.185h-52.471c-4.855 0-9.188-4.311-9.188-9.185v-21.628c0-5.41 4.332-9.765 9.188-9.765h52.471a9.724 9.724 0 0 1 9.743 9.765v21.628zm0 49.76c0 4.874-4.333 9.188-9.743 9.188h-52.471c-4.855 0-9.188-4.313-9.188-9.188v-21.625c0-5.41 4.332-9.189 9.188-9.189h52.471c5.41 0 9.743 3.779 9.743 9.189v21.625zm-335.845-53.018a9.728 9.728 0 0 1-9.746 9.763h-52.441c-5.419 0-9.211-4.352-9.211-9.763v-21.05c0-5.459 3.792-9.76 9.211-9.76h52.441c5.405 0 9.746 4.301 9.746 9.76v21.05zm0 49.765a9.73 9.73 0 0 1-9.746 9.763h-52.441c-5.419 0-9.211-4.354-9.211-9.763v-21.103c0-5.41 3.792-9.712 9.211-9.712h52.441c5.405 0 9.746 4.302 9.746 9.712v21.103zM82.405 263.885c0 5.399-4.347 9.71-9.212 9.71H20.751c-5.419 0-9.736-4.311-9.736-9.71v-21.104c0-5.41 4.317-9.761 9.736-9.761h52.442c4.865 0 9.212 4.351 9.212 9.761v21.104zm164.946 99.47a9.724 9.724 0 0 1-9.751 9.763h-51.903a9.72 9.72 0 0 1-9.746-9.763v-21.05c0-5.459 4.327-9.76 9.746-9.76H237.6c5.419 0 9.751 4.301 9.751 9.76v21.05z"/></svg>
</span>
</div>
<div v-on:click="toggleReplayGain" class="next-button" title="ReplayGain">
<div id="rg-pregain-info">{{playerStats.replayGainPreGainDb}}db</div>
<svg id="rg-status" v-bind:class="{ 'aux-button-active': playerStats.replayGain }" class="center" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" style="width:35px;webkit-logical-width:35px;webkit-logical-height:25px;user-select:none;transform-origin:12.5px 12.5px;r:0;perspective-origin:12.5px 12.5px;overflow-y:hidden;overflow-x:hidden;inline-size:35px;height:25px;d:none;block-size:25px;background:0% 0%/auto padding-box border-box" overflow="hidden" display="block" fill="#fff"><g transform="translate(-.938 19.85)" style="y:0;user-select:none;transform:matrix(1,0,0,1,-.9375,19.85);perspective-origin:0 0"><g class="ld" style="y:0;x:0;user-select:none;transform-origin:7.9375px 0;transform:none;r:0;perspective-origin:0 0;line-height:31.4286px;font:400 22px/31.4286px 'Varela Round','century gothic',verdana;d:none"><text style="y:0;user-select:none;r:0;perspective-origin:7.9375px 0;line-height:31.4286px;font:400 22px/31.4286px Arial" white-space="nowrap" display="block">R</text></g></g><g transform="translate(14.938 19.85)" style="y:0;user-select:none;transform:matrix(1,0,0,1,14.9375,19.85);perspective-origin:0 0"><g class="ld" style="y:0;x:0;user-select:none;transform-origin:5.5px 0;transform:none;r:0;perspective-origin:0 0;line-height:31.4286px;font:400 22px/31.4286px 'Varela Round','century gothic',verdana;d:none"><text style="y:0;user-select:none;r:0;perspective-origin:5.5px 0;line-height:31.4286px;font:400 22px/31.4286px Arial" white-space="nowrap" display="block">G</text></g></g></svg>
</div>
<div v-on:click="toggleVolume" class="player-button">
<img class="noselect fill-white center" :src="volumeSrc" title="Mute/Unmute">
</div>
<div class="volume-bar">
<input v-model="curVol" class="volume-slider" type="range" max="100" value="0" step="1">
</div>
<div v-on:click="toggleRepeat" class="next-button" title="Repeat">
<svg v-bind:class="{ 'aux-button-active': playerStats.shouldLoop }" class="repeat-button center" xmlns="http://www.w3.org/2000/svg" version="1" viewBox="0 0 24 24" enable-background="new 0 0 24 24" width="25" height="25">
<path d="M 17 2 L 17 5 L 6 5 C 4.3 5 3 6.3 3 8 L 3 14.8125 L 5 13.1875 L 5 8 C 5 7.4 5.4 7 6 7 L 17 7 L 17 10 L 22 6 L 17 2 z M 21 9.1875 L 19 10.8125 L 19 16 C 19 16.6 18.6 17 18 17 L 7 17 L 7 14 L 2 18 L 7 22 L 7 19 L 18 19 C 19.7 19 21 17.7 21 16 L 21 9.1875 z"/>
</svg>
</div>
<div v-on:click="toggleShuffle" class="next-button" title="Shuffle">
<svg class="shuffle-button center" v-bind:class="{ 'aux-button-active': playerStats.shuffle }" xmlns="http://www.w3.org/2000/svg" version="1" viewBox="0 0 24 24" enable-background="new 0 0 24 24" width="25" height="25">
<path d="M 17 2 L 17 5 L 14.1875 5 C 13.0875 5 12 5.5875 11.5 6.6875 L 6.59375 16.5 C 6.49375 16.8 6.1125 17 5.8125 17 L 2 17 L 2 19 L 5.8125 19 C 6.9125 19 8 18.4125 8.5 17.3125 L 13.40625 7.5 C 13.50625 7.2 13.8875 7 14.1875 7 L 17 7 L 17 10 L 22 6 L 17 2 z M 2 5 L 2 7 L 5.8125 7 C 6.1125 7 6.4875 7.19375 6.6875 7.59375 L 8.40625 10.90625 L 9.5 8.6875 L 8.5 6.6875 C 7.9 5.5875 6.9125 5 5.8125 5 L 2 5 z M 11.59375 13.09375 L 10.5 15.3125 L 11.5 17.3125 C 12 18.3125 12.9875 19 14.1875 19 L 17 19 L 17 22 L 22 18 L 17 14 L 17 17 L 14.1875 17 C 13.8875 17 13.5125 16.80625 13.3125 16.40625 L 11.59375 13.09375 z"/>
</svg>
</div>
<div v-on:click="toggleAutoDJ" class="next-button" title="Auto DJ">
<svg v-bind:class="{ 'aux-button-active': playerStats.autoDJ }" class="center" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" style="width:25px;webkit-logical-width:25px;webkit-logical-height:25px;user-select:none;transform-origin:12.5px 12.5px;r:0;perspective-origin:12.5px 12.5px;overflow-y:hidden;overflow-x:hidden;inline-size:25px;height:25px;d:none;block-size:25px;background:0% 0%/auto padding-box border-box" overflow="hidden" display="block" fill="#fff"><g transform="translate(-.938 19.85)" style="y:0;user-select:none;transform:matrix(1,0,0,1,-.9375,19.85);perspective-origin:0 0"><g class="ld" style="y:0;x:0;user-select:none;transform-origin:7.9375px 0;transform:none;r:0;perspective-origin:0 0;line-height:31.4286px;font:400 22px/31.4286px 'Varela Round','century gothic',verdana;d:none"><text style="y:0;user-select:none;r:0;perspective-origin:7.9375px 0;line-height:31.4286px;font:400 22px/31.4286px Arial" white-space="nowrap" display="block">D</text></g></g><g transform="translate(14.938 19.85)" style="y:0;user-select:none;transform:matrix(1,0,0,1,14.9375,19.85);perspective-origin:0 0"><g class="ld" style="y:0;x:0;user-select:none;transform-origin:5.5px 0;transform:none;r:0;perspective-origin:0 0;line-height:31.4286px;font:400 22px/31.4286px 'Varela Round','century gothic',verdana;d:none"><text style="y:0;user-select:none;r:0;perspective-origin:5.5px 0;line-height:31.4286px;font:400 22px/31.4286px Arial" white-space="nowrap" display="block">J</text></g></g></svg>
</div>
</div>
</div>
</section>
</div>
</div>
<!-- /6 columns -->
<script type="text/javascript" src="assets/js/lib/lazy-load.js"></script>
<script>
// Lazy load all images in scroallable areas
const ll = new LazyLoad({
container: document.getElementById('filelist')
});
</script>
<script type="text/javascript" src="assets/js/mstream.js"></script>
<script src="assets/js/mstream.vue-player-controls.js"></script>
<script src="assets/js/mstream.vue-browser.js"></script>
</body>

View File

@ -1,12 +1,63 @@
{
"name": "mstream-hybrid-app",
"version": "1.0.0",
"description": "",
"name": "mstream-desktop-app",
"version": "5.9.4",
"description": "mStream Desktop Player",
"main": "index.js",
"scripts": {
"start": "electron .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
"engines": {
"node": ">=10.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/IrosTheBeggar/mStream"
},
"author": {
"name": "Paul Sori",
"email": "paul@mstream.io"
},
"homepage": "https://mstream.io/",
"license": "GPL-3.0",
"build": {
"appId": "io.mstream.desktop",
"productName": "mStream Desktop",
"electronVersion": "16.0.2",
"files": [
"**/*",
"!admin/*",
"!login/*",
"!shared/*",
"!package-lock.json"
],
"mac": {
"category": "public.app-category.music"
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
]
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": [
"x64",
"arm64",
"armv7l"
]
}
]
}
},
"devDependencies": {
"electron-builder": "22.14.5"
}
}

View File

@ -1,10 +0,0 @@
// window.addEventListener('DOMContentLoaded', () => {
// const replaceText = (selector, text) => {
// const element = document.getElementById(selector)
// if (element) element.innerText = text
// }
// for (const type of ['chrome', 'node', 'electron']) {
// replaceText(`${type}-version`, process.versions[type])
// }
// })

View File

@ -27,53 +27,10 @@
<link rel="stylesheet" href="../assets/css/materialize.css">
<!-- Dependencies -->
<script src="../assets/js/lib/axios.js"></script>
<script src="../assets/js/lib/vue2.js"></script>
<!-- mStream Player -->
<script src="../assets/js/mstream.player.js"></script>
<script defer src="../assets/js/mstream.vue.player.js"></script>
<!-- Boot Up App -->
<script defer>
// load the playlist
sharedPlaylist.playlist.forEach(item => {
const newSong = {
url: `../media/${item}?token=${sharedPlaylist.token}`,
filepath: item,
authToken: sharedPlaylist.token,
metadata: {
"artist": "",
"album": "",
"track": "",
"title": "",
"year": "",
"album-art": ""
}
};
MSTREAMPLAYER.addSong(newSong, true);
axios.post('../api/v1/db/metadata', {
'filepath': item,
'token': sharedPlaylist.token
})
.then((response) => {
// handle success
if (response.data.metadata) {
newSong.metadata = response.data.metadata;
MSTREAMPLAYER.resetCurrentMetadata();
}
})
.catch((error) => {
iziToast.warning({
title: 'Metadata Lookup Failed',
position: 'topCenter',
timeout: 3500
});
})
});
</script>
<style>
.row-mod {
margin-bottom: 0px !important;
@ -209,4 +166,54 @@
</ul>
</div>
</div>
<script src="../assets/js/mstream.vue.player.js"></script>
<!-- Boot Up App -->
<script>
// load the playlist
sharedPlaylist.playlist.forEach(item => {
const newSong = {
url: `../media/${item}?token=${sharedPlaylist.token}`,
filepath: item,
authToken: sharedPlaylist.token,
metadata: {}
};
MSTREAMPLAYER.addSong(newSong, true);
fetch('../api/v1/db/metadata', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
'filepath': item,
'token': sharedPlaylist.token
})
})
.then(async (response) => {
if (response.ok !== true) {
throw new Error(response);
}
const data = await response.json();
// handle success
if (data.metadata) {
newSong.metadata = data.metadata;
MSTREAMPLAYER.resetCurrentMetadata();
}
})
.catch((error) => {
console.log(error);
// TODO: Don't spam error messages when an entire playlist fails to load
iziToast.warning({
title: 'Metadata Lookup Failed',
position: 'topCenter',
timeout: 3500
});
});
});
</script>
</body>