Compare commits
224 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d40b034ed | ||
|
|
0a22c1f738 | ||
|
|
41c1432eeb | ||
|
|
adacc6a0f6 | ||
|
|
81a931c884 | ||
|
|
ace6d952ec | ||
|
|
0547301905 | ||
|
|
50b0cf1d1e | ||
|
|
ea2736432b | ||
|
|
63d4d5eb02 | ||
|
|
dd8dc84a07 | ||
|
|
2b296220c2 | ||
|
|
bada002845 | ||
|
|
af2fad9dd1 | ||
|
|
e6bb1b3322 | ||
|
|
17da6533de | ||
|
|
e38725e755 | ||
|
|
4055f5411b | ||
|
|
183e45faf3 | ||
|
|
02ae8c1ada | ||
|
|
6488cd1db7 | ||
|
|
09dd860bd3 | ||
|
|
4200b08e5a | ||
|
|
b9945ba75e | ||
|
|
69332b6393 | ||
|
|
b759ef8d48 | ||
|
|
3bd282dd4a | ||
|
|
688dc55c9e | ||
|
|
9ddb83761c | ||
|
|
380bda7652 | ||
|
|
164a97f86a | ||
|
|
14b6c92644 | ||
|
|
70fdf88f88 | ||
|
|
3e7c5092e5 | ||
|
|
c686ace36c | ||
|
|
b66c6d5be3 | ||
|
|
c37b87e631 | ||
|
|
06f15420d8 | ||
|
|
795d68cbe5 | ||
|
|
49924b3450 | ||
|
|
01d63ed9e1 | ||
|
|
d58ec37679 | ||
|
|
ba51894cab | ||
|
|
c1a2114773 | ||
|
|
fb1e080a42 | ||
|
|
c84b4c822d | ||
|
|
1146fd914f | ||
|
|
cb9992836c | ||
|
|
d0077659c6 | ||
|
|
b37414745f | ||
|
|
30af7689f9 | ||
|
|
a7439ebf3b | ||
|
|
6d5fb03e3f | ||
|
|
780fbc56d8 | ||
|
|
27f276bb31 | ||
|
|
e587b4b21d | ||
|
|
ad9764a149 | ||
|
|
5706ec8647 | ||
|
|
21565a42cb | ||
|
|
f037aa9d1d | ||
|
|
c94c5c5eea | ||
|
|
765286ccd5 | ||
|
|
24c177a163 | ||
|
|
bad3895d06 | ||
|
|
22c12ee030 | ||
|
|
55205f12ad | ||
|
|
95439866e9 | ||
|
|
d1c278ebf7 | ||
|
|
0c327a3a77 | ||
|
|
40579fb5e9 | ||
|
|
b79ebb6c07 | ||
|
|
47cf9e366a | ||
|
|
00411f7845 | ||
|
|
9e22625476 | ||
|
|
086ead9b17 | ||
|
|
e5e8750e9c | ||
|
|
825633152e | ||
|
|
f93ba629b4 | ||
|
|
f0408dd659 | ||
|
|
7ec3774ea7 | ||
|
|
6e38686c30 | ||
|
|
cb1d48db4b | ||
|
|
4f466e4595 | ||
|
|
7f4f5283ec | ||
|
|
b4aa11af9a | ||
|
|
3ea7a48b62 | ||
|
|
4763202afd | ||
|
|
48638fc363 | ||
|
|
5ad80e40f5 | ||
|
|
de061eecb6 | ||
|
|
e48bed3e0f | ||
|
|
369ac4b68f | ||
|
|
723314a3e7 | ||
|
|
db01674eaf | ||
|
|
e665e98680 | ||
|
|
5192139568 | ||
|
|
3a8827c533 | ||
|
|
c76c055d18 | ||
|
|
c4995ede32 | ||
|
|
8827acdd7e | ||
|
|
a27011c2bb | ||
|
|
426807e0ab | ||
|
|
9b1d13c01d | ||
|
|
580565ae1c | ||
|
|
8515570709 | ||
|
|
81fb44d9f8 | ||
|
|
a088235e39 | ||
|
|
8983283519 | ||
|
|
81a4f875ff | ||
|
|
0898957061 | ||
|
|
b69c902911 | ||
|
|
309993f10c | ||
|
|
f12854c32d | ||
|
|
34d9ed4cbe | ||
|
|
b31556c163 | ||
|
|
b17487fb40 | ||
|
|
a0347979e9 | ||
|
|
d8d8e036ab | ||
|
|
9676b65b22 | ||
|
|
6c7182d0b2 | ||
|
|
653bd7735e | ||
|
|
07cbb19c43 | ||
|
|
05f4a8d66b | ||
|
|
a1f5f972a7 | ||
|
|
e8e2b306b6 | ||
|
|
800a915455 | ||
|
|
b4a4e54405 | ||
|
|
f3edf033c3 | ||
|
|
b2b48f0cdf | ||
|
|
9c128711f4 | ||
|
|
5aaa673db1 | ||
|
|
f13e208d74 | ||
|
|
28b9042d2d | ||
|
|
4c8a73a4c9 | ||
|
|
18464de107 | ||
|
|
34f8071bc8 | ||
|
|
e9acfa4fb9 | ||
|
|
cef579fdd7 | ||
|
|
d6b940bbdc | ||
|
|
4f8a3d3442 | ||
|
|
28d858fa58 | ||
|
|
298eff0ff1 | ||
|
|
02cc287e64 | ||
|
|
db962d0347 | ||
|
|
ae0cb0181f | ||
|
|
7b811a1386 | ||
|
|
0ed62f8983 | ||
|
|
de42cd89ba | ||
|
|
1e7b3e5292 | ||
|
|
0832a19e49 | ||
|
|
60279bbdf0 | ||
|
|
b40f2db946 | ||
|
|
bbe458f367 | ||
|
|
6a08cfb73b | ||
|
|
1b58946e58 | ||
|
|
0ad01a09c1 | ||
|
|
ca2c984932 | ||
|
|
34993cd182 | ||
|
|
bb6e13fdb7 | ||
|
|
074a46e9a8 | ||
|
|
2436db83ad | ||
|
|
35d60fcac3 | ||
|
|
7a31305d7c | ||
|
|
3a24fddcb1 | ||
|
|
21654b1216 | ||
|
|
2c7993be46 | ||
|
|
96a284c857 | ||
|
|
6282d5d2ac | ||
|
|
fc68493958 | ||
|
|
18202c30f6 | ||
|
|
59608bd989 | ||
|
|
79fa20f2d8 | ||
|
|
4cf737715c | ||
|
|
9888d7a729 | ||
|
|
a4a48c668b | ||
|
|
b7a4a3c522 | ||
|
|
51fac59d28 | ||
|
|
5c294ea325 | ||
|
|
d069e3b0e6 | ||
|
|
f94477b9b5 | ||
|
|
70999f4ef1 | ||
|
|
857c8ea4f3 | ||
|
|
84af6e2350 | ||
|
|
d003f939c0 | ||
|
|
fa93162f56 | ||
|
|
b14a2b09d5 | ||
|
|
786fe37abe | ||
|
|
e252323c4f | ||
|
|
e3dcdd34c1 | ||
|
|
295516600d | ||
|
|
0040d41b82 | ||
|
|
decd2f40f1 | ||
|
|
049252c58e | ||
|
|
5afa97c5a4 | ||
|
|
7ed6a18fc5 | ||
|
|
93eca0a283 | ||
|
|
142486968d | ||
|
|
ec7ee22179 | ||
|
|
9e4c964e1e | ||
|
|
75846b3eff | ||
|
|
38f4521272 | ||
|
|
80f11c213d | ||
|
|
536bb95bd9 | ||
|
|
1880fc85c0 | ||
|
|
3321a6b142 | ||
|
|
da334171fd | ||
|
|
474f304a79 | ||
|
|
fb02eca7d7 | ||
|
|
3a1b5676f0 | ||
|
|
b7074a1ff6 | ||
|
|
fd8f3a0f37 | ||
|
|
dcf1c00e13 | ||
|
|
7c7d2180d1 | ||
|
|
1e2043c196 | ||
|
|
e534a245d9 | ||
|
|
d4286485e5 | ||
|
|
c3111f5095 | ||
|
|
8d5e8427ff | ||
|
|
fb0d94db8c | ||
|
|
8b0479861b | ||
|
|
01fe83d678 | ||
|
|
0984bcab01 | ||
|
|
5b1d14afa5 | ||
|
|
93f2a43756 |
36
.github/workflows/build-webapp.yml
vendored
Normal 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') }}
|
||||
12
.github/workflows/build.yml
vendored
@ -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
|
||||
|
||||
34
.github/workflows/npm-publish.yml
vendored
@ -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}}
|
||||
|
||||
18
.github/workflows/update-website.yaml
vendored
@ -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
@ -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/*
|
||||
|
||||
|
||||
34
README.md
@ -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
|
||||
|
||||
@ -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
|
||||
BIN
bin/rpn/rpn-osx
116
build/index.html
Normal 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>
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -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
59
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
847
src/api/admin.js
126
src/api/auth.js
@ -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();
|
||||
});
|
||||
}
|
||||
822
src/api/db.js
@ -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) => {
|
||||
|
||||
// });
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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});
|
||||
});
|
||||
}
|
||||
|
||||
@ -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, '/')
|
||||
};
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -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({});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
35
src/db/image-compress-manager.js
Normal 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;
|
||||
}
|
||||
53
src/db/image-compress-script.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
@ -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();
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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, () => {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
@ -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
@ -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 };
|
||||
@ -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
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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
@ -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;
|
||||
})();
|
||||
@ -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>
|
||||
@ -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
@ -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;
|
||||
}
|
||||
@ -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
@ -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)">• {{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;
|
||||
})()
|
||||
13412
webapp/alpha/vue3.js
2
webapp/assets/css/izi-modal.min.css
vendored
1
webapp/assets/css/lazy-load-polyfill.css
Normal file
@ -0,0 +1 @@
|
||||
img[data-lazy-src]{will-change:contents}
|
||||
@ -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;
|
||||
}
|
||||
21
webapp/assets/css/modal.css
Normal 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;
|
||||
}
|
||||
@ -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{
|
||||
|
||||
BIN
webapp/assets/img/app-store-logo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 18 KiB |
BIN
webapp/assets/img/play-store-logo.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
@ -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;
|
||||
})();
|
||||
@ -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) {
|
||||
|
||||
2
webapp/assets/js/lib/butterchurn.min.js
vendored
4
webapp/assets/js/lib/hyst-modal.js
Normal file
4
webapp/assets/js/lib/jquery-2.2.4.min.js
vendored
3
webapp/assets/js/lib/lazy-load-polyfill.js
Normal 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}});
|
||||
445
webapp/assets/js/lib/star-rating.js
Normal 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)
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)">• {{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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
BIN
webapp/build/icon.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
webapp/build/mstream-logo-cut.icns
Normal file
BIN
webapp/build/mstream-logo-cut.ico
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
webapp/build/tray-icon-osx.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
webapp/build/tray-icon.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
webapp/build/tray-icon@2x.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
@ -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(() => {
|
||||
|
||||
@ -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
@ -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"> </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>
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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])
|
||||
// }
|
||||
// })
|
||||
@ -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>
|
||||