Compare commits
96 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 |
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
|
||||
|
||||
1
.gitignore
vendored
@ -21,7 +21,6 @@ bin/ffmpeg/*
|
||||
|
||||
save/*.json
|
||||
|
||||
package-lock.json
|
||||
.DS_Store
|
||||
.vscode/*
|
||||
|
||||
|
||||
31
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,32 +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
|
||||
|
||||
[](https://play.google.com/store/apps/details?id=mstream.music&hl=en_US&gl=US)
|
||||
|
||||
[This App is Open Source. See the Source Code](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
@ -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
|
||||
```
|
||||
|
||||
8107
package-lock.json
generated
Normal file
58
package.json
@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "mstream",
|
||||
"version": "5.9.1",
|
||||
"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": "15.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,32 +86,35 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"archiver": "^5.3.0",
|
||||
"axios": "^0.21.4",
|
||||
"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.20.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.2",
|
||||
"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.32",
|
||||
"music-metadata": "^7.11.4",
|
||||
"nanoid": "^3.1.25",
|
||||
"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.11.7"
|
||||
"electron-builder": "25.1.8"
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ const fileExplorer = require('../util/file-explorer');
|
||||
const admin = require('../util/admin');
|
||||
const config = require('../state/config');
|
||||
const dbQueue = require('../db/task-queue');
|
||||
const imageCompress = require('../db/image-compress-manager');
|
||||
const transcode = require('./transcode');
|
||||
const db = require('../db/manager');
|
||||
const { joiValidate } = require('../util/validation');
|
||||
@ -107,6 +108,16 @@ exports.setup = (mstream) => {
|
||||
res.json({});
|
||||
});
|
||||
|
||||
mstream.post("/api/v1/admin/db/params/new-scan", async (req, res) => {
|
||||
const schema = Joi.object({
|
||||
newScan: Joi.boolean().required()
|
||||
});
|
||||
joiValidate(schema, req.body);
|
||||
|
||||
await admin.editNewScan(req.body.newScan);
|
||||
res.json({});
|
||||
});
|
||||
|
||||
mstream.post("/api/v1/admin/db/params/pause", async (req, res) => {
|
||||
const schema = Joi.object({
|
||||
pause: Joi.number().integer().min(0).required()
|
||||
@ -137,6 +148,16 @@ exports.setup = (mstream) => {
|
||||
res.json({});
|
||||
});
|
||||
|
||||
mstream.post("/api/v1/admin/db/params/compress-image", async (req, res) => {
|
||||
const schema = Joi.object({
|
||||
compressImage: Joi.boolean().required()
|
||||
});
|
||||
joiValidate(schema, req.body);
|
||||
|
||||
await admin.editCompressImages(req.body.compressImage);
|
||||
res.json({});
|
||||
});
|
||||
|
||||
mstream.get("/api/v1/admin/users", (req, res) => {
|
||||
// Scrub passwords
|
||||
const memClone = JSON.parse(JSON.stringify(config.program.users));
|
||||
@ -153,11 +174,17 @@ exports.setup = (mstream) => {
|
||||
const schema = Joi.object({
|
||||
directory: Joi.string().required(),
|
||||
vpath: Joi.string().pattern(/[a-zA-Z0-9-]+/).required(),
|
||||
autoAccess: Joi.boolean().default(false)
|
||||
autoAccess: Joi.boolean().default(false),
|
||||
isAudioBooks: Joi.boolean().default(false)
|
||||
});
|
||||
const input = joiValidate(schema, req.body);
|
||||
|
||||
await admin.addDirectory(input.value.directory, input.value.vpath, input.value.autoAccess, mstream);
|
||||
await admin.addDirectory(
|
||||
input.value.directory,
|
||||
input.value.vpath,
|
||||
input.value.autoAccess,
|
||||
input.value.isAudioBooks,
|
||||
mstream);
|
||||
res.json({});
|
||||
|
||||
try {
|
||||
@ -194,6 +221,10 @@ exports.setup = (mstream) => {
|
||||
);
|
||||
res.json({});
|
||||
});
|
||||
|
||||
mstream.post("/api/v1/admin/db/force-compress-images", (req, res) => {
|
||||
res.json({ started: imageCompress.run() });
|
||||
});
|
||||
|
||||
mstream.post("/api/v1/admin/db/scan/all", (req, res) => {
|
||||
dbQueue.scanAll();
|
||||
@ -276,10 +307,21 @@ exports.setup = (mstream) => {
|
||||
writeLogs: config.program.writeLogs,
|
||||
secret: config.program.secret.slice(-4),
|
||||
ssl: config.program.ssl,
|
||||
storage: config.program.storage
|
||||
storage: config.program.storage,
|
||||
maxRequestSize: config.program.maxRequestSize
|
||||
});
|
||||
});
|
||||
|
||||
mstream.post("/api/v1/admin/config/max-request-size", async (req, res) => {
|
||||
const schema = Joi.object({
|
||||
maxRequestSize: Joi.string().pattern(/[0-9]+(KB|MB)/i).required()
|
||||
});
|
||||
joiValidate(schema, req.body);
|
||||
|
||||
await admin.editMaxRequestSize(req.body.maxRequestSize);
|
||||
res.json({});
|
||||
});
|
||||
|
||||
mstream.post("/api/v1/admin/config/port", async (req, res) => {
|
||||
const schema = Joi.object({
|
||||
port: Joi.number().required()
|
||||
|
||||
@ -22,6 +22,7 @@ exports.setup = (mstream) => {
|
||||
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',
|
||||
});
|
||||
|
||||
|
||||
203
src/api/db.js
@ -40,18 +40,23 @@ function renderMetadataObj(row) {
|
||||
"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 } })
|
||||
}
|
||||
|
||||
@ -74,28 +79,53 @@ exports.setup = (mstream) => {
|
||||
});
|
||||
|
||||
mstream.post('/api/v1/db/metadata', (req, res) => {
|
||||
const pathInfo = vpath.getVPathInfo(req.body.filepath, req.user);
|
||||
if (!db.getFileCollection()) { return res.json({ "filepath": req.body.filepath, "metadata": {} }); }
|
||||
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 + '-' + req.user.username;
|
||||
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 res.json({ "filepath": req.body.filepath, "metadata": {} });
|
||||
return { "filepath": filepath, "metadata": null };
|
||||
}
|
||||
|
||||
res.json(renderMetadataObj(result[0]));
|
||||
return renderMetadataObj(result[0]);
|
||||
}
|
||||
|
||||
// legacy enpoint, moved to POST
|
||||
mstream.get('/api/v1/db/artists', (req, res) => {
|
||||
const artists = getArtists(req);
|
||||
res.json(artists);
|
||||
});
|
||||
|
||||
mstream.get('/api/v1/db/artists', (req, res) => {
|
||||
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));
|
||||
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)) {
|
||||
@ -107,8 +137,8 @@ exports.setup = (mstream) => {
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
res.json(artists);
|
||||
});
|
||||
return artists;
|
||||
}
|
||||
|
||||
mstream.post('/api/v1/db/artists-albums', (req, res) => {
|
||||
const albums = { "albums": [] };
|
||||
@ -116,7 +146,7 @@ exports.setup = (mstream) => {
|
||||
|
||||
const results = db.getFileCollection().chain().find({
|
||||
'$and': [
|
||||
renderOrClause(req.user.vpaths),
|
||||
renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
|
||||
{'artist': { '$eq': String(req.body.artist) }}
|
||||
]
|
||||
}).simplesort('year', true).data();
|
||||
@ -146,10 +176,20 @@ exports.setup = (mstream) => {
|
||||
});
|
||||
|
||||
mstream.get('/api/v1/db/albums', (req, res) => {
|
||||
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));
|
||||
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)) {
|
||||
@ -164,14 +204,14 @@ exports.setup = (mstream) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
res.json(albums);
|
||||
});
|
||||
return albums;
|
||||
}
|
||||
|
||||
mstream.post('/api/v1/db/album-songs', (req, res) => {
|
||||
if (!db.getFileCollection()) { throw new Error('DB Not Working'); }
|
||||
|
||||
const searchClause = [
|
||||
renderOrClause(req.user.vpaths),
|
||||
renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
|
||||
{'album': { '$eq': req.body.album ? String(req.body.album) : null }}
|
||||
];
|
||||
|
||||
@ -205,6 +245,7 @@ exports.setup = (mstream) => {
|
||||
noAlbums: Joi.boolean().optional(),
|
||||
noTitles: Joi.boolean().optional(),
|
||||
noFiles: Joi.boolean().optional(),
|
||||
ignoreVPaths: Joi.array().items(Joi.string()).optional()
|
||||
});
|
||||
joiValidate(schema, req.body);
|
||||
|
||||
@ -226,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']}}
|
||||
]
|
||||
};
|
||||
@ -259,6 +300,16 @@ exports.setup = (mstream) => {
|
||||
}
|
||||
|
||||
mstream.get('/api/v1/db/rated', (req, res) => {
|
||||
const songs = getRatedSongs(req);
|
||||
res.json(songs);
|
||||
});
|
||||
|
||||
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) => {
|
||||
@ -287,7 +338,7 @@ exports.setup = (mstream) => {
|
||||
|
||||
const results = db.getUserMetadataCollection().chain().eqJoin(db.getFileCollection().chain(), leftFun, rightFun, mapFun).find({
|
||||
'$and': [
|
||||
renderOrClause(req.user.vpaths),
|
||||
renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
|
||||
{ 'rating': { '$gt': 0 } }
|
||||
]
|
||||
}).simplesort('rating', true).data();
|
||||
@ -296,8 +347,9 @@ exports.setup = (mstream) => {
|
||||
for (const row of results) {
|
||||
songs.push(renderMetadataObj(row));
|
||||
}
|
||||
res.json(songs);
|
||||
});
|
||||
|
||||
return songs;
|
||||
}
|
||||
|
||||
mstream.post('/api/v1/db/rate-song', (req, res) => {
|
||||
const schema = Joi.object({
|
||||
@ -329,7 +381,10 @@ exports.setup = (mstream) => {
|
||||
});
|
||||
|
||||
mstream.post('/api/v1/db/recent/added', (req, res) => {
|
||||
const schema = Joi.object({ limit: Joi.number().integer().min(1).required() });
|
||||
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'); }
|
||||
@ -340,7 +395,7 @@ exports.setup = (mstream) => {
|
||||
|
||||
const results = db.getFileCollection().chain().find({
|
||||
'$and': [
|
||||
renderOrClause(req.user.vpaths),
|
||||
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();
|
||||
@ -353,6 +408,106 @@ exports.setup = (mstream) => {
|
||||
res.json(songs);
|
||||
});
|
||||
|
||||
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 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),
|
||||
{ '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) => {
|
||||
if (!db.getFileDbName()) { throw new Error('No DB'); };
|
||||
|
||||
@ -369,7 +524,7 @@ exports.setup = (mstream) => {
|
||||
|
||||
let orClause = { '$or': [] };
|
||||
for (let vpath of req.user.vpaths) {
|
||||
if (req.body.ignoreVPaths && typeof req.body.ignoreVPaths === 'object' && req.body.ignoreVPaths[vpath] === true) {
|
||||
if (req.body.ignoreVPaths && typeof req.body.ignoreVPaths === 'object' && req.body.ignoreVPaths.includes(vpath)) {
|
||||
continue;
|
||||
}
|
||||
orClause['$or'].push({ 'vpath': { '$eq': vpath } });
|
||||
@ -458,4 +613,8 @@ exports.setup = (mstream) => {
|
||||
|
||||
res.json(returnThis);
|
||||
});
|
||||
|
||||
// mstream.post('/api/v1/db/song-position', (req, res) => {
|
||||
|
||||
// });
|
||||
}
|
||||
|
||||
@ -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');
|
||||
@ -16,7 +16,8 @@ exports.setup = (mstream) => {
|
||||
mstream.post("/api/v1/file-explorer", async (req, res) => {
|
||||
const schema = Joi.object({
|
||||
directory: Joi.string().allow("").required(),
|
||||
sort: Joi.boolean().default(true)
|
||||
sort: Joi.boolean().default(true),
|
||||
pullMetadata: Joi.boolean().default(false)
|
||||
});
|
||||
const { value } = joiValidate(schema, req.body);
|
||||
|
||||
@ -48,7 +49,7 @@ exports.setup = (mstream) => {
|
||||
}
|
||||
|
||||
// get directory contents
|
||||
const folderContents = await fileExplorer.getDirectoryContents(pathInfo.fullPath, config.program.supportedAudioFiles, value.sort);
|
||||
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);
|
||||
@ -106,15 +107,16 @@ exports.setup = (mstream) => {
|
||||
const pathInfo = vpath.getVPathInfo(decodeURI(req.headers['data-location']), req.user);
|
||||
mkdirp.sync(pathInfo.fullPath);
|
||||
|
||||
const busboy = new Busboy({ headers: req.headers });
|
||||
busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
|
||||
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));
|
||||
});
|
||||
|
||||
busboy.on('finish', () => { res.json({}); });
|
||||
req.pipe(busboy);
|
||||
bb.on('close', () => { res.json({}); });
|
||||
req.pipe(bb);
|
||||
});
|
||||
|
||||
mstream.post("/api/v1/file-explorer/m3u", async (req, res) => {
|
||||
|
||||
@ -15,11 +15,22 @@ exports.setup = (mstream) => {
|
||||
}
|
||||
}
|
||||
|
||||
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', (req, res) => {
|
||||
@ -91,7 +102,8 @@ exports.setup = (mstream) => {
|
||||
db.getPlaylistCollection().insert({
|
||||
name: req.body.title,
|
||||
filepath: null,
|
||||
user: req.user.username
|
||||
user: req.user.username,
|
||||
live: false
|
||||
});
|
||||
|
||||
db.saveUserDB();
|
||||
@ -101,7 +113,8 @@ exports.setup = (mstream) => {
|
||||
mstream.post('/api/v1/playlist/save', (req, res) => {
|
||||
const schema = Joi.object({
|
||||
title: Joi.string().required(),
|
||||
songs: Joi.array().items(Joi.string())
|
||||
songs: Joi.array().items(Joi.string()),
|
||||
live: Joi.boolean().optional()
|
||||
});
|
||||
joiValidate(schema, req.body);
|
||||
|
||||
@ -125,8 +138,10 @@ exports.setup = (mstream) => {
|
||||
db.getPlaylistCollection().insert({
|
||||
name: req.body.title,
|
||||
filepath: null,
|
||||
user: req.user.username
|
||||
user: req.user.username,
|
||||
live: typeof req.body.live === 'boolean' ? req.body.live : false
|
||||
});
|
||||
|
||||
|
||||
db.saveUserDB();
|
||||
res.json({});
|
||||
@ -139,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;
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ exports.setup = (mstream) => {
|
||||
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 {
|
||||
|
||||
@ -3,7 +3,9 @@ const Joi = require('joi');
|
||||
const axios = require('axios');
|
||||
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();
|
||||
|
||||
@ -37,6 +39,60 @@ exports.setup = (mstream) => {
|
||||
);
|
||||
});
|
||||
|
||||
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(
|
||||
{
|
||||
artist: dbFileInfo.artist,
|
||||
album: dbFileInfo.album,
|
||||
track: dbFileInfo.title
|
||||
},
|
||||
req.user['lastfm-user'],
|
||||
(post_return_data) => {}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
mstream.post('/api/v1/lastfm/test-login', async (req, res) => {
|
||||
const schema = Joi.object({
|
||||
username: Joi.string().required(),
|
||||
|
||||
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);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
const winston = require('winston');
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Joi = require('joi');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const jwt = require('jsonwebtoken');
|
||||
@ -63,7 +64,7 @@ exports.serveIt = async configFile => {
|
||||
|
||||
// Magic Middleware Things
|
||||
mstream.use(cookieParser());
|
||||
mstream.use(express.json());
|
||||
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", "*");
|
||||
@ -82,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();
|
||||
});
|
||||
@ -90,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) => {
|
||||
@ -107,18 +117,18 @@ exports.serveIt = async configFile => {
|
||||
jwt.verify(req.cookies['x-access-token'], config.program.secret);
|
||||
next();
|
||||
} catch(err) {
|
||||
return res.redirect(301, 'login');
|
||||
return res.redirect(302, '/login');
|
||||
}
|
||||
});
|
||||
|
||||
mstream.get('/login', (req, res, next) => {
|
||||
if (Object.keys(config.program.users).length === 0){
|
||||
return res.redirect(301, '..');
|
||||
return res.redirect(302, '..');
|
||||
}
|
||||
|
||||
try {
|
||||
jwt.verify(req.cookies['x-access-token'], config.program.secret);
|
||||
return res.redirect(301, '..');
|
||||
return res.redirect(302, '..');
|
||||
} catch(err) {
|
||||
next();
|
||||
}
|
||||
@ -151,7 +161,15 @@ exports.serveIt = async configFile => {
|
||||
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) => {
|
||||
|
||||
@ -17,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({
|
||||
@ -25,7 +27,7 @@ const dbOptions = Joi.object({
|
||||
});
|
||||
|
||||
const transcodeOptions = Joi.object({
|
||||
algorithm: Joi.string().valid(...getTransAlgos()).default('buffer'),
|
||||
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(...getTransCodecs()).default('opus'),
|
||||
@ -53,7 +55,7 @@ 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()
|
||||
@ -72,12 +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(),
|
||||
type: Joi.string().valid('music', 'audiobook').default('music'),
|
||||
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",
|
||||
|
||||
@ -18,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` };
|
||||
@ -30,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);
|
||||
@ -44,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 => {
|
||||
@ -187,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;
|
||||
@ -242,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 = {}; }
|
||||
@ -269,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;
|
||||
@ -328,12 +358,14 @@ 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();
|
||||
}
|
||||
|
||||
@ -358,12 +390,12 @@ function testSSL(jsonLoad) {
|
||||
}
|
||||
|
||||
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();
|
||||
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();
|
||||
}
|
||||
@ -23,7 +23,8 @@ function isAsync(fn) {
|
||||
function wrapAsync(fn) {
|
||||
return (req, res, next = noop) => {
|
||||
fn(req, res, next)
|
||||
.then(() => !res.finished && next())
|
||||
.catch(next);
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -39,7 +39,6 @@
|
||||
<link rel="stylesheet" href="index.css">
|
||||
|
||||
<script src="../assets/js/api.js"></script>
|
||||
<script>API.checkAuthAndKickToLogin();</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@ -262,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...'}}
|
||||
@ -345,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
|
||||
}
|
||||
});
|
||||
|
||||
@ -445,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">
|
||||
@ -625,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]],
|
||||
});
|
||||
@ -696,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>
|
||||
@ -817,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',
|
||||
@ -930,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>
|
||||
@ -987,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>
|
||||
@ -1139,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,
|
||||
@ -2371,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 {
|
||||
@ -3214,6 +3444,7 @@ 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,
|
||||
|
||||
@ -39,8 +39,12 @@ const MSTREAMAPI = (() => {
|
||||
return req('POST', mstreamModule.currentServer.host + 'api/v1/file-explorer/recursive', { directory: directory });
|
||||
}
|
||||
|
||||
mstreamModule.savePlaylist = (title, songs) => {
|
||||
return req('POST', mstreamModule.currentServer.host + 'api/v1/playlist/save', { title: title, songs: songs });
|
||||
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) => {
|
||||
@ -71,20 +75,20 @@ const MSTREAMAPI = (() => {
|
||||
return req('POST', mstreamModule.currentServer.host + 'api/v1/db/search', postObject);
|
||||
}
|
||||
|
||||
mstreamModule.artists = () => {
|
||||
return req('GET', mstreamModule.currentServer.host + 'api/v1/db/artists', false);
|
||||
mstreamModule.artists = (postObject) => {
|
||||
return req('POST', mstreamModule.currentServer.host + 'api/v1/db/artists', postObject);
|
||||
}
|
||||
|
||||
mstreamModule.albums = () => {
|
||||
return req('GET', mstreamModule.currentServer.host + 'api/v1/db/albums', false);
|
||||
mstreamModule.albums = (postObject) => {
|
||||
return req('POST', mstreamModule.currentServer.host + 'api/v1/db/albums', postObject);
|
||||
}
|
||||
|
||||
mstreamModule.artistAlbums = (artist) => {
|
||||
return req('POST', mstreamModule.currentServer.host + "api/v1/db/artists-albums", { artist: artist });
|
||||
mstreamModule.artistAlbums = (postObject) => {
|
||||
return req('POST', mstreamModule.currentServer.host + "api/v1/db/artists-albums", postObject);
|
||||
}
|
||||
|
||||
mstreamModule.albumSongs = (album, artist, year) => {
|
||||
return req('POST', mstreamModule.currentServer.host + "api/v1/db/album-songs", { album, artist, year });
|
||||
mstreamModule.albumSongs = (postObject) => {
|
||||
return req('POST', mstreamModule.currentServer.host + "api/v1/db/album-songs", postObject);
|
||||
}
|
||||
|
||||
mstreamModule.dbStatus = () => {
|
||||
@ -99,12 +103,20 @@ const MSTREAMAPI = (() => {
|
||||
return req('POST', mstreamModule.currentServer.host + "api/v1/db/rate-song", { filepath: filepath, rating: rating });
|
||||
}
|
||||
|
||||
mstreamModule.getRated = () => {
|
||||
return req('GET', mstreamModule.currentServer.host + "api/v1/db/rated", false);
|
||||
mstreamModule.getRated = (postObject) => {
|
||||
return req('POST', mstreamModule.currentServer.host + "api/v1/db/rated", postObject);
|
||||
}
|
||||
|
||||
mstreamModule.getRecentlyAdded = (limit) => {
|
||||
return req('POST', mstreamModule.currentServer.host + "api/v1/db/recent/added", { limit: limit });
|
||||
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) => {
|
||||
@ -120,6 +132,10 @@ const MSTREAMAPI = (() => {
|
||||
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 });
|
||||
@ -132,7 +148,7 @@ const MSTREAMAPI = (() => {
|
||||
mstreamModule.logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
Cookies.remove('x-access-token');
|
||||
window.location.href = './login';
|
||||
document.location.assign(window.location.href + (window.location.href.slice(-1) === '/' ? '' : '/') + 'login');
|
||||
}
|
||||
|
||||
return mstreamModule;
|
||||
|
||||
@ -90,13 +90,6 @@ myDropzone.on('error', (err, msg, xhr) => {
|
||||
iziToast.error(iziStuff);
|
||||
});
|
||||
|
||||
// Setup scrobbling
|
||||
MSTREAMPLAYER.scrobble = function () {
|
||||
if (MSTREAMPLAYER.playerStats.metadata.artist && MSTREAMPLAYER.playerStats.metadata.title) {
|
||||
MSTREAMAPI.scrobbleByMetadata(MSTREAMPLAYER.playerStats.metadata.artist, MSTREAMPLAYER.playerStats.metadata.album, MSTREAMPLAYER.playerStats.metadata.title);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////// Global Variables
|
||||
// These vars track your position within the file explorer
|
||||
var fileExplorerArray = [];
|
||||
@ -128,7 +121,7 @@ function renderAlbum(id, artist, name, albumArtFile, year) {
|
||||
return `<li class="collection-item">
|
||||
<div ${year ? `data-year="${year}"` : '' } ${artist ? `data-artist="${artist}"` : '' } ${id ? `data-album="${id}"` : '' } class="albumz flex2" onclick="getAlbumsOnClick(this);">
|
||||
${albumArtFile ?
|
||||
`<img class="album-art-box" loading="lazy" src="${MSTREAMAPI.currentServer.host}album-art/${albumArtFile}?token=${MSTREAMAPI.currentServer.token}">`:
|
||||
`<img class="album-art-box" loading="lazy" src="${MSTREAMAPI.currentServer.host}album-art/${albumArtFile}?compress=s&token=${MSTREAMAPI.currentServer.token}">`:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="album-art-box" viewBox="0 0 512 512" xml:space="preserve"><path d="M437 75C390.7 28.6 326.7 0 256 0 114.6 0 0 114.6 0 256c0 70.7 28.6 134.7 75 181s110.3 75 181 75c141.4 0 256-114.6 256-256 0-70.7-28.6-134.7-75-181zM256 477.9c-122.3 0-221.9-99.5-221.9-221.9S133.7 34.1 256 34.1 477.9 133.7 477.9 256 378.3 477.9 256 477.9z"/><path d="M256 145.1c-61.3 0-110.9 49.7-110.9 110.9S194.7 366.9 256 366.9 366.9 317.3 366.9 256c0-61.2-49.7-110.9-110.9-110.9zm0 187.7c-42.4 0-76.8-34.4-76.8-76.8s34.4-76.8 76.8-76.8 76.8 34.4 76.8 76.8-34.4 76.8-76.8 76.8z"/><path d="M238.9 238.9H273V273h-34.1zM256 102.4V68.3h-.6c-31 0-60.1 7.6-85.8 21l1-.5c-26 13.5-47.7 31.9-64.5 54.2l-.3.5 27.3 20.5c28.1-37.5 72.4-61.5 122.3-61.5l.6-.1z"/></svg>'}
|
||||
<span><b>${name}</b> ${year ? `<br>[${year}]` : ''}</span>
|
||||
</div>
|
||||
@ -144,7 +137,7 @@ function renderArtist(artist) {
|
||||
function renderFileWithMetadataHtml(filepath, lokiId, metadata) {
|
||||
return `<li data-lokiid="${lokiId}" class="collection-item">
|
||||
<div data-file_location="${filepath}" class="filez flex" onclick="onFileClick(this);">
|
||||
<img class="album-art-box" loading="lazy" ${metadata['album-art'] ? `src="${MSTREAMAPI.currentServer.host}album-art/${metadata['album-art']}?token=${MSTREAMAPI.currentServer.token}"` : 'src="assets/img/default.png"'}>
|
||||
<img class="album-art-box" loading="lazy" ${metadata['album-art'] ? `src="${MSTREAMAPI.currentServer.host}album-art/${metadata['album-art']}?compress=s&token=${MSTREAMAPI.currentServer.token}"` : 'src="assets/img/default.png"'}>
|
||||
<div>
|
||||
<b><span>${(!metadata || !metadata.title) ? filepath.split("/").pop() : `${metadata.title}`}</span></b>
|
||||
${metadata.artist ? `</b><br><span style="font-size:15px;">${metadata.artist}</span>` : ''}
|
||||
@ -164,9 +157,9 @@ function createMusicFileHtml(fileLocation, title, aa, rating, subtitle) {
|
||||
<div data-file_location="${fileLocation}" class="filez ${aa ? 'flex2' : ''}" onclick="onFileClick(this);">
|
||||
${aa ? `<img loading="lazy" class="album-art-box" ${aa}>` : '<svg class="music-image" height="18" width="18" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path d="M9 37.5c-3.584 0-6.5-2.916-6.5-6.5s2.916-6.5 6.5-6.5a6.43 6.43 0 012.785.634l.715.34V5.429l25-3.846V29c0 3.584-2.916 6.5-6.5 6.5s-6.5-2.916-6.5-6.5 2.916-6.5 6.5-6.5a6.43 6.43 0 012.785.634l.715.34V11.023l-19 2.931V31c0 3.584-2.916 6.5-6.5 6.5z" fill="#8bb7f0"/><path d="M37 2.166V29c0 3.308-2.692 6-6 6s-6-2.692-6-6 2.692-6 6-6a5.93 5.93 0 012.57.586l1.43.68V10.441l-1.152.178-18 2.776-.848.13V31c0 3.308-2.692 6-6 6s-6-2.692-6-6 2.692-6 6-6a5.93 5.93 0 012.57.586l1.43.68V5.858l24-3.692M38 1L12 5v19.683A6.962 6.962 0 009 24a7 7 0 107 7V14.383l18-2.776v11.076A6.962 6.962 0 0031 22a7 7 0 107 7V1z" fill="#4e7ab5"/></svg>'}
|
||||
<span>
|
||||
${subtitle ? `<b>` : ''}
|
||||
${subtitle !== undefined ? `<b>` : ''}
|
||||
<span class="${aa ? '' : 'item-text'}">${rating ? `[${rating}] ` : ''}${title}</span>
|
||||
${subtitle ? `</b><br><span>${subtitle}</span>` : ''}
|
||||
${subtitle !== undefined ? `</b><br><span>${subtitle}</span>` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div class="song-button-box">
|
||||
@ -430,6 +423,7 @@ async function init() {
|
||||
response.playlists.forEach(p => {
|
||||
VUEPLAYERCORE.playlists.push(p);
|
||||
document.getElementById('pop-f').innerHTML += `<div class="pop-list-item" onclick="addToPlaylistUI('${p.name}')">• ${p.name}</div>`;
|
||||
document.getElementById('live-playlist-select').innerHTML += `<option value="${p.name}">${p.name}</option>`;
|
||||
});
|
||||
|
||||
if (response.transcode) {
|
||||
@ -463,6 +457,27 @@ async function init() {
|
||||
MSTREAMPLAYER.transcodeOptions.selectedAlgo = localStorage.getItem('trans-algo-select');
|
||||
} catch (e) {}
|
||||
|
||||
try{
|
||||
VUEPLAYERCORE.livePlaylist.name = localStorage.getItem('live-playlist-auto-start') ? localStorage.getItem('live-playlist-auto-start') : false;
|
||||
|
||||
if (VUEPLAYERCORE.livePlaylist.name) {
|
||||
// get current playlist
|
||||
const response = await MSTREAMAPI.loadPlaylist(VUEPLAYERCORE.livePlaylist.name);
|
||||
|
||||
// set the queue to the current playlist
|
||||
MSTREAMPLAYER.clearPlaylist();
|
||||
response.forEach(value => {
|
||||
VUEPLAYERCORE.addSongWizard(value.filepath, value.metadata, false, undefined, false, true);
|
||||
});
|
||||
|
||||
document.getElementById('set_live_playlist').classList.remove('green');
|
||||
document.getElementById('set_live_playlist').classList.add('blue');
|
||||
document.getElementById('set_live_playlist').value = 'Disable Live Playlist';
|
||||
document.getElementById('live-playlist-hide-these').hidden = true;
|
||||
}
|
||||
|
||||
}catch(err) {}
|
||||
|
||||
dbStatus();
|
||||
}
|
||||
|
||||
@ -537,6 +552,10 @@ function openSaveModal() {
|
||||
myModal.open('#savePlaylist');
|
||||
}
|
||||
|
||||
function openLivePlaylistModal() {
|
||||
myModal.open('#livePlaylist');
|
||||
}
|
||||
|
||||
function openNewPlaylistModal() {
|
||||
myModal.open('#newPlaylist');
|
||||
}
|
||||
@ -545,7 +564,38 @@ function openPlaybackModal() {
|
||||
myModal.open('#speedModal');
|
||||
}
|
||||
|
||||
function openMetadataModal(metadata, fp) {
|
||||
if (metadata === null) {
|
||||
return iziToast.warning({
|
||||
title: 'No Metadata Found',
|
||||
position: 'topCenter',
|
||||
timeout: 3500
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('meta--title').innerHTML = metadata.title;
|
||||
document.getElementById('meta--album').innerHTML = metadata.album;
|
||||
document.getElementById('meta--artist').innerHTML = metadata.artist;
|
||||
document.getElementById('meta--year').innerHTML = metadata.year;
|
||||
document.getElementById('meta--disk').innerHTML = metadata.disk;
|
||||
document.getElementById('meta--track').innerHTML = metadata.track;
|
||||
document.getElementById('meta--rating').innerHTML = metadata.rating;
|
||||
document.getElementById('meta--rg').innerHTML = metadata['replaygain-track'];
|
||||
document.getElementById('meta--fp').innerHTML = fp;
|
||||
document.getElementById('meta--fp').href = 'media' + fp;
|
||||
document.getElementById('meta--aa').innerHTML = 'album-art/' + metadata['album-art'];
|
||||
if (metadata['album-art']) {
|
||||
document.getElementById('meta--aa').href = `album-art/${metadata['album-art']}`;
|
||||
} else {
|
||||
document.getElementById('meta--aa').href = '#';
|
||||
}
|
||||
|
||||
myModal.open('#metadataModel');
|
||||
}
|
||||
|
||||
function openEditModal() {
|
||||
document.getElementById('server_address').value = MSTREAMAPI.currentServer.host;
|
||||
document.getElementById('server_username').value = MSTREAMAPI.currentServer.username;
|
||||
myModal.open('#editServer');
|
||||
}
|
||||
|
||||
@ -624,42 +674,6 @@ function downloadFileplaylist(el) {
|
||||
document.getElementById('downform').innerHTML = '';
|
||||
}
|
||||
|
||||
function getArtistz(el) {
|
||||
const artist = el.getAttribute('data-artist');
|
||||
programState.push({
|
||||
state: 'artist',
|
||||
name: artist,
|
||||
previousScroll: document.getElementById('filelist').scrollTop,
|
||||
previousSearch: document.getElementById('localSearchBar').value
|
||||
});
|
||||
|
||||
getArtistsAlbums(artist)
|
||||
}
|
||||
|
||||
async function getArtistsAlbums(artist) {
|
||||
setBrowserRootPanel('Albums');
|
||||
document.getElementById('directoryName').innerHTML = 'Artist: ' + artist;
|
||||
document.getElementById('filelist').innerHTML = getLoadingSvg();
|
||||
|
||||
try {
|
||||
const response = await MSTREAMAPI.artistAlbums(artist);
|
||||
let albums = '';
|
||||
response.albums.forEach(value => {
|
||||
const albumString = value.name ? value.name : 'SINGLES';
|
||||
// 'value.name === null ? artist : null' is some clever shit that only passes in artist info when the album is null
|
||||
// This is so we get the singles for this artist
|
||||
// If the album is specified, we don't want to limit by artist
|
||||
albums += renderAlbum(value.name, value.name === null ? artist : null, albumString, value.album_art_file, value.year);
|
||||
currentBrowsingList.push({ type: 'album', name: value.name, artist: artist, album_art_file: value.album_art_file })
|
||||
});
|
||||
|
||||
document.getElementById('filelist').innerHTML = albums;
|
||||
}catch (err) {
|
||||
document.getElementById('filelist').innerHTML = '<div>Server call failed</div>';
|
||||
return boilerplateFailure(err);
|
||||
}
|
||||
}
|
||||
|
||||
function onSearchButtonClick() {
|
||||
// Hide Filepath
|
||||
document.getElementById('search_folders').classList.toggle('super-hide');
|
||||
@ -720,6 +734,7 @@ async function getAllPlaylists() {
|
||||
const response = await MSTREAMAPI.getAllPlaylists();
|
||||
VUEPLAYERCORE.playlists.length = 0;
|
||||
document.getElementById('pop-f').innerHTML = '<div class="pop-f pop-playlist">Add To Playlist:</div>';
|
||||
document.getElementById('live-playlist-select').innerHTML = `<option value="" disabled selected>Select Playlist</option>`;
|
||||
|
||||
// loop through the json array and make an array of corresponding divs
|
||||
let playlists = '<ul class="collection">';
|
||||
@ -729,6 +744,7 @@ async function getAllPlaylists() {
|
||||
currentBrowsingList.push(lol);
|
||||
VUEPLAYERCORE.playlists.push(lol);
|
||||
document.getElementById('pop-f').innerHTML += `<div class="pop-list-item" onclick="addToPlaylistUI('${p.name}')">• ${p.name}</div>`;
|
||||
document.getElementById('live-playlist-select').innerHTML += `<option value="${p.name}">${p.name}</option>`;
|
||||
});
|
||||
playlists += '</ul>'
|
||||
|
||||
@ -838,6 +854,7 @@ async function newPlaylist() {
|
||||
document.getElementById("newPlaylistForm").reset();
|
||||
VUEPLAYERCORE.playlists.push({ name: title, type: 'playlist'});
|
||||
document.getElementById('pop-f').innerHTML += `<div class="pop-list-item" onclick="addToPlaylistUI('${title}')">• ${title}</div>`;
|
||||
document.getElementById('live-playlist-select').innerHTML += `<option value="${title}">${title}</option>`;
|
||||
|
||||
if (programState[0].state === 'allPlaylists') {
|
||||
getAllPlaylists();
|
||||
@ -848,6 +865,80 @@ async function newPlaylist() {
|
||||
document.getElementById('new_playlist').disabled = false;
|
||||
}
|
||||
|
||||
async function setLivePlaylist() {
|
||||
try{
|
||||
document.getElementById('set_live_playlist').disabled = true;
|
||||
|
||||
if (VUEPLAYERCORE.livePlaylist.name !== false) {
|
||||
VUEPLAYERCORE.livePlaylist.name = false;
|
||||
document.getElementById('set_live_playlist').classList.remove('blue');
|
||||
document.getElementById('set_live_playlist').classList.add('green');
|
||||
document.getElementById('set_live_playlist').value = 'Enable Live Playlist';
|
||||
document.getElementById('live-playlist-hide-these').hidden = false;
|
||||
myModal.close();
|
||||
return;
|
||||
}
|
||||
|
||||
let livePlaylistName;
|
||||
|
||||
if (document.getElementById('radio-use-existing').checked === true) {
|
||||
if (document.getElementById('live-playlist-select').value === "") {
|
||||
const err = new Error('No Playlist Selected');
|
||||
err.responseJSON = { error: 'No Playlist Selected' };
|
||||
throw err;
|
||||
}
|
||||
livePlaylistName = document.getElementById('live-playlist-select').value;
|
||||
} else {
|
||||
if (document.getElementById('new-live-playlist-name').value === "") {
|
||||
const err = new Error('Playlist Name Required');
|
||||
err.responseJSON = { error: 'Playlist Name Required' };
|
||||
throw err;
|
||||
}
|
||||
livePlaylistName = document.getElementById('new-live-playlist-name').value;
|
||||
}
|
||||
|
||||
// check if checkbox is checked
|
||||
if(document.getElementById('persist_live_queue').checked === true) {
|
||||
localStorage.setItem('live-playlist-auto-start', livePlaylistName)
|
||||
} else {
|
||||
localStorage.removeItem('live-playlist-auto-start');
|
||||
}
|
||||
|
||||
// set live var
|
||||
VUEPLAYERCORE.livePlaylist.name = livePlaylistName;
|
||||
|
||||
// get current playlist
|
||||
const response = await MSTREAMAPI.loadPlaylist(VUEPLAYERCORE.livePlaylist.name);
|
||||
|
||||
// set the queue to the current playlist
|
||||
if (response.length > 0) {
|
||||
MSTREAMPLAYER.clearPlaylist();
|
||||
response.forEach(value => {
|
||||
VUEPLAYERCORE.addSongWizard(value.filepath, value.metadata, false, undefined, false, true);
|
||||
});
|
||||
} else {
|
||||
// save current queue
|
||||
const songs = [];
|
||||
for (let i = 0; i < MSTREAMPLAYER.playlist.length; i++) {
|
||||
songs.push(MSTREAMPLAYER.playlist[i].filepath);
|
||||
}
|
||||
MSTREAMAPI.savePlaylist(livePlaylistName, songs, true);
|
||||
}
|
||||
|
||||
document.getElementById('set_live_playlist').classList.remove('green');
|
||||
document.getElementById('set_live_playlist').classList.add('blue');
|
||||
document.getElementById('set_live_playlist').value = 'Disable Live Playlist';
|
||||
document.getElementById('live-playlist-hide-these').hidden = true;
|
||||
|
||||
// close modal
|
||||
myModal.close();
|
||||
} catch(err) {
|
||||
boilerplateFailure(err);
|
||||
} finally {
|
||||
document.getElementById('set_live_playlist').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function savePlaylist() {
|
||||
if (MSTREAMPLAYER.playlist.length == 0) {
|
||||
iziToast.warning({
|
||||
@ -883,8 +974,11 @@ async function savePlaylist() {
|
||||
|
||||
VUEPLAYERCORE.playlists.push({ name: title, type: 'playlist'});
|
||||
document.getElementById('pop-f').innerHTML += `<div class="pop-list-item" onclick="addToPlaylistUI('${title}')">• ${title}</div>`;
|
||||
document.getElementById('live-playlist-select').innerHTML += `<option value="${title}">${title}</option>`;
|
||||
}catch(err) {
|
||||
boilerplateFailure(err);
|
||||
} finally {
|
||||
document.getElementById('save_playlist').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -895,7 +989,12 @@ async function getAllArtists() {
|
||||
programState = [{ state: 'allArtists' }];
|
||||
|
||||
try {
|
||||
const response = await MSTREAMAPI.artists();
|
||||
const response = await MSTREAMAPI.artists({
|
||||
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
|
||||
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
|
||||
})
|
||||
});
|
||||
|
||||
// parse through the json array and make an array of corresponding divs
|
||||
let artists = '<ul class="collection">';
|
||||
response.artists.forEach(value => {
|
||||
@ -929,7 +1028,13 @@ async function getArtistsAlbums(artist) {
|
||||
document.getElementById('filelist').innerHTML = getLoadingSvg();
|
||||
|
||||
try {
|
||||
const response = await MSTREAMAPI.artistAlbums(artist);
|
||||
const response = await MSTREAMAPI.artistAlbums({
|
||||
artist: artist,
|
||||
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
|
||||
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
|
||||
})
|
||||
});
|
||||
|
||||
let albums = '<ul>';
|
||||
response.albums.forEach(value => {
|
||||
const albumString = value.name ? value.name : 'SINGLES';
|
||||
@ -956,7 +1061,12 @@ async function getAllAlbums() {
|
||||
programState = [{ state: 'allAlbums' }];
|
||||
|
||||
try {
|
||||
const response = await MSTREAMAPI.albums();
|
||||
const response = await MSTREAMAPI.albums({
|
||||
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
|
||||
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
|
||||
})
|
||||
});
|
||||
|
||||
//parse through the json array and make an array of corresponding divs
|
||||
let albums = '<ul class="collection">';
|
||||
response.albums.forEach(value => {
|
||||
@ -1001,7 +1111,15 @@ async function getAlbumSongs(album, artist, year) {
|
||||
document.getElementById('localSearchBar').value = '';
|
||||
|
||||
try {
|
||||
const response = await MSTREAMAPI.albumSongs(album, artist, year)
|
||||
const response = await MSTREAMAPI.albumSongs({
|
||||
album,
|
||||
artist,
|
||||
year,
|
||||
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
|
||||
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
|
||||
})
|
||||
});
|
||||
|
||||
//parse through the json array and make an array of corresponding divs
|
||||
let files = '<ul class="collection">';
|
||||
response.forEach(song => {
|
||||
@ -1024,7 +1142,11 @@ async function getRatedSongs() {
|
||||
programState = [{ state: 'allRated' }];
|
||||
|
||||
try {
|
||||
const response = await MSTREAMAPI.getRated();
|
||||
const response = await MSTREAMAPI.getRated({
|
||||
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
|
||||
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
|
||||
})
|
||||
});
|
||||
//parse through the json array and make an array of corresponding divs
|
||||
let files = '';
|
||||
response.forEach(value => {
|
||||
@ -1040,10 +1162,10 @@ async function getRatedSongs() {
|
||||
});
|
||||
|
||||
files += createMusicFileHtml(value.filepath,
|
||||
value.metadata.title ? value.metadata.title : value.filepath,
|
||||
value.metadata['album-art'] ? `src="${MSTREAMAPI.currentServer.host}album-art/${value.metadata['album-art']}?token=${MSTREAMAPI.currentServer.token}"` : `src="assets/img/default.png"`,
|
||||
value.metadata.title ? value.metadata.title : value.filepath.split('/').pop(),
|
||||
value.metadata['album-art'] ? `src="${MSTREAMAPI.currentServer.host}album-art/${value.metadata['album-art']}?compress=s&token=${MSTREAMAPI.currentServer.token}"` : `src="assets/img/default.png"`,
|
||||
rating,
|
||||
value.metadata.artist ? `<span style="font-size:15px;">${value.metadata.artist}</span>` : undefined);
|
||||
value.metadata.artist ? `<span style="font-size:15px;">${value.metadata.artist}</span>` : '');
|
||||
});
|
||||
|
||||
document.getElementById('filelist').innerHTML = files;
|
||||
@ -1053,6 +1175,106 @@ async function getRatedSongs() {
|
||||
}
|
||||
}
|
||||
|
||||
///////////////// Recently Played
|
||||
function getRecentlyPlayed() {
|
||||
setBrowserRootPanel('Recently Played');
|
||||
document.getElementById('filelist').innerHTML = getLoadingSvg();
|
||||
document.getElementById('directoryName').innerHTML = 'Get last <input onkeydown="submitRecentlyPlayed();" onfocusout="redoRecentlyPlayed();" id="recently-played-limit" class="recently-added-input" type="number" min="1" step="1" value="100"> songs';
|
||||
|
||||
redoRecentlyPlayed();
|
||||
}
|
||||
|
||||
async function redoRecentlyPlayed() {
|
||||
currentBrowsingList = [];
|
||||
programState = [{ state: 'recentlyPlayed'}];
|
||||
|
||||
try {
|
||||
const response = await MSTREAMAPI.getRecentlyPlayed(
|
||||
document.getElementById('recently-played-limit').value,
|
||||
Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
|
||||
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
|
||||
}));
|
||||
|
||||
//parse through the json array and make an array of corresponding divs
|
||||
let filelist = '<ul class="collection">';
|
||||
response.forEach(el => {
|
||||
currentBrowsingList.push({
|
||||
type: 'file',
|
||||
name: el.metadata.title ? el.metadata.artist + ' - ' + el.metadata.title : el.filepath.split("/").pop()
|
||||
});
|
||||
|
||||
filelist += createMusicFileHtml(el.filepath,
|
||||
el.metadata.title ? `${el.metadata.title}`: el.filepath.split("/").pop(),
|
||||
el.metadata['album-art'] ? `src="${MSTREAMAPI.currentServer.host}album-art/${el.metadata['album-art']}?compress=s&token=${MSTREAMAPI.currentServer.token}"` : `src="assets/img/default.png"`,
|
||||
undefined,
|
||||
el.metadata.artist ? `<span style="font-size:15px;">${el.metadata.artist}</span>` : '');
|
||||
});
|
||||
|
||||
filelist += '</ul>'
|
||||
|
||||
document.getElementById('filelist').innerHTML = filelist;
|
||||
}catch(err) {
|
||||
document.getElementById('filelist').innerHTML = '<div>Server call failed</div>';
|
||||
return boilerplateFailure(err);
|
||||
}
|
||||
}
|
||||
|
||||
function submitRecentlyPlayed() {
|
||||
if (event.keyCode === 13) {
|
||||
document.getElementById("recently-played-limit").blur();
|
||||
}
|
||||
}
|
||||
|
||||
///////////////// Most Played
|
||||
function getMostPlayed() {
|
||||
setBrowserRootPanel('Most Played');
|
||||
document.getElementById('filelist').innerHTML = getLoadingSvg();
|
||||
document.getElementById('directoryName').innerHTML = 'Get last <input onkeydown="submitMostPlayed();" onfocusout="redoMostPlayed();" id="most-played-limit" class="recently-added-input" type="number" min="1" step="1" value="100"> songs';
|
||||
|
||||
redoMostPlayed();
|
||||
}
|
||||
|
||||
async function redoMostPlayed() {
|
||||
currentBrowsingList = [];
|
||||
programState = [{ state: 'mostPlayed'}];
|
||||
|
||||
try {
|
||||
const response = await MSTREAMAPI.getMostPlayed(
|
||||
document.getElementById('most-played-limit').value,
|
||||
Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
|
||||
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
|
||||
}));
|
||||
|
||||
//parse through the json array and make an array of corresponding divs
|
||||
let filelist = '<ul class="collection">';
|
||||
response.forEach(el => {
|
||||
currentBrowsingList.push({
|
||||
type: 'file',
|
||||
name: el.metadata.title ? el.metadata.artist + ' - ' + el.metadata.title : el.filepath.split("/").pop()
|
||||
});
|
||||
|
||||
filelist += createMusicFileHtml(el.filepath,
|
||||
el.metadata.title ? `${el.metadata.title}`: el.filepath.split("/").pop(),
|
||||
el.metadata['album-art'] ? `src="${MSTREAMAPI.currentServer.host}album-art/${el.metadata['album-art']}?compress=s&token=${MSTREAMAPI.currentServer.token}"` : `src="assets/img/default.png"`,
|
||||
undefined,
|
||||
el.metadata.artist ? `<span style="font-size:15px;">${el.metadata.artist} [${el.metadata['play-count']} plays]</span>` : `<span style="font-size:15px;">[${el.metadata['play-count']} plays]</span>`);
|
||||
});
|
||||
|
||||
filelist += '</ul>'
|
||||
|
||||
document.getElementById('filelist').innerHTML = filelist;
|
||||
}catch(err) {
|
||||
document.getElementById('filelist').innerHTML = '<div>Server call failed</div>';
|
||||
return boilerplateFailure(err);
|
||||
}
|
||||
}
|
||||
|
||||
function submitMostPlayed() {
|
||||
if (event.keyCode === 13) {
|
||||
document.getElementById("most-played-limit").blur();
|
||||
}
|
||||
}
|
||||
|
||||
///////////////// Recently Added
|
||||
function getRecentlyAdded() {
|
||||
setBrowserRootPanel('Recently Added');
|
||||
@ -1067,7 +1289,12 @@ async function redoRecentlyAdded() {
|
||||
programState = [{ state: 'recentlyAdded'}];
|
||||
|
||||
try {
|
||||
const response = await MSTREAMAPI.getRecentlyAdded(document.getElementById('recently-added-limit').value);
|
||||
const response = await MSTREAMAPI.getRecentlyAdded(
|
||||
document.getElementById('recently-added-limit').value,
|
||||
Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
|
||||
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
|
||||
}));
|
||||
|
||||
//parse through the json array and make an array of corresponding divs
|
||||
let filelist = '<ul class="collection">';
|
||||
response.forEach(el => {
|
||||
@ -1078,9 +1305,9 @@ async function redoRecentlyAdded() {
|
||||
|
||||
filelist += createMusicFileHtml(el.filepath,
|
||||
el.metadata.title ? `${el.metadata.title}`: el.filepath.split("/").pop(),
|
||||
el.metadata['album-art'] ? `src="${MSTREAMAPI.currentServer.host}album-art/${el.metadata['album-art']}?token=${MSTREAMAPI.currentServer.token}"` : `src="assets/img/default.png"`,
|
||||
el.metadata['album-art'] ? `src="${MSTREAMAPI.currentServer.host}album-art/${el.metadata['album-art']}?compress=s&token=${MSTREAMAPI.currentServer.token}"` : `src="assets/img/default.png"`,
|
||||
undefined,
|
||||
el.metadata.artist ? `<span style="font-size:15px;">${el.metadata.artist}</span>` : undefined);
|
||||
el.metadata.artist ? `<span style="font-size:15px;">${el.metadata.artist}</span>` : '');
|
||||
});
|
||||
|
||||
filelist += '</ul>'
|
||||
@ -1201,8 +1428,13 @@ function getMobilePanel(){
|
||||
<a target="_blank" href="https://play.google.com/store/apps/details?id=mstream.music&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1">
|
||||
<img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png'/>
|
||||
</a>
|
||||
<div class="mobile-placeholder"> </div>
|
||||
</div>
|
||||
<div class="mobile-links pad-6">
|
||||
<a target="_blank" href="https://apps.apple.com/us/app/mstream-player/id1605378892">
|
||||
<img alt='Get it on The App Store' src='assets/img/app-store-logo.png'/>
|
||||
</a>
|
||||
</div>
|
||||
<br>
|
||||
<div class="pad-6">
|
||||
<a target="_blank" href="/qr"><b>Checkout the QR Code tool to help add your server to the app</b></a>
|
||||
</div>`;
|
||||
@ -1314,7 +1546,14 @@ function createJukeboxPanel() {
|
||||
return '<div class="pad-6">An error occurred. Please refresh the page and try again</div>';
|
||||
}
|
||||
|
||||
const address = `${window.location.protocol}//${window.location.host}/remote/${JUKEBOX.stats.adminCode}`;
|
||||
let address = '';
|
||||
if(MSTREAMAPI.currentServer.host) {
|
||||
address = `${MSTREAMAPI.currentServer.host}remote/${JUKEBOX.stats.adminCode}`;
|
||||
}else {
|
||||
address = `${window.location.protocol}//${window.location.host}/remote/${JUKEBOX.stats.adminCode}`;
|
||||
}
|
||||
|
||||
// const address = `${window.location.protocol}//${window.location.host}/remote/${JUKEBOX.stats.adminCode}`;
|
||||
return `<div class="autoselect pad-6">
|
||||
<h4>Code: ${JUKEBOX.stats.adminCode}</h4>
|
||||
<h4><a target="_blank" href="${address}">${address}</a><h4>
|
||||
@ -1455,7 +1694,13 @@ async function submitSearchForm() {
|
||||
try {
|
||||
document.getElementById('search-results').innerHTML += '<div class="loading-screen"><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>'
|
||||
|
||||
var postObject = { search: document.getElementById('search-term').value};
|
||||
const postObject = {
|
||||
search: document.getElementById('search-term').value,
|
||||
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
|
||||
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
|
||||
})
|
||||
};
|
||||
|
||||
if (document.getElementById("search-in-artists") && document.getElementById("search-in-artists").checked === false) { postObject.noArtists = true; }
|
||||
searchToggles.artists = document.getElementById("search-in-artists").checked;
|
||||
if (document.getElementById("search-in-albums") && document.getElementById("search-in-albums").checked === false) { postObject.noAlbums = true; }
|
||||
@ -1510,6 +1755,31 @@ async function submitSearchForm() {
|
||||
}
|
||||
}
|
||||
|
||||
///////////////// Config
|
||||
function advancedConfig() {
|
||||
setBrowserRootPanel('Config', false);
|
||||
|
||||
let newHtml = `<div class="pad-6">
|
||||
<h5>Use Folders</h5>
|
||||
<p>Unchecked folders will be ignored in all DB queries (including Auto DJ)</p>`;
|
||||
|
||||
for (let i = 0; i < MSTREAMAPI.currentServer.vpaths.length; i++) {
|
||||
let checkedString = '';
|
||||
if (!MSTREAMPLAYER.ignoreVPaths[MSTREAMAPI.currentServer.vpaths[i]]) {
|
||||
checkedString = 'checked';
|
||||
}
|
||||
newHtml += `
|
||||
<label for="autodj-folder-${MSTREAMAPI.currentServer.vpaths[i]}">
|
||||
<input ${checkedString} id="autodj-folder-${MSTREAMAPI.currentServer.vpaths[i]}" type="checkbox"
|
||||
value="${MSTREAMAPI.currentServer.vpaths[i]}" name="autodj-folders" onchange="onAutoDJFolderChange(this)">
|
||||
<span>${MSTREAMAPI.currentServer.vpaths[i]}</span>
|
||||
</label><br>`;
|
||||
}
|
||||
|
||||
document.getElementById('filelist').innerHTML = newHtml;
|
||||
}
|
||||
|
||||
|
||||
////////////////// Layout
|
||||
function setupLayoutPanel() {
|
||||
setBrowserRootPanel('Layout', false);
|
||||
@ -1583,15 +1853,27 @@ async function updateServer() {
|
||||
try {
|
||||
document.getElementById('save_server').disabled = true;
|
||||
|
||||
let host = document.getElementById('server_address').value;
|
||||
if (host.slice(-1) !== '/') {
|
||||
host += '/';
|
||||
}
|
||||
|
||||
const res = await MSTREAMAPI.login(document.getElementById('server_username').value,
|
||||
document.getElementById('server_password').value,
|
||||
document.getElementById('server_address').value);
|
||||
host);
|
||||
|
||||
MSTREAMAPI.currentServer.host = document.getElementById('server_address').value;
|
||||
MSTREAMAPI.currentServer.host = host;
|
||||
MSTREAMAPI.currentServer.username = document.getElementById('server_username').value;
|
||||
|
||||
MSTREAMAPI.currentServer.token = res.token;
|
||||
|
||||
myModal.close();
|
||||
|
||||
init();
|
||||
loadFileExplorer();
|
||||
localStorage.setItem('current-server', JSON.stringify(MSTREAMAPI.currentServer));
|
||||
document.getElementById('server_password').value = '';
|
||||
}catch(err) {
|
||||
console.log(err)
|
||||
boilerplateFailure(err);
|
||||
}finally {
|
||||
document.getElementById('save_server').disabled = false;
|
||||
@ -1630,6 +1912,16 @@ function initElectron() {
|
||||
<span>Edit Server</span>
|
||||
</div>`;
|
||||
|
||||
try {
|
||||
const curServer = JSON.parse(localStorage.getItem("current-server"));
|
||||
console.log(curServer);
|
||||
if (curServer.host && curServer.token) {
|
||||
MSTREAMAPI.currentServer.host = curServer.host;
|
||||
MSTREAMAPI.currentServer.token = curServer.token;
|
||||
MSTREAMAPI.currentServer.username = curServer.username;
|
||||
}
|
||||
}catch(err) {}
|
||||
|
||||
// check if server
|
||||
if (!MSTREAMAPI.currentServer.host) {
|
||||
openEditModal();
|
||||
|
||||
@ -997,4 +997,12 @@ select {
|
||||
|
||||
.mobile-links img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.no-margin p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.make-white {
|
||||
color: #FFF;
|
||||
}
|
||||
@ -1,6 +1,10 @@
|
||||
const VUEPLAYERCORE = (() => {
|
||||
const mstreamModule = {};
|
||||
|
||||
mstreamModule.livePlaylist = {
|
||||
name: false
|
||||
};
|
||||
|
||||
mstreamModule.altLayout = {
|
||||
'moveMeta': false,
|
||||
'audioBookCtrls': false,
|
||||
@ -86,17 +90,24 @@ const VUEPLAYERCORE = (() => {
|
||||
playlists: mstreamModule.playlists,
|
||||
showClear: showClearLink,
|
||||
altLayout: mstreamModule.altLayout,
|
||||
meta: MSTREAMPLAYER.playerStats.metadata
|
||||
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']}?token=${MSTREAMPLAYER.getCurrentSong().authToken}`;
|
||||
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);
|
||||
@ -111,6 +122,13 @@ const VUEPLAYERCORE = (() => {
|
||||
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 {
|
||||
@ -170,6 +188,13 @@ const VUEPLAYERCORE = (() => {
|
||||
},
|
||||
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");
|
||||
@ -369,10 +394,13 @@ const VUEPLAYERCORE = (() => {
|
||||
if (!this.meta['album-art']) {
|
||||
return 'assets/img/default.png';
|
||||
}
|
||||
return MSTREAMAPI.currentServer.host + `album-art/${this.meta['album-art']}?token=${MSTREAMPLAYER.getCurrentSong().authToken}`;
|
||||
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.
|
||||
@ -506,11 +534,12 @@ const VUEPLAYERCORE = (() => {
|
||||
}
|
||||
});
|
||||
|
||||
mstreamModule.addSongWizard = async (filepath, metadata, lookupMetadata, position) => {
|
||||
mstreamModule.addSongWizard = async (filepath, metadata, lookupMetadata, position, livePlaylist, autoPlayOff) => {
|
||||
// Escape filepath
|
||||
var rawFilepath = 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);
|
||||
}
|
||||
@ -535,8 +564,18 @@ const VUEPLAYERCORE = (() => {
|
||||
|
||||
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);
|
||||
MSTREAMPLAYER.addSong(newSong, autoPlayOff);
|
||||
if (mstreamModule.livePlaylist.name && livePlaylist !== false) {
|
||||
await MSTREAMAPI.addToPlaylist(mstreamModule.livePlaylist.name, newSong.filepath);
|
||||
}
|
||||
}
|
||||
|
||||
// perform lookup
|
||||
@ -550,5 +589,16 @@ const VUEPLAYERCORE = (() => {
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
})()
|
||||
|
||||
BIN
webapp/assets/img/app-store-logo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
webapp/assets/img/play-store-logo.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
@ -22,27 +22,14 @@ 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.href = `../login?redirect=${encodeURIComponent(window.location.pathname)}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
Cookies.remove('x-access-token');
|
||||
window.location.href = '../login';
|
||||
document.location.assign(window.location.href.replace('/admin', '') + (window.location.href.slice(-1) === '/' ? '' : '/') + 'login');
|
||||
}
|
||||
|
||||
module.goToPlayer = () => {
|
||||
window.location.assign('../');
|
||||
window.location.assign(window.location.href.replace('/admin', ''));
|
||||
}
|
||||
|
||||
module.axios = axios.create({
|
||||
|
||||
@ -177,6 +177,7 @@ var MSTREAMAPI = (function () {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -872,7 +872,7 @@ function getRatedSongs(el) {
|
||||
});
|
||||
|
||||
files += createMusicFileHtml(value.filepath,
|
||||
value.metadata.title ? value.metadata.title : value.filepath,
|
||||
value.metadata.title ? value.metadata.title : value.filepath.split('/').pop(),
|
||||
value.metadata['album-art'] ? `data-original="album-art/${value.metadata['album-art']}?token=${MSTREAMAPI.currentServer.token}"` : `src="assets/img/default.png"`,
|
||||
rating,
|
||||
value.metadata.artist ? `<span style="font-size:15px;">${value.metadata.artist}</span>` : undefined);
|
||||
@ -1387,13 +1387,6 @@ function openPlaybackModal() {
|
||||
myModal.open('#speedModal');
|
||||
}
|
||||
|
||||
function logout(){
|
||||
localStorage.removeItem('token');
|
||||
Cookies.remove('x-access-token');
|
||||
MSTREAMAPI.updateCurrentServer("", "", "");
|
||||
window.location.href = 'login';
|
||||
}
|
||||
|
||||
// Modals
|
||||
const myModal = new HystModal({});
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -40,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
|
||||
@ -71,7 +73,9 @@ const MSTREAMPLAYER = (() => {
|
||||
const params = {
|
||||
ignoreList: autoDjIgnoreArray,
|
||||
minRating: mstreamModule.minRating,
|
||||
ignoreVPaths: mstreamModule.ignoreVPaths
|
||||
ignoreVPaths: Object.keys(mstreamModule.ignoreVPaths).filter((vpath) => {
|
||||
return mstreamModule.ignoreVPaths[vpath] === true;
|
||||
})
|
||||
};
|
||||
|
||||
const res = await MSTREAMAPI.getRandomSong(params);
|
||||
@ -158,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;
|
||||
@ -471,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
|
||||
@ -485,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,
|
||||
@ -495,6 +500,11 @@ const MSTREAMPLAYER = (() => {
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -564,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;
|
||||
}
|
||||
}
|
||||
@ -607,6 +624,7 @@ const MSTREAMPLAYER = (() => {
|
||||
currentTime: 0,
|
||||
playing: false,
|
||||
shouldLoop: false,
|
||||
shouldLoopOne: false,
|
||||
shuffle: false,
|
||||
volume: 100,
|
||||
metadata: {
|
||||
@ -680,7 +698,7 @@ const MSTREAMPLAYER = (() => {
|
||||
url += `&algo=${mstreamModule.transcodeOptions.selectedAlgo}`;
|
||||
}
|
||||
}
|
||||
console.log(url)
|
||||
|
||||
player.playerObject.src = url;
|
||||
player.songObject = song;
|
||||
player.playerObject.load();
|
||||
@ -697,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();
|
||||
}
|
||||
@ -746,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) {
|
||||
@ -777,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
|
||||
@ -848,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) {
|
||||
|
||||
@ -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: {
|
||||
|
||||
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 |
@ -151,6 +151,57 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Playlist Modal -->
|
||||
<div class="hystmodal" id="livePlaylist" aria-hidden="true">
|
||||
<div class="hystmodal__wrap">
|
||||
<div class="hystmodal__window" role="dialog" aria-modal="true">
|
||||
<a data-hystclose class="hystmodal__close">Close</a>
|
||||
<div class="hystmodal__styled">
|
||||
<h5>Live Playlist Mode</h5>
|
||||
<p>
|
||||
Live playlist mode keeps your current queue synced to the server. For now this only works on the web app. It will be available on mobile soon.
|
||||
</p>
|
||||
<p>
|
||||
Using live playlist on two devices at the same time might result in some odd behavior
|
||||
</p>
|
||||
<form action="javascript:setLivePlaylist()">
|
||||
<label for="persist_live_queue">
|
||||
<input type="checkbox" checked class="filled-in" id="persist_live_queue" name="persist_live_queue" />
|
||||
<span>Start Live Playlist Automatically</span>
|
||||
</label>
|
||||
<br>
|
||||
|
||||
<div id="live-playlist-hide-these">
|
||||
<p>
|
||||
<label>
|
||||
<input class="with-gap" name="group1" type="radio" checked />
|
||||
<span>Make a new playlist</span>
|
||||
</label>
|
||||
<input id="new-live-playlist-name" type="text" name="live-queue-name" placeholder="Playlist Name" autocomplete="off" class="make-white">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label>
|
||||
<input class="with-gap" name="group1" type="radio" id="radio-use-existing" />
|
||||
<span>Use an existing playlist</span>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<select id="live-playlist-select" class="browser-default">
|
||||
<option value="" disabled selected>Select Playlist</option>
|
||||
</select>
|
||||
<!-- <label>Materialize Select</label> -->
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<input id="set_live_playlist" type="submit" class="btn green" value="Enable Live Playlist">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Server Modal -->
|
||||
<div class="hystmodal" id="editServer" aria-hidden="true">
|
||||
<div class="hystmodal__wrap">
|
||||
@ -188,6 +239,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadata Model -->
|
||||
<div class="hystmodal" id="metadataModel" 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 no-margin">
|
||||
<h5>Metadata</h5>
|
||||
<p><b>Title:</b> <span id="meta--title"></span></p>
|
||||
<p><b>Album:</b> <span id="meta--album"></span></p>
|
||||
<p><b>Artist:</b> <span id="meta--artist"></span></p>
|
||||
<p><b>Year:</b> <span id="meta--year"></span></p>
|
||||
<p><b>Track #:</b> <span id="meta--track"></span></p>
|
||||
<p><b>Disc #:</b><span id="meta--disk"></span></p>
|
||||
<p><b>Rating:</b> <span id="meta--rating"></span></p>
|
||||
<p><b>Replay Gain:</b> <span id="meta--rg"></span></p>
|
||||
<p><b>File Path:</b> <a href="#" target="_blank" id="meta--fp"></a></p>
|
||||
<p><b>Album Art:</b> <a href="#" target="_blank" id="meta--aa"></a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sidenav">
|
||||
<div class="side-nav-header">
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
@ -248,7 +322,15 @@
|
||||
</div>
|
||||
<div onclick="changeView(getRecentlyAdded, this)" class="side-nav-item my-waves">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="22" height="22"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm61.8-104.4l-84.9-61.7c-3.1-2.3-4.9-5.9-4.9-9.7V116c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v141.7l66.8 48.6c5.4 3.9 6.5 11.4 2.6 16.8L334.6 349c-3.9 5.3-11.4 6.5-16.8 2.6z"/></svg>
|
||||
<span>Recent</span>
|
||||
<span>Recent Added</span>
|
||||
</div>
|
||||
<div onclick="changeView(getRecentlyPlayed, this)" class="side-nav-item my-waves">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="22" height="22"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm61.8-104.4l-84.9-61.7c-3.1-2.3-4.9-5.9-4.9-9.7V116c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v141.7l66.8 48.6c5.4 3.9 6.5 11.4 2.6 16.8L334.6 349c-3.9 5.3-11.4 6.5-16.8 2.6z"/></svg>
|
||||
<span>Recently Played</span>
|
||||
</div>
|
||||
<div onclick="changeView(getMostPlayed, this)" class="side-nav-item my-waves">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="22px" viewBox="0 0 24 24" width="22px" fill="#FFFFFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M3.5 18.49l6-6.01 4 4L22 6.92l-1.41-1.41-7.09 7.97-4-4L2 16.99z"/></svg>
|
||||
<span>Most Played</span>
|
||||
</div>
|
||||
<div class="side-nav-item my-waves" onclick="changeView(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>
|
||||
@ -281,6 +363,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24" fill="#FFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 3h8v8H3zm10 0h8v8h-8zM3 13h8v8H3zm15 0h-2v3h-3v2h3v3h2v-3h3v-2h-3z"/></svg>
|
||||
<span>Layout</span>
|
||||
</div>
|
||||
<!-- <div class="side-nav-item my-waves" onclick="changeView(advancedConfig, this);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24" fill="#FFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 3h8v8H3zm10 0h8v8h-8zM3 13h8v8H3zm15 0h-2v3h-3v2h3v3h2v-3h3v-2h-3z"/></svg>
|
||||
<span>Config</span>
|
||||
</div> -->
|
||||
<div class="side-nav-header">
|
||||
<span>Navigation</span>
|
||||
</div>
|
||||
@ -313,7 +399,7 @@
|
||||
<div class="col s12 m4 l3 h100-res">
|
||||
<div class="card aa-card">
|
||||
<div v-if="altLayout.moveMeta === false" class="card-image hide-on-small-only">
|
||||
<img :src="albumArtPath" />
|
||||
<img class="pointer" :src="albumArtPath" v-on:click="getSongInfo" />
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
@ -338,7 +424,7 @@
|
||||
<div class="card-content">
|
||||
<p>
|
||||
<b>Title:</b>
|
||||
{{ (meta.title) ? meta.title : '' }}
|
||||
{{ (meta.title) ? meta.title : (meta.filepath ? meta.filepath.split('/').pop() : '') }}
|
||||
</p>
|
||||
<p>
|
||||
<b>Artist:</b>
|
||||
@ -360,7 +446,9 @@
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div v-if="altLayout.audioBookCtrls === false" class="button-block">
|
||||
<svg class="pointer margin-lr" v-on:click="toggleRepeat" v-bind:class="{ 'aux-button-active': playerStats.shouldLoop }" xmlns="http://www.w3.org/2000/svg" version="1" viewBox="0 0 24 24" width="25" height="25"><path d="M17 2v3H6C4.3 5 3 6.3 3 8v6.813l2-1.626V8c0-.6.4-1 1-1h11v3l5-4-5-4zm4 7.188l-2 1.624V16c0 .6-.4 1-1 1H7v-3l-5 4 5 4v-3h11c1.7 0 3-1.3 3-3V9.187z"/></svg>
|
||||
<svg v-if="playerStats.shouldLoopOne === false" class="pointer margin-lr" v-on:click="toggleRepeat" v-bind:class="{ 'aux-button-active': playerStats.shouldLoop }" xmlns="http://www.w3.org/2000/svg" version="1" viewBox="0 0 24 24" width="25" height="25"><path d="M17 2v3H6C4.3 5 3 6.3 3 8v6.813l2-1.626V8c0-.6.4-1 1-1h11v3l5-4-5-4zm4 7.188l-2 1.624V16c0 .6-.4 1-1 1H7v-3l-5 4 5 4v-3h11c1.7 0 3-1.3 3-3V9.187z"/></svg>
|
||||
<svg v-else class="pointer margin-lr aux-button-active" v-on:click="toggleRepeat" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="25" height="25"><path d="M23.3 29.9V20.45H20.5V18H25.75V29.9ZM14 44 6 36 14 28 16.1 30.2 11.8 34.5H35V26.5H38V37.5H11.8L16.1 41.8ZM10 21.5V10.5H36.2L31.9 6.2L34 4L42 12L34 20L31.9 17.8L36.2 13.5H13V21.5Z"/></svg>
|
||||
|
||||
<svg class="pointer margin-lr" v-on:click="toggleShuffle" v-bind:class="{ 'aux-button-active': playerStats.shuffle }" xmlns="http://www.w3.org/2000/svg" version="1" viewBox="0 0 24 24" width="25" height="25"><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>
|
||||
<svg class="auto-dj" v-on:click="toggleAutoDJ" 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" ><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>
|
||||
@ -438,6 +526,10 @@
|
||||
<h5>Now Playing</h5>
|
||||
</div>
|
||||
<div class="flex grow flex-end" id="playlist-buttons">
|
||||
<a title="Live Playlist" onclick="openLivePlaylistModal();">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-bind:class="{ 'aux-button-active': livePlaylist.name }" xml:space="preserve" width="25" height="25" viewBox="0 0 512 512"><path d="M48 64h160c17.672 0 32-14.328 32-32 0-17.674-14.328-32-32-32H48C30.328 0 16 14.326 16 32c0 17.672 14.328 32 32 32zM48 160h160c17.672 0 32-14.328 32-32 0-17.674-14.328-32-32-32H48c-17.672 0-32 14.326-32 32 0 17.672 14.328 32 32 32zM240 224c0-17.674-14.328-32-32-32H48c-17.672 0-32 14.326-32 32 0 17.672 14.328 32 32 32h160c17.672 0 32-14.328 32-32z"/><path d="M411.328 75.914C393.043 61.805 368 42.477 368 32c0-17.672-14.328-32-32-32s-32 14.328-32 32v293.58c-10.023-3.549-20.762-5.58-32-5.58-53.02 0-96 42.98-96 96s42.98 96 96 96 96-42.98 96-96V123.293c1.414 1.094 2.82 2.203 4.23 3.293 36.105 27.852 59.77 48.078 59.77 74.305 0 40.766-21.684 63.516-22.305 64.164-12.672 12.32-12.961 32.578-.641 45.25 6.273 6.453 14.605 9.695 22.949 9.695 8.035 0 16.082-3.008 22.301-9.055 4.27-4.148 41.695-42.484 41.695-110.055.001-59.64-46.948-95.867-84.671-124.976z"/></svg>
|
||||
</a>
|
||||
|
||||
<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>
|
||||
@ -447,7 +539,7 @@
|
||||
<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();">
|
||||
<a title="Clear Playlist" onclick="VUEPLAYERCORE.clearQueue();">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="11 11 26 26" 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>
|
||||
@ -456,12 +548,12 @@
|
||||
<div v-if="altLayout.moveMeta === true" style="background-color: #1a1a1a !important;" class="card card-mod data-card hide-on-small-only">
|
||||
<div class="card-content flex">
|
||||
<div class="custom-card-img">
|
||||
<img :src="albumArtPath" class="block" />
|
||||
<img :src="albumArtPath" class="block pointer" v-on:click="getSongInfo" />
|
||||
</div>
|
||||
<div class="card-meta">
|
||||
<p>
|
||||
<b>Title:</b>
|
||||
{{ (meta.title) ? meta.title : '' }}
|
||||
{{ (meta.title) ? meta.title : (meta.filepath ? meta.filepath.split('/').pop() : '') }}
|
||||
</p>
|
||||
<p>
|
||||
<b>Artist:</b>
|
||||
@ -484,6 +576,7 @@
|
||||
<a v-show="showClear.val === true" v-on:click="clearRating">clear</a>
|
||||
</div>
|
||||
<div style="visibility:hidden; position: absolute;" class="pop-d" id="pop-d" role="tooltip">
|
||||
<div class="pop-list-item" v-on:click="gsi2" >Song Info</div>
|
||||
<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>
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
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.webContents.openDevTools()
|
||||
win.loadFile('./index.html')
|
||||
}
|
||||
|
||||
|
||||
@ -16,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.href = goTo;
|
||||
window.location.assign(window.location.href.replace('/login', ''));
|
||||
|
||||
iziToast.success({
|
||||
title: 'Login Success!',
|
||||
|
||||
@ -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])
|
||||
// }
|
||||
// })
|
||||