Compare commits

..

96 Commits

Author SHA1 Message Date
IrosTheBeggar
7d40b034ed Merge branch 'master' of github.com:IrosTheBeggar/mStream into master 2025-04-07 14:09:09 -04:00
IrosTheBeggar
0a22c1f738 fix missing limit bug 2025-04-07 14:08:44 -04:00
Paul
41c1432eeb
Merge pull request #489 from Hegi/feature/format-date-time-for-shared-playlist
feat: format expire date on admin ui
2025-04-05 20:29:28 -04:00
Istvan Hegedus
adacc6a0f6
feat: format expire date on admin ui 2025-01-27 22:31:01 -07:00
IrosTheBeggar
81a931c884 5.13.1 2024-12-21 11:27:27 -05:00
IrosTheBeggar
ace6d952ec remove 32 bit windows build 2024-12-21 11:27:11 -05:00
IrosTheBeggar
0547301905 5.13.0 2024-12-21 11:05:29 -05:00
IrosTheBeggar
50b0cf1d1e remove binaries 2024-12-21 11:03:19 -05:00
IrosTheBeggar
ea2736432b remove binaries 2024-12-21 11:00:57 -05:00
IrosTheBeggar
63d4d5eb02 code cleanup 2024-12-21 10:40:01 -05:00
IrosTheBeggar
dd8dc84a07 config to use new scan code 2024-12-21 01:37:35 -05:00
IrosTheBeggar
2b296220c2 config to use new scan code 2024-12-21 01:03:46 -05:00
IrosTheBeggar
bada002845 new scanner code 2024-12-21 00:45:57 -05:00
IrosTheBeggar
af2fad9dd1 compress images defaults to true 2024-12-21 00:27:10 -05:00
IrosTheBeggar
e6bb1b3322 Merge branch 'master' of github.com:IrosTheBeggar/mStream into master 2024-12-21 00:22:03 -05:00
IrosTheBeggar
17da6533de update packages 2024-12-21 00:19:48 -05:00
IrosTheBeggar
e38725e755 update packages 2024-12-21 00:15:40 -05:00
Paul
4055f5411b
Update README.md 2024-07-15 21:28:53 -04:00
Paul
183e45faf3
Update README.md 2024-07-15 21:26:18 -04:00
IrosTheBeggar
02ae8c1ada update github actions 2024-05-05 21:06:04 -04:00
IrosTheBeggar
6488cd1db7 v5.12.2 2024-05-05 20:42:41 -04:00
IrosTheBeggar
09dd860bd3 update npm packages 2024-05-05 20:40:48 -04:00
IrosTheBeggar
4200b08e5a update github actions 2024-05-05 20:36:41 -04:00
IrosTheBeggar
b9945ba75e update github action 2024-05-05 20:31:28 -04:00
IrosTheBeggar
69332b6393 v5.12.1 2024-05-05 10:46:19 -04:00
IrosTheBeggar
b759ef8d48 update github actions 2024-05-05 10:41:25 -04:00
IrosTheBeggar
3bd282dd4a fix node min version 2024-05-05 10:38:58 -04:00
IrosTheBeggar
688dc55c9e fix node min version 2024-05-05 10:37:11 -04:00
IrosTheBeggar
9ddb83761c v5.12.0 2024-03-13 23:42:53 -04:00
IrosTheBeggar
380bda7652 update electron version 2024-03-13 23:42:24 -04:00
IrosTheBeggar
164a97f86a update dependency 2024-03-13 23:37:46 -04:00
IrosTheBeggar
14b6c92644 update dependacy 2024-03-13 16:40:53 -04:00
IrosTheBeggar
70fdf88f88 fix xml parser 2024-03-13 16:17:23 -04:00
IrosTheBeggar
3e7c5092e5 fix syncthing enable api 2024-03-13 16:08:43 -04:00
IrosTheBeggar
c686ace36c live playlist mode 2024-02-07 00:46:41 -05:00
IrosTheBeggar
b66c6d5be3 live playlist mode 2024-02-07 00:34:49 -05:00
IrosTheBeggar
c37b87e631 live playlist mode 2024-01-28 19:30:34 -05:00
Paul
06f15420d8
Merge pull request #415 from nguyenbnt/master
Fix config folders when adding a new directory
2023-11-09 09:36:51 -05:00
IrosTheBeggar
795d68cbe5 update npm dependencies 2023-11-07 16:04:16 -05:00
Nguyen Bui
49924b3450 Fix config folders when adding a new directory 2023-06-28 10:44:55 +07:00
IrosTheBeggar
01d63ed9e1 remove regex validation for username 2022-12-10 23:10:07 -05:00
IrosTheBeggar
d58ec37679 revert music metadata 2022-12-10 00:44:01 -05:00
IrosTheBeggar
ba51894cab update deps 2022-12-10 00:29:41 -05:00
IrosTheBeggar
c1a2114773 update electron builder 2022-12-10 00:22:06 -05:00
Paul
fb1e080a42
Merge pull request #384 from viktor02/master
Added page title changes
2022-12-10 00:04:51 -05:00
Paul
c84b4c822d
Merge pull request #398 from tdammers/issue-397
Escape question marks in filenames
2022-12-09 23:56:51 -05:00
Paul
1146fd914f
Merge pull request #388 from thewriteway/master
update dependencies to latest supported versions.
2022-12-09 23:49:16 -05:00
Tobias Dammers
cb9992836c Escape question marks in filenames 2022-11-30 14:15:50 +01:00
chosenpath
d0077659c6 update m3u8-parser dependency 2022-09-29 09:02:21 +01:00
chosenpath
b37414745f update dependencies to latest supported versions. 2022-09-27 17:13:54 +01:00
Viktor Karpov
30af7689f9 fix: swap artist and name position 2022-08-03 06:18:26 +03:00
Viktor Karpov
a7439ebf3b Added page title changes 2022-08-03 03:47:42 +03:00
IrosTheBeggar
6d5fb03e3f loop one song option 2022-04-26 21:31:35 -04:00
IrosTheBeggar
780fbc56d8 chnage default transcode algo 2022-04-05 13:09:29 -04:00
IrosTheBeggar
27f276bb31 ui update 2022-03-29 13:32:05 -04:00
IrosTheBeggar
e587b4b21d stats 2022-03-29 13:19:34 -04:00
IrosTheBeggar
ad9764a149 new scrobble api 2022-03-28 23:12:50 -04:00
IrosTheBeggar
5706ec8647 audio book folders on server 2022-03-26 18:00:13 -04:00
IrosTheBeggar
21565a42cb add genre to db 2022-03-26 00:47:57 -04:00
IrosTheBeggar
f037aa9d1d update dependencies 2022-03-24 14:37:23 -04:00
IrosTheBeggar
c94c5c5eea vpath filter for db api 2022-03-24 13:52:01 -04:00
IrosTheBeggar
765286ccd5 v5.11.4 2022-03-22 11:27:00 -04:00
IrosTheBeggar
24c177a163 selfhost play stoee logo 2022-03-21 14:20:08 -04:00
IrosTheBeggar
bad3895d06 Update readme 2022-03-21 14:01:11 -04:00
IrosTheBeggar
22c12ee030 v5.11.3 2022-02-27 01:45:17 -05:00
IrosTheBeggar
55205f12ad add fileapth to metadata panel 2022-02-27 01:43:05 -05:00
IrosTheBeggar
95439866e9 metadata info 2022-02-27 01:27:53 -05:00
IrosTheBeggar
d1c278ebf7 increase cookie experation time 2022-02-26 23:47:32 -05:00
IrosTheBeggar
0c327a3a77 install docs 2022-02-26 23:46:37 -05:00
IrosTheBeggar
40579fb5e9 v5.11.2 2022-02-22 00:13:18 -05:00
IrosTheBeggar
b79ebb6c07 edit server max request size 2022-02-22 00:05:12 -05:00
IrosTheBeggar
47cf9e366a allow larger requests 2022-02-21 12:33:10 -05:00
IrosTheBeggar
00411f7845 pull metadata on file explorer api 2022-02-20 20:14:52 -05:00
IrosTheBeggar
9e22625476 batch metadata endpoint 2022-02-20 19:30:52 -05:00
IrosTheBeggar
086ead9b17 v5.11.0 2022-02-02 13:36:13 -05:00
IrosTheBeggar
e5e8750e9c image compression script 2022-02-02 13:32:59 -05:00
IrosTheBeggar
825633152e add compression to webapp 2022-02-02 00:58:56 -05:00
IrosTheBeggar
f93ba629b4 update admin page 2022-02-02 00:45:43 -05:00
IrosTheBeggar
f0408dd659 compression edits 2022-02-02 00:28:16 -05:00
IrosTheBeggar
7ec3774ea7 update album art api 2022-02-01 21:34:31 -05:00
IrosTheBeggar
6e38686c30 image compression 2022-02-01 17:02:01 -05:00
IrosTheBeggar
cb1d48db4b bump min version 2022-01-31 10:45:13 -05:00
IrosTheBeggar
4f466e4595 v5.10.0 2022-01-31 01:22:19 -05:00
IrosTheBeggar
7f4f5283ec update dependacies 2022-01-31 01:21:03 -05:00
IrosTheBeggar
b4aa11af9a IPv6 support 2022-01-31 00:56:15 -05:00
IrosTheBeggar
3ea7a48b62 code cleanup 2022-01-31 00:24:55 -05:00
IrosTheBeggar
4763202afd fix redirect bug 2022-01-31 00:20:14 -05:00
IrosTheBeggar
48638fc363 fix async error bug 2022-01-30 22:52:10 -05:00
IrosTheBeggar
5ad80e40f5 another access error 2022-01-30 21:41:41 -05:00
IrosTheBeggar
de061eecb6 fix access error 2022-01-30 21:34:59 -05:00
IrosTheBeggar
e48bed3e0f v5.9.4 2021-12-29 10:27:56 -05:00
IrosTheBeggar
369ac4b68f whoops 2021-12-29 10:25:06 -05:00
IrosTheBeggar
723314a3e7 v5.9.2 2021-12-29 10:17:29 -05:00
IrosTheBeggar
db01674eaf fix pakcage json for webapp 2021-12-29 10:14:37 -05:00
IrosTheBeggar
e665e98680 build webapp step 2021-12-29 10:09:24 -05:00
IrosTheBeggar
5192139568 desktop app stuff 2021-12-29 10:00:59 -05:00
59 changed files with 9991 additions and 381 deletions

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

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

View File

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

View File

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

View File

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

1
.gitignore vendored
View File

@ -21,7 +21,6 @@ bin/ffmpeg/*
save/*.json save/*.json
package-lock.json
.DS_Store .DS_Store
.vscode/* .vscode/*

View File

@ -8,7 +8,7 @@ Main|Shared|Admin
## Demo & Other Links ## 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) #### [Discord Channel](https://discord.gg/AM896Rr)
@ -28,32 +28,33 @@ Main|Shared|Admin
## Installing mStream ## Installing mStream
* [Install From Source](docs/install.md)
* [Docker Instructions](https://github.com/linuxserver/docker-mstream) * [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 * [Binaries for Win/OSX/Linux](https://mstream.io/server)
- Runs in background and starts mStream on boot * [Install From Source](docs/install.md)
- Automatic updates
- Adds a tray icon to manage mStream
* [AWS Cloud using Terraform](https://gitlab.com/SiliconTao-Systems/nova) * [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 ## Quick Install from CLI
Deploying an mStream server is simple.
```shell ```shell
# Install From Git # Install From Git
git clone https://github.com/IrosTheBeggar/mStream.git git clone https://github.com/IrosTheBeggar/mStream.git
cd mStream cd mStream
npm install --production
# Boot mStream # Install dependencies and run
node cli-boot-wrapper.js npm run-script wizard
``` ```
## Android App
[![mStream Android App](https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png)](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 ## Technical Details
* **Dependencies:** NodeJS v10 or greater * **Dependencies:** NodeJS v10 or greater

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

8107
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -8,6 +8,7 @@ const fileExplorer = require('../util/file-explorer');
const admin = require('../util/admin'); const admin = require('../util/admin');
const config = require('../state/config'); const config = require('../state/config');
const dbQueue = require('../db/task-queue'); const dbQueue = require('../db/task-queue');
const imageCompress = require('../db/image-compress-manager');
const transcode = require('./transcode'); const transcode = require('./transcode');
const db = require('../db/manager'); const db = require('../db/manager');
const { joiValidate } = require('../util/validation'); const { joiValidate } = require('../util/validation');
@ -107,6 +108,16 @@ exports.setup = (mstream) => {
res.json({}); 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) => { mstream.post("/api/v1/admin/db/params/pause", async (req, res) => {
const schema = Joi.object({ const schema = Joi.object({
pause: Joi.number().integer().min(0).required() pause: Joi.number().integer().min(0).required()
@ -137,6 +148,16 @@ exports.setup = (mstream) => {
res.json({}); 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) => { mstream.get("/api/v1/admin/users", (req, res) => {
// Scrub passwords // Scrub passwords
const memClone = JSON.parse(JSON.stringify(config.program.users)); const memClone = JSON.parse(JSON.stringify(config.program.users));
@ -153,11 +174,17 @@ exports.setup = (mstream) => {
const schema = Joi.object({ const schema = Joi.object({
directory: Joi.string().required(), directory: Joi.string().required(),
vpath: Joi.string().pattern(/[a-zA-Z0-9-]+/).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); 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({}); res.json({});
try { try {
@ -194,6 +221,10 @@ exports.setup = (mstream) => {
); );
res.json({}); 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) => { mstream.post("/api/v1/admin/db/scan/all", (req, res) => {
dbQueue.scanAll(); dbQueue.scanAll();
@ -276,10 +307,21 @@ exports.setup = (mstream) => {
writeLogs: config.program.writeLogs, writeLogs: config.program.writeLogs,
secret: config.program.secret.slice(-4), secret: config.program.secret.slice(-4),
ssl: config.program.ssl, 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) => { mstream.post("/api/v1/admin/config/port", async (req, res) => {
const schema = Joi.object({ const schema = Joi.object({
port: Joi.number().required() port: Joi.number().required()

View File

@ -22,6 +22,7 @@ exports.setup = (mstream) => {
const token = jwt.sign({ username: req.body.username }, config.program.secret); const token = jwt.sign({ username: req.body.username }, config.program.secret);
res.cookie('x-access-token', token, { res.cookie('x-access-token', token, {
maxAge: 157784630000, // 5 years in ms
sameSite: 'Strict', sameSite: 'Strict',
}); });

View File

@ -40,18 +40,23 @@ function renderMetadataObj(row) {
"year": row.year ? row.year : null, "year": row.year ? row.year : null,
"album-art": row.aaFile ? row.aaFile : null, "album-art": row.aaFile ? row.aaFile : null,
"rating": row.rating ? row.rating : 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 "replaygain-track": row.replaygainTrack ? row.replaygainTrack : null
} }
}; };
} }
function renderOrClause(vpaths) { function renderOrClause(vpaths, ignoreVPaths) {
if (vpaths.length === 1) { if (vpaths.length === 1) {
return { 'vpath': { '$eq': vpaths[0] } }; return { 'vpath': { '$eq': vpaths[0] } };
} }
const returnThis = { '$or': [] } const returnThis = { '$or': [] }
for (let vpath of vpaths) { for (let vpath of vpaths) {
if (ignoreVPaths && typeof ignoreVPaths === 'object' && ignoreVPaths.includes(vpath)) {
continue;
}
returnThis['$or'].push({ 'vpath': { '$eq': vpath } }) returnThis['$or'].push({ 'vpath': { '$eq': vpath } })
} }
@ -74,28 +79,53 @@ exports.setup = (mstream) => {
}); });
mstream.post('/api/v1/db/metadata', (req, res) => { mstream.post('/api/v1/db/metadata', (req, res) => {
const pathInfo = vpath.getVPathInfo(req.body.filepath, req.user); res.json(this.pullMetaData(req.body.filepath, req.user));
if (!db.getFileCollection()) { return res.json({ "filepath": req.body.filepath, "metadata": {} }); } });
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) => { 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) const result = db.getFileCollection().chain().find({ '$and': [{'filepath': pathInfo.relativePath}, {'vpath': pathInfo.vpath}] }, true)
.eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data(); .eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data();
if (!result || !result[0]) { 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": [] }; const artists = { "artists": [] };
if (!db.getFileCollection()) { res.json(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 = {}; const store = {};
for (let row of results) { for (let row of results) {
if (!store[row.artist] && !(row.artist === undefined || row.artist === null)) { if (!store[row.artist] && !(row.artist === undefined || row.artist === null)) {
@ -107,8 +137,8 @@ exports.setup = (mstream) => {
return a.localeCompare(b); return a.localeCompare(b);
}); });
res.json(artists); return artists;
}); }
mstream.post('/api/v1/db/artists-albums', (req, res) => { mstream.post('/api/v1/db/artists-albums', (req, res) => {
const albums = { "albums": [] }; const albums = { "albums": [] };
@ -116,7 +146,7 @@ exports.setup = (mstream) => {
const results = db.getFileCollection().chain().find({ const results = db.getFileCollection().chain().find({
'$and': [ '$and': [
renderOrClause(req.user.vpaths), renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
{'artist': { '$eq': String(req.body.artist) }} {'artist': { '$eq': String(req.body.artist) }}
] ]
}).simplesort('year', true).data(); }).simplesort('year', true).data();
@ -146,10 +176,20 @@ exports.setup = (mstream) => {
}); });
mstream.get('/api/v1/db/albums', (req, res) => { 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": [] }; const albums = { "albums": [] };
if (!db.getFileCollection()) { return res.json(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 = {}; const store = {};
for (let row of results) { for (let row of results) {
if (store[`${row.album}${row.year}`] || (row.album === undefined || row.album === null)) { 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); return a.name.localeCompare(b.name);
}); });
res.json(albums); return albums;
}); }
mstream.post('/api/v1/db/album-songs', (req, res) => { mstream.post('/api/v1/db/album-songs', (req, res) => {
if (!db.getFileCollection()) { throw new Error('DB Not Working'); } if (!db.getFileCollection()) { throw new Error('DB Not Working'); }
const searchClause = [ const searchClause = [
renderOrClause(req.user.vpaths), renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
{'album': { '$eq': req.body.album ? String(req.body.album) : null }} {'album': { '$eq': req.body.album ? String(req.body.album) : null }}
]; ];
@ -205,6 +245,7 @@ exports.setup = (mstream) => {
noAlbums: Joi.boolean().optional(), noAlbums: Joi.boolean().optional(),
noTitles: Joi.boolean().optional(), noTitles: Joi.boolean().optional(),
noFiles: Joi.boolean().optional(), noFiles: Joi.boolean().optional(),
ignoreVPaths: Joi.array().items(Joi.string()).optional()
}); });
joiValidate(schema, req.body); joiValidate(schema, req.body);
@ -226,7 +267,7 @@ exports.setup = (mstream) => {
const findThis = { const findThis = {
'$and': [ '$and': [
renderOrClause(req.user.vpaths), renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
{[searchCol]: {'$regex': [escapeStringRegexp(String(req.body.search)), 'i']}} {[searchCol]: {'$regex': [escapeStringRegexp(String(req.body.search)), 'i']}}
] ]
}; };
@ -259,6 +300,16 @@ exports.setup = (mstream) => {
} }
mstream.get('/api/v1/db/rated', (req, res) => { 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'); } if (!db.getFileCollection()) { throw new Error('DB Not Ready'); }
const mapFun = (left, right) => { const mapFun = (left, right) => {
@ -287,7 +338,7 @@ exports.setup = (mstream) => {
const results = db.getUserMetadataCollection().chain().eqJoin(db.getFileCollection().chain(), leftFun, rightFun, mapFun).find({ const results = db.getUserMetadataCollection().chain().eqJoin(db.getFileCollection().chain(), leftFun, rightFun, mapFun).find({
'$and': [ '$and': [
renderOrClause(req.user.vpaths), renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
{ 'rating': { '$gt': 0 } } { 'rating': { '$gt': 0 } }
] ]
}).simplesort('rating', true).data(); }).simplesort('rating', true).data();
@ -296,8 +347,9 @@ exports.setup = (mstream) => {
for (const row of results) { for (const row of results) {
songs.push(renderMetadataObj(row)); songs.push(renderMetadataObj(row));
} }
res.json(songs);
}); return songs;
}
mstream.post('/api/v1/db/rate-song', (req, res) => { mstream.post('/api/v1/db/rate-song', (req, res) => {
const schema = Joi.object({ const schema = Joi.object({
@ -329,7 +381,10 @@ exports.setup = (mstream) => {
}); });
mstream.post('/api/v1/db/recent/added', (req, res) => { 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); joiValidate(schema, req.body);
if (!db.getFileCollection()) { throw new Error('DB Not Ready'); } if (!db.getFileCollection()) { throw new Error('DB Not Ready'); }
@ -340,7 +395,7 @@ exports.setup = (mstream) => {
const results = db.getFileCollection().chain().find({ const results = db.getFileCollection().chain().find({
'$and': [ '$and': [
renderOrClause(req.user.vpaths), renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
{ 'ts': { '$gt': 0 } } { 'ts': { '$gt': 0 } }
] ]
}).simplesort('ts', true).limit(req.body.limit).eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data(); }).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); 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) => { mstream.post('/api/v1/db/random-songs', (req, res) => {
if (!db.getFileDbName()) { throw new Error('No DB'); }; if (!db.getFileDbName()) { throw new Error('No DB'); };
@ -369,7 +524,7 @@ exports.setup = (mstream) => {
let orClause = { '$or': [] }; let orClause = { '$or': [] };
for (let vpath of req.user.vpaths) { 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; continue;
} }
orClause['$or'].push({ 'vpath': { '$eq': vpath } }); orClause['$or'].push({ 'vpath': { '$eq': vpath } });
@ -458,4 +613,8 @@ exports.setup = (mstream) => {
res.json(returnThis); res.json(returnThis);
}); });
// mstream.post('/api/v1/db/song-position', (req, res) => {
// });
} }

View File

@ -1,7 +1,7 @@
const path = require('path'); const path = require('path');
const fs = require('fs').promises; const fs = require('fs').promises;
const fsOld = require('fs'); const fsOld = require('fs');
const Busboy = require("busboy"); const busboy = require("busboy");
const Joi = require('joi'); const Joi = require('joi');
const mkdirp = require('make-dir'); const mkdirp = require('make-dir');
const winston = require('winston'); const winston = require('winston');
@ -16,7 +16,8 @@ exports.setup = (mstream) => {
mstream.post("/api/v1/file-explorer", async (req, res) => { mstream.post("/api/v1/file-explorer", async (req, res) => {
const schema = Joi.object({ const schema = Joi.object({
directory: Joi.string().allow("").required(), 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); const { value } = joiValidate(schema, req.body);
@ -48,7 +49,7 @@ exports.setup = (mstream) => {
} }
// get directory contents // 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 // Format directory string for return value
let returnDirectory = path.join(pathInfo.vpath, pathInfo.relativePath); 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); const pathInfo = vpath.getVPathInfo(decodeURI(req.headers['data-location']), req.user);
mkdirp.sync(pathInfo.fullPath); mkdirp.sync(pathInfo.fullPath);
const busboy = new Busboy({ headers: req.headers }); const bb = busboy({ headers: req.headers });
busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { bb.on('file', (fieldname, file, info) => {
const { filename } = info;
const saveTo = path.join(pathInfo.fullPath, filename); const saveTo = path.join(pathInfo.fullPath, filename);
winston.info(`Uploading from ${req.user.username} to: ${saveTo}`); winston.info(`Uploading from ${req.user.username} to: ${saveTo}`);
file.pipe(fsOld.createWriteStream(saveTo)); file.pipe(fsOld.createWriteStream(saveTo));
}); });
busboy.on('finish', () => { res.json({}); }); bb.on('close', () => { res.json({}); });
req.pipe(busboy); req.pipe(bb);
}); });
mstream.post("/api/v1/file-explorer/m3u", async (req, res) => { mstream.post("/api/v1/file-explorer/m3u", async (req, res) => {

View File

@ -15,11 +15,22 @@ exports.setup = (mstream) => {
} }
} }
res.json({ const returnThis = {
vpaths: req.user.vpaths, vpaths: req.user.vpaths,
playlists: getPlaylists(req.user.username), 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) => { mstream.post('/api/v1/playlist/delete', (req, res) => {
@ -91,7 +102,8 @@ exports.setup = (mstream) => {
db.getPlaylistCollection().insert({ db.getPlaylistCollection().insert({
name: req.body.title, name: req.body.title,
filepath: null, filepath: null,
user: req.user.username user: req.user.username,
live: false
}); });
db.saveUserDB(); db.saveUserDB();
@ -101,7 +113,8 @@ exports.setup = (mstream) => {
mstream.post('/api/v1/playlist/save', (req, res) => { mstream.post('/api/v1/playlist/save', (req, res) => {
const schema = Joi.object({ const schema = Joi.object({
title: Joi.string().required(), title: Joi.string().required(),
songs: Joi.array().items(Joi.string()) songs: Joi.array().items(Joi.string()),
live: Joi.boolean().optional()
}); });
joiValidate(schema, req.body); joiValidate(schema, req.body);
@ -125,8 +138,10 @@ exports.setup = (mstream) => {
db.getPlaylistCollection().insert({ db.getPlaylistCollection().insert({
name: req.body.title, name: req.body.title,
filepath: null, filepath: null,
user: req.user.username user: req.user.username,
live: typeof req.body.live === 'boolean' ? req.body.live : false
}); });
db.saveUserDB(); db.saveUserDB();
res.json({}); res.json({});
@ -139,13 +154,9 @@ exports.setup = (mstream) => {
function getPlaylists(username) { function getPlaylists(username) {
const playlists = []; const playlists = [];
const results = db.getPlaylistCollection().find({ 'user': { '$eq': username } }); const results = db.getPlaylistCollection().find({ 'user': { '$eq': username }, 'filepath': { '$eq': null } });
const store = {};
for (let row of results) { for (let row of results) {
if (!store[row.name]) { playlists.push({ name: row.name });
playlists.push({ name: row.name });
store[row.name] = true;
}
} }
return playlists; return playlists;
} }

View File

@ -24,7 +24,7 @@ exports.setup = (mstream) => {
else if (req.body.modTime !== dbFileInfo.modified) { else if (req.body.modTime !== dbFileInfo.modified) {
db.getFileCollection().findAndRemove(dbObj); db.getFileCollection().findAndRemove(dbObj);
return res.json({}); return res.json({});
} }
// update the record with the new scan ID // 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 // This lets us clear out old files wit ha bulk delete at the end of the scan
else { else {

View File

@ -3,7 +3,9 @@ const Joi = require('joi');
const axios = require('axios'); const axios = require('axios');
const config = require('../state/config'); const config = require('../state/config');
const scribble = require('../state/lastfm'); const scribble = require('../state/lastfm');
const db = require('../db/manager');
const { joiValidate } = require('../util/validation'); const { joiValidate } = require('../util/validation');
const { getVPathInfo } = require('../util/vpath');
const Scrobbler = new scribble(); 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) => { mstream.post('/api/v1/lastfm/test-login', async (req, res) => {
const schema = Joi.object({ const schema = Joi.object({
username: Joi.string().required(), username: Joi.string().required(),

View File

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

View File

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

View File

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

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

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

View File

@ -19,10 +19,6 @@ function addScanTask(vpath) {
} }
} }
function removeTask(taskId) {
}
function scanAll() { function scanAll() {
Object.keys(config.program.folders).forEach((vpath) => { Object.keys(config.program.folders).forEach((vpath) => {
addScanTask(vpath); addScanTask(vpath);
@ -50,10 +46,13 @@ function runScan(scanObj) {
pause: config.program.scanOptions.pause, pause: config.program.scanOptions.pause,
supportedFiles: config.program.supportedAudioFiles, supportedFiles: config.program.supportedAudioFiles,
scanId: scanObj.id, 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}`); winston.info(`File scan started on ${jsonLoad.directory}`);
runningTasks.add(forkedScan); runningTasks.add(forkedScan);
vpathLimiter.add(scanObj.vpath); vpathLimiter.add(scanObj.vpath);

View File

@ -1,6 +1,7 @@
const winston = require('winston'); const winston = require('winston');
const express = require('express'); const express = require('express');
const fs = require('fs'); const fs = require('fs');
const path = require('path');
const Joi = require('joi'); const Joi = require('joi');
const cookieParser = require('cookie-parser'); const cookieParser = require('cookie-parser');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
@ -63,7 +64,7 @@ exports.serveIt = async configFile => {
// Magic Middleware Things // Magic Middleware Things
mstream.use(cookieParser()); mstream.use(cookieParser());
mstream.use(express.json()); mstream.use(express.json({ limit: config.program.maxRequestSize }));
mstream.use(express.urlencoded({ extended: true })); mstream.use(express.urlencoded({ extended: true }));
mstream.use((req, res, next) => { // CORS mstream.use((req, res, next) => { // CORS
res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Origin", "*");
@ -82,7 +83,7 @@ exports.serveIt = async configFile => {
const matchEnd = req.path.match(/(\/)+$/g); const matchEnd = req.path.match(/(\/)+$/g);
const queryString = req.url.match(/(\?.*)/g) === null ? '' : req.url.match(/(\?.*)/g); const queryString = req.url.match(/(\?.*)/g) === null ? '' : req.url.match(/(\?.*)/g);
// redirect to a more sane URL // 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(); next();
}); });
@ -90,7 +91,16 @@ exports.serveIt = async configFile => {
// Block access to admin page if necessary // Block access to admin page if necessary
mstream.get('/admin', (req, res, next) => { mstream.get('/admin', (req, res, next) => {
if (config.program.lockAdmin === true) { return res.send('<p>Admin Page Disabled</p>'); } 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) => { 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); jwt.verify(req.cookies['x-access-token'], config.program.secret);
next(); next();
} catch(err) { } catch(err) {
return res.redirect(301, 'login'); return res.redirect(302, '/login');
} }
}); });
mstream.get('/login', (req, res, next) => { mstream.get('/login', (req, res, next) => {
if (Object.keys(config.program.users).length === 0){ if (Object.keys(config.program.users).length === 0){
return res.redirect(301, '..'); return res.redirect(302, '..');
} }
try { try {
jwt.verify(req.cookies['x-access-token'], config.program.secret); jwt.verify(req.cookies['x-access-token'], config.program.secret);
return res.redirect(301, '..'); return res.redirect(302, '..');
} catch(err) { } catch(err) {
next(); next();
} }
@ -151,7 +161,15 @@ exports.serveIt = async configFile => {
mstream.get('/api/', (req, res) => res.json({ "server": require('../package.json').version, "apiVersions": ["1"] })); mstream.get('/api/', (req, res) => res.json({ "server": require('../package.json').version, "apiVersions": ["1"] }));
// album art folder // 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 // TODO: determine if user has access to the exact file
// mstream.all('/media/*', (req, res, next) => { // mstream.all('/media/*', (req, res, next) => {

View File

@ -17,7 +17,9 @@ const scanOptions = Joi.object({
saveInterval: Joi.number().default(250), saveInterval: Joi.number().default(250),
pause: Joi.number().min(0).default(0), pause: Joi.number().min(0).default(0),
bootScanDelay: Joi.number().default(3), 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({ const dbOptions = Joi.object({
@ -25,7 +27,7 @@ const dbOptions = Joi.object({
}); });
const transcodeOptions = 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), enabled: Joi.boolean().default(false),
ffmpegDirectory: Joi.string().default(path.join(__dirname, '../../bin/ffmpeg')), ffmpegDirectory: Joi.string().default(path.join(__dirname, '../../bin/ffmpeg')),
defaultCodec: Joi.string().valid(...getTransCodecs()).default('opus'), defaultCodec: Joi.string().valid(...getTransCodecs()).default('opus'),
@ -53,7 +55,7 @@ const federationOptions = Joi.object({
}); });
const schema = 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), port: Joi.number().default(3000),
supportedAudioFiles: Joi.object().pattern( supportedAudioFiles: Joi.object().pattern(
Joi.string(), Joi.boolean() Joi.string(), Joi.boolean()
@ -72,12 +74,13 @@ const schema = Joi.object({
rpn: rpnOptions.default(rpnOptions.validate({}).value), rpn: rpnOptions.default(rpnOptions.validate({}).value),
transcode: transcodeOptions.default(transcodeOptions.validate({}).value), transcode: transcodeOptions.default(transcodeOptions.validate({}).value),
secret: Joi.string().optional(), secret: Joi.string().optional(),
maxRequestSize: Joi.string().pattern(/[0-9]+(KB|MB)/i).default('1MB'),
db: dbOptions.default(dbOptions.validate({}).value), db: dbOptions.default(dbOptions.validate({}).value),
folders: Joi.object().pattern( folders: Joi.object().pattern(
Joi.string(), Joi.string(),
Joi.object({ Joi.object({
root: Joi.string().required(), root: Joi.string().required(),
type: Joi.string().valid('music', 'audiobook').default('music'), type: Joi.string().valid('music', 'audio-books').default('music'),
}) })
).default({}), ).default({}),
users: Joi.object().pattern( users: Joi.object().pattern(

View File

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

View File

@ -18,7 +18,7 @@ exports.saveFile = async (saveData, file) => {
return await fs.writeFile(file, JSON.stringify(saveData, null, 2), 'utf8') 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 // confirm directory is real
const stat = await fs.stat(directory); const stat = await fs.stat(directory);
if (!stat.isDirectory()) { throw `${directory} is not a 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 // Once the file save is complete, the new user will be added
const memClone = JSON.parse(JSON.stringify(config.program.folders)); const memClone = JSON.parse(JSON.stringify(config.program.folders));
memClone[vpath] = { root: directory }; memClone[vpath] = { root: directory };
if (isAudioBooks) { memClone[vpath].type = 'audio-books'; }
// add directory to config file // add directory to config file
const loadConfig = await this.loadFile(config.configFile); const loadConfig = await this.loadFile(config.configFile);
@ -44,7 +45,7 @@ exports.addDirectory = async (directory, vpath, autoAccess, mstream) => {
await this.saveFile(loadConfig, config.configFile); await this.saveFile(loadConfig, config.configFile);
// add directory to program // add directory to program
config.program.folders[vpath] = { root: directory }; config.program.folders[vpath] = memClone[vpath];
if (autoAccess === true) { if (autoAccess === true) {
Object.values(config.program.users).forEach(user => { Object.values(config.program.users).forEach(user => {
@ -187,6 +188,17 @@ exports.editPort = async (port) => {
mStreamServer.reboot(); 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) => { exports.editUpload = async (val) => {
const loadConfig = await this.loadFile(config.configFile); const loadConfig = await this.loadFile(config.configFile);
loadConfig.noUpload = val; loadConfig.noUpload = val;
@ -242,6 +254,15 @@ exports.editSkipImg = async (val) => {
config.program.scanOptions.skipImg = 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) => { exports.editPause = async (val) => {
const loadConfig = await this.loadFile(config.configFile); const loadConfig = await this.loadFile(config.configFile);
if (!loadConfig.scanOptions) { loadConfig.scanOptions = {}; } if (!loadConfig.scanOptions) { loadConfig.scanOptions = {}; }
@ -269,6 +290,15 @@ exports.editMaxConcurrentTasks = async (val) => {
config.program.scanOptions.maxConcurrentTasks = 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) => { exports.editWriteLogs = async (val) => {
const loadConfig = await this.loadFile(config.configFile); const loadConfig = await this.loadFile(config.configFile);
loadConfig.writeLogs = val; loadConfig.writeLogs = val;
@ -328,12 +358,14 @@ exports.lockAdminApi = async (val) => {
} }
exports.enableFederation = 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); const loadConfig = await this.loadFile(config.configFile);
loadConfig.federation.enabled = val; loadConfig.federation = memClone;
await this.saveFile(loadConfig, config.configFile); await this.saveFile(loadConfig, config.configFile);
config.program.federation.enabled = val; config.program.federation.enabled = val;
syncthing.setup(); syncthing.setup();
} }
@ -358,12 +390,12 @@ function testSSL(jsonLoad) {
} }
exports.setSSL = async (cert, key) => { exports.setSSL = async (cert, key) => {
const sslObj = { key, cert }; const sslObj = { key, cert };
await testSSL(sslObj); await testSSL(sslObj);
const loadConfig = await this.loadFile(config.configFile); const loadConfig = await this.loadFile(config.configFile);
loadConfig.ssl = sslObj; loadConfig.ssl = sslObj;
await this.saveFile(loadConfig, config.configFile); await this.saveFile(loadConfig, config.configFile);
config.program.ssl = sslObj; config.program.ssl = sslObj;
mStreamServer.reboot(); mStreamServer.reboot();
} }

View File

@ -23,7 +23,8 @@ function isAsync(fn) {
function wrapAsync(fn) { function wrapAsync(fn) {
return (req, res, next = noop) => { return (req, res, next = noop) => {
fn(req, res, next) fn(req, res, next)
.then(() => !res.finished && next()) .catch((err) => {
.catch(next); next(err);
});
} }
}; };

View File

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

View File

@ -39,7 +39,6 @@
<link rel="stylesheet" href="index.css"> <link rel="stylesheet" href="index.css">
<script src="../assets/js/api.js"></script> <script src="../assets/js/api.js"></script>
<script>API.checkAuthAndKickToLogin();</script>
</head> </head>
<body> <body>

View File

@ -262,6 +262,10 @@ const foldersView = Vue.component('folders-view', {
<input id="folder-auto-access" type="checkbox" checked/> <input id="folder-auto-access" type="checkbox" checked/>
<span>Give Access To All Users</span> <span>Give Access To All Users</span>
</label></div> </label></div>
<div class="pad-checkbox"><label>
<input id="folder-is-audiobooks" type="checkbox"/>
<span>Audiobooks & Podcasts</span>
</label></div>
</div> </div>
<button class="btn green waves-effect waves-light col m6 s12" type="submit" :disabled="submitPending === true"> <button class="btn green waves-effect waves-light col m6 s12" type="submit" :disabled="submitPending === true">
{{submitPending === false ? 'Add Folder' : 'Adding...'}} {{submitPending === false ? 'Add Folder' : 'Adding...'}}
@ -345,7 +349,8 @@ const foldersView = Vue.component('folders-view', {
data: { data: {
directory: this.folder.value, directory: this.folder.value,
vpath: this.dirName, 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"> <form id="add-user-form" @submit.prevent="addUser">
<div class="row"> <div class="row">
<div class="input-field directory-name-field col s12 m6"> <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> <label for="new-username">Username</label>
</div> </div>
<div class="input-field directory-name-field col s12 m6"> <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', title: 'You will be taken the login page',
position: 'center', position: 'center',
buttons: [['<button>Go!</button>', (instance, toast) => { buttons: [['<button>Go!</button>', (instance, toast) => {
API.checkAuthAndKickToLogin(); API.logout();
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button'); instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
}, true]], }, true]],
}); });
@ -696,6 +701,12 @@ const advancedView = Vue.component('advanced-view', {
[<a v-on:click="openModal('edit-port-modal')">edit</a>] [<a v-on:click="openModal('edit-port-modal')">edit</a>]
</td> </td>
</tr> </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> <tr>
<td><b>Address:</b> {{params.address}}</td> <td><b>Address:</b> {{params.address}}</td>
<td> <td>
@ -817,7 +828,7 @@ const advancedView = Vue.component('advanced-view', {
url: `${API.url()}/api/v1/admin/config/secret`, url: `${API.url()}/api/v1/admin/config/secret`,
data: { strength: 128 } data: { strength: 128 }
}).then(() => { }).then(() => {
API.checkAuthAndKickToLogin(); API.logout();
}).catch(() => { }).catch(() => {
iziToast.error({ iziToast.error({
title: 'Failed', title: 'Failed',
@ -930,6 +941,19 @@ const dbView = Vue.component('db-view', {
[<a v-on:click="toggleSkipImg()">edit</a>] [<a v-on:click="toggleSkipImg()">edit</a>]
</td> </td>
</tr> </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> <tr>
<td><b>Max Concurrent Scans:</b> {{dbParams.maxConcurrentTasks}}</td> <td><b>Max Concurrent Scans:</b> {{dbParams.maxConcurrentTasks}}</td>
<td> <td>
@ -987,7 +1011,7 @@ const dbView = Vue.component('db-view', {
<tr v-for="(v, k) in sharedPlaylists"> <tr v-for="(v, k) in sharedPlaylists">
<th><a target="_blank" v-bind:href="'/shared/'+ v.playlistId">{{v.playlistId}}</a></th> <th><a target="_blank" v-bind:href="'/shared/'+ v.playlistId">{{v.playlistId}}</a></th>
<th>{{v.user}}</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> <th>[<a v-on:click="deletePlaylist(v)">delete</a>]</th>
</tr> </tr>
</tbody> </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() { toggleSkipImg: function() {
iziToast.question({ iziToast.question({
timeout: 20000, 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', { const editPortModal = Vue.component('edit-port-modal', {
data() { data() {
return { return {
@ -3214,6 +3444,7 @@ const modVM = new Vue({
'user-access-modal': userAccessView, 'user-access-modal': userAccessView,
'file-explorer-modal': fileExplorerModal, 'file-explorer-modal': fileExplorerModal,
'edit-port-modal': editPortModal, 'edit-port-modal': editPortModal,
'edit-request-size-modal': editRequestSizeModal,
'edit-address-modal': editAddressModal, 'edit-address-modal': editAddressModal,
'edit-scan-interval-modal': editScanIntervalView, 'edit-scan-interval-modal': editScanIntervalView,
'edit-save-interval-modal': editSaveIntervalView, 'edit-save-interval-modal': editSaveIntervalView,

View File

@ -39,8 +39,12 @@ const MSTREAMAPI = (() => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/file-explorer/recursive', { directory: directory }); return req('POST', mstreamModule.currentServer.host + 'api/v1/file-explorer/recursive', { directory: directory });
} }
mstreamModule.savePlaylist = (title, songs) => { mstreamModule.savePlaylist = (title, songs, live) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/playlist/save', { title: title, songs: songs }); 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) => { mstreamModule.newPlaylist = (title) => {
@ -71,20 +75,20 @@ const MSTREAMAPI = (() => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/db/search', postObject); return req('POST', mstreamModule.currentServer.host + 'api/v1/db/search', postObject);
} }
mstreamModule.artists = () => { mstreamModule.artists = (postObject) => {
return req('GET', mstreamModule.currentServer.host + 'api/v1/db/artists', false); return req('POST', mstreamModule.currentServer.host + 'api/v1/db/artists', postObject);
} }
mstreamModule.albums = () => { mstreamModule.albums = (postObject) => {
return req('GET', mstreamModule.currentServer.host + 'api/v1/db/albums', false); return req('POST', mstreamModule.currentServer.host + 'api/v1/db/albums', postObject);
} }
mstreamModule.artistAlbums = (artist) => { mstreamModule.artistAlbums = (postObject) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/artists-albums", { artist: artist }); return req('POST', mstreamModule.currentServer.host + "api/v1/db/artists-albums", postObject);
} }
mstreamModule.albumSongs = (album, artist, year) => { mstreamModule.albumSongs = (postObject) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/album-songs", { album, artist, year }); return req('POST', mstreamModule.currentServer.host + "api/v1/db/album-songs", postObject);
} }
mstreamModule.dbStatus = () => { mstreamModule.dbStatus = () => {
@ -99,12 +103,20 @@ const MSTREAMAPI = (() => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/rate-song", { filepath: filepath, rating: rating }); return req('POST', mstreamModule.currentServer.host + "api/v1/db/rate-song", { filepath: filepath, rating: rating });
} }
mstreamModule.getRated = () => { mstreamModule.getRated = (postObject) => {
return req('GET', mstreamModule.currentServer.host + "api/v1/db/rated", false); return req('POST', mstreamModule.currentServer.host + "api/v1/db/rated", postObject);
} }
mstreamModule.getRecentlyAdded = (limit) => { mstreamModule.getRecentlyAdded = (limit, ignoreVPaths) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/recent/added", { limit: limit }); 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) => { 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 }); 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 // LOGIN
mstreamModule.login = (username, password, url) => { mstreamModule.login = (username, password, url) => {
return req('POST', url ? url + "api/v1/auth/login" : "api/v1/auth/login", { username: username, password: password }); 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 = () => { mstreamModule.logout = () => {
localStorage.removeItem('token'); localStorage.removeItem('token');
Cookies.remove('x-access-token'); Cookies.remove('x-access-token');
window.location.href = './login'; document.location.assign(window.location.href + (window.location.href.slice(-1) === '/' ? '' : '/') + 'login');
} }
return mstreamModule; return mstreamModule;

View File

@ -90,13 +90,6 @@ myDropzone.on('error', (err, msg, xhr) => {
iziToast.error(iziStuff); 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 ////////////////////////////// Global Variables
// These vars track your position within the file explorer // These vars track your position within the file explorer
var fileExplorerArray = []; var fileExplorerArray = [];
@ -128,7 +121,7 @@ function renderAlbum(id, artist, name, albumArtFile, year) {
return `<li class="collection-item"> return `<li class="collection-item">
<div ${year ? `data-year="${year}"` : '' } ${artist ? `data-artist="${artist}"` : '' } ${id ? `data-album="${id}"` : '' } class="albumz flex2" onclick="getAlbumsOnClick(this);"> <div ${year ? `data-year="${year}"` : '' } ${artist ? `data-artist="${artist}"` : '' } ${id ? `data-album="${id}"` : '' } class="albumz flex2" onclick="getAlbumsOnClick(this);">
${albumArtFile ? ${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>'} '<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> <span><b>${name}</b> ${year ? `<br>[${year}]` : ''}</span>
</div> </div>
@ -144,7 +137,7 @@ function renderArtist(artist) {
function renderFileWithMetadataHtml(filepath, lokiId, metadata) { function renderFileWithMetadataHtml(filepath, lokiId, metadata) {
return `<li data-lokiid="${lokiId}" class="collection-item"> return `<li data-lokiid="${lokiId}" class="collection-item">
<div data-file_location="${filepath}" class="filez flex" onclick="onFileClick(this);"> <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> <div>
<b><span>${(!metadata || !metadata.title) ? filepath.split("/").pop() : `${metadata.title}`}</span></b> <b><span>${(!metadata || !metadata.title) ? filepath.split("/").pop() : `${metadata.title}`}</span></b>
${metadata.artist ? `</b><br><span style="font-size:15px;">${metadata.artist}</span>` : ''} ${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);"> <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>'} ${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> <span>
${subtitle ? `<b>` : ''} ${subtitle !== undefined ? `<b>` : ''}
<span class="${aa ? '' : 'item-text'}">${rating ? `[${rating}] ` : ''}${title}</span> <span class="${aa ? '' : 'item-text'}">${rating ? `[${rating}] ` : ''}${title}</span>
${subtitle ? `</b><br><span>${subtitle}</span>` : ''} ${subtitle !== undefined ? `</b><br><span>${subtitle}</span>` : ''}
</span> </span>
</div> </div>
<div class="song-button-box"> <div class="song-button-box">
@ -430,6 +423,7 @@ async function init() {
response.playlists.forEach(p => { response.playlists.forEach(p => {
VUEPLAYERCORE.playlists.push(p); VUEPLAYERCORE.playlists.push(p);
document.getElementById('pop-f').innerHTML += `<div class="pop-list-item" onclick="addToPlaylistUI('${p.name}')">&#8226; ${p.name}</div>`; document.getElementById('pop-f').innerHTML += `<div class="pop-list-item" onclick="addToPlaylistUI('${p.name}')">&#8226; ${p.name}</div>`;
document.getElementById('live-playlist-select').innerHTML += `<option value="${p.name}">${p.name}</option>`;
}); });
if (response.transcode) { if (response.transcode) {
@ -463,6 +457,27 @@ async function init() {
MSTREAMPLAYER.transcodeOptions.selectedAlgo = localStorage.getItem('trans-algo-select'); MSTREAMPLAYER.transcodeOptions.selectedAlgo = localStorage.getItem('trans-algo-select');
} catch (e) {} } 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(); dbStatus();
} }
@ -537,6 +552,10 @@ function openSaveModal() {
myModal.open('#savePlaylist'); myModal.open('#savePlaylist');
} }
function openLivePlaylistModal() {
myModal.open('#livePlaylist');
}
function openNewPlaylistModal() { function openNewPlaylistModal() {
myModal.open('#newPlaylist'); myModal.open('#newPlaylist');
} }
@ -545,7 +564,38 @@ function openPlaybackModal() {
myModal.open('#speedModal'); 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() { function openEditModal() {
document.getElementById('server_address').value = MSTREAMAPI.currentServer.host;
document.getElementById('server_username').value = MSTREAMAPI.currentServer.username;
myModal.open('#editServer'); myModal.open('#editServer');
} }
@ -624,42 +674,6 @@ function downloadFileplaylist(el) {
document.getElementById('downform').innerHTML = ''; 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() { function onSearchButtonClick() {
// Hide Filepath // Hide Filepath
document.getElementById('search_folders').classList.toggle('super-hide'); document.getElementById('search_folders').classList.toggle('super-hide');
@ -720,6 +734,7 @@ async function getAllPlaylists() {
const response = await MSTREAMAPI.getAllPlaylists(); const response = await MSTREAMAPI.getAllPlaylists();
VUEPLAYERCORE.playlists.length = 0; VUEPLAYERCORE.playlists.length = 0;
document.getElementById('pop-f').innerHTML = '<div class="pop-f pop-playlist">Add To Playlist:</div>'; 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 // loop through the json array and make an array of corresponding divs
let playlists = '<ul class="collection">'; let playlists = '<ul class="collection">';
@ -729,6 +744,7 @@ async function getAllPlaylists() {
currentBrowsingList.push(lol); currentBrowsingList.push(lol);
VUEPLAYERCORE.playlists.push(lol); VUEPLAYERCORE.playlists.push(lol);
document.getElementById('pop-f').innerHTML += `<div class="pop-list-item" onclick="addToPlaylistUI('${p.name}')">&#8226; ${p.name}</div>`; document.getElementById('pop-f').innerHTML += `<div class="pop-list-item" onclick="addToPlaylistUI('${p.name}')">&#8226; ${p.name}</div>`;
document.getElementById('live-playlist-select').innerHTML += `<option value="${p.name}">${p.name}</option>`;
}); });
playlists += '</ul>' playlists += '</ul>'
@ -838,6 +854,7 @@ async function newPlaylist() {
document.getElementById("newPlaylistForm").reset(); document.getElementById("newPlaylistForm").reset();
VUEPLAYERCORE.playlists.push({ name: title, type: 'playlist'}); VUEPLAYERCORE.playlists.push({ name: title, type: 'playlist'});
document.getElementById('pop-f').innerHTML += `<div class="pop-list-item" onclick="addToPlaylistUI('${title}')">&#8226; ${title}</div>`; document.getElementById('pop-f').innerHTML += `<div class="pop-list-item" onclick="addToPlaylistUI('${title}')">&#8226; ${title}</div>`;
document.getElementById('live-playlist-select').innerHTML += `<option value="${title}">${title}</option>`;
if (programState[0].state === 'allPlaylists') { if (programState[0].state === 'allPlaylists') {
getAllPlaylists(); getAllPlaylists();
@ -848,6 +865,80 @@ async function newPlaylist() {
document.getElementById('new_playlist').disabled = false; 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() { async function savePlaylist() {
if (MSTREAMPLAYER.playlist.length == 0) { if (MSTREAMPLAYER.playlist.length == 0) {
iziToast.warning({ iziToast.warning({
@ -883,8 +974,11 @@ async function savePlaylist() {
VUEPLAYERCORE.playlists.push({ name: title, type: 'playlist'}); VUEPLAYERCORE.playlists.push({ name: title, type: 'playlist'});
document.getElementById('pop-f').innerHTML += `<div class="pop-list-item" onclick="addToPlaylistUI('${title}')">&#8226; ${title}</div>`; document.getElementById('pop-f').innerHTML += `<div class="pop-list-item" onclick="addToPlaylistUI('${title}')">&#8226; ${title}</div>`;
document.getElementById('live-playlist-select').innerHTML += `<option value="${title}">${title}</option>`;
}catch(err) { }catch(err) {
boilerplateFailure(err); boilerplateFailure(err);
} finally {
document.getElementById('save_playlist').disabled = false;
} }
} }
@ -895,7 +989,12 @@ async function getAllArtists() {
programState = [{ state: 'allArtists' }]; programState = [{ state: 'allArtists' }];
try { 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 // parse through the json array and make an array of corresponding divs
let artists = '<ul class="collection">'; let artists = '<ul class="collection">';
response.artists.forEach(value => { response.artists.forEach(value => {
@ -929,7 +1028,13 @@ async function getArtistsAlbums(artist) {
document.getElementById('filelist').innerHTML = getLoadingSvg(); document.getElementById('filelist').innerHTML = getLoadingSvg();
try { 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>'; let albums = '<ul>';
response.albums.forEach(value => { response.albums.forEach(value => {
const albumString = value.name ? value.name : 'SINGLES'; const albumString = value.name ? value.name : 'SINGLES';
@ -956,7 +1061,12 @@ async function getAllAlbums() {
programState = [{ state: 'allAlbums' }]; programState = [{ state: 'allAlbums' }];
try { 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 //parse through the json array and make an array of corresponding divs
let albums = '<ul class="collection">'; let albums = '<ul class="collection">';
response.albums.forEach(value => { response.albums.forEach(value => {
@ -1001,7 +1111,15 @@ async function getAlbumSongs(album, artist, year) {
document.getElementById('localSearchBar').value = ''; document.getElementById('localSearchBar').value = '';
try { 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 //parse through the json array and make an array of corresponding divs
let files = '<ul class="collection">'; let files = '<ul class="collection">';
response.forEach(song => { response.forEach(song => {
@ -1024,7 +1142,11 @@ async function getRatedSongs() {
programState = [{ state: 'allRated' }]; programState = [{ state: 'allRated' }];
try { 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 //parse through the json array and make an array of corresponding divs
let files = ''; let files = '';
response.forEach(value => { response.forEach(value => {
@ -1040,10 +1162,10 @@ async function getRatedSongs() {
}); });
files += createMusicFileHtml(value.filepath, 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'] ? `src="${MSTREAMAPI.currentServer.host}album-art/${value.metadata['album-art']}?token=${MSTREAMAPI.currentServer.token}"` : `src="assets/img/default.png"`, 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, 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; 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 &nbsp;&nbsp;<input onkeydown="submitRecentlyPlayed();" onfocusout="redoRecentlyPlayed();" id="recently-played-limit" class="recently-added-input" type="number" min="1" step="1" value="100">&nbsp;&nbsp; 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 &nbsp;&nbsp;<input onkeydown="submitMostPlayed();" onfocusout="redoMostPlayed();" id="most-played-limit" class="recently-added-input" type="number" min="1" step="1" value="100">&nbsp;&nbsp; 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 ///////////////// Recently Added
function getRecentlyAdded() { function getRecentlyAdded() {
setBrowserRootPanel('Recently Added'); setBrowserRootPanel('Recently Added');
@ -1067,7 +1289,12 @@ async function redoRecentlyAdded() {
programState = [{ state: 'recentlyAdded'}]; programState = [{ state: 'recentlyAdded'}];
try { 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 //parse through the json array and make an array of corresponding divs
let filelist = '<ul class="collection">'; let filelist = '<ul class="collection">';
response.forEach(el => { response.forEach(el => {
@ -1078,9 +1305,9 @@ async function redoRecentlyAdded() {
filelist += createMusicFileHtml(el.filepath, filelist += createMusicFileHtml(el.filepath,
el.metadata.title ? `${el.metadata.title}`: el.filepath.split("/").pop(), 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, 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>' 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"> <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'/> <img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png'/>
</a> </a>
<div class="mobile-placeholder">&nbsp;</div>
</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"> <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> <a target="_blank" href="/qr"><b>Checkout the QR Code tool to help add your server to the app</b></a>
</div>`; </div>`;
@ -1314,7 +1546,14 @@ function createJukeboxPanel() {
return '<div class="pad-6">An error occurred. Please refresh the page and try again</div>'; 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"> return `<div class="autoselect pad-6">
<h4>Code: ${JUKEBOX.stats.adminCode}</h4> <h4>Code: ${JUKEBOX.stats.adminCode}</h4>
<h4><a target="_blank" href="${address}">${address}</a><h4> <h4><a target="_blank" href="${address}">${address}</a><h4>
@ -1455,7 +1694,13 @@ async function submitSearchForm() {
try { 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>' 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; } if (document.getElementById("search-in-artists") && document.getElementById("search-in-artists").checked === false) { postObject.noArtists = true; }
searchToggles.artists = document.getElementById("search-in-artists").checked; searchToggles.artists = document.getElementById("search-in-artists").checked;
if (document.getElementById("search-in-albums") && document.getElementById("search-in-albums").checked === false) { postObject.noAlbums = true; } 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 ////////////////// Layout
function setupLayoutPanel() { function setupLayoutPanel() {
setBrowserRootPanel('Layout', false); setBrowserRootPanel('Layout', false);
@ -1583,15 +1853,27 @@ async function updateServer() {
try { try {
document.getElementById('save_server').disabled = true; 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, const res = await MSTREAMAPI.login(document.getElementById('server_username').value,
document.getElementById('server_password').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.username = document.getElementById('server_username').value;
MSTREAMAPI.currentServer.token = res.token; MSTREAMAPI.currentServer.token = res.token;
myModal.close();
init();
loadFileExplorer();
localStorage.setItem('current-server', JSON.stringify(MSTREAMAPI.currentServer));
document.getElementById('server_password').value = '';
}catch(err) { }catch(err) {
console.log(err)
boilerplateFailure(err); boilerplateFailure(err);
}finally { }finally {
document.getElementById('save_server').disabled = false; document.getElementById('save_server').disabled = false;
@ -1630,6 +1912,16 @@ function initElectron() {
<span>Edit Server</span> <span>Edit Server</span>
</div>`; </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 // check if server
if (!MSTREAMAPI.currentServer.host) { if (!MSTREAMAPI.currentServer.host) {
openEditModal(); openEditModal();

View File

@ -997,4 +997,12 @@ select {
.mobile-links img { .mobile-links img {
width: 100%; width: 100%;
}
.no-margin p {
margin: 0;
}
.make-white {
color: #FFF;
} }

View File

@ -1,6 +1,10 @@
const VUEPLAYERCORE = (() => { const VUEPLAYERCORE = (() => {
const mstreamModule = {}; const mstreamModule = {};
mstreamModule.livePlaylist = {
name: false
};
mstreamModule.altLayout = { mstreamModule.altLayout = {
'moveMeta': false, 'moveMeta': false,
'audioBookCtrls': false, 'audioBookCtrls': false,
@ -86,17 +90,24 @@ const VUEPLAYERCORE = (() => {
playlists: mstreamModule.playlists, playlists: mstreamModule.playlists,
showClear: showClearLink, showClear: showClearLink,
altLayout: mstreamModule.altLayout, altLayout: mstreamModule.altLayout,
meta: MSTREAMPLAYER.playerStats.metadata meta: MSTREAMPLAYER.playerStats.metadata,
livePlaylist: mstreamModule.livePlaylist
}, },
computed: { computed: {
albumArtPath: function () { albumArtPath: function () {
if (!this.meta['album-art']) { if (!this.meta['album-art']) {
return 'assets/img/default.png'; 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: { methods: {
getSongInfo: function() {
openMetadataModal(MSTREAMPLAYER.getCurrentSong().metadata, MSTREAMPLAYER.getCurrentSong().rawFilePath);
},
gsi2: function() {
openMetadataModal(cps.metadata, cps.rawFilePath);
},
goToArtist: function() { goToArtist: function() {
const el = document.createElement('DIV'); const el = document.createElement('DIV');
el.setAttribute('data-artist', this.meta.artist); el.setAttribute('data-artist', this.meta.artist);
@ -111,6 +122,13 @@ const VUEPLAYERCORE = (() => {
checkMove: function (event) { checkMove: function (event) {
document.getElementById("pop").style.visibility = "hidden"; document.getElementById("pop").style.visibility = "hidden";
MSTREAMPLAYER.resetPositionCache(); 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 () { clearRating: async function () {
try { try {
@ -170,6 +188,13 @@ const VUEPLAYERCORE = (() => {
}, },
removeSong: function (event) { removeSong: function (event) {
MSTREAMPLAYER.removeSongAtPosition(this.index, false); 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) { downloadSong: function (event) {
const link = document.createElement("a"); const link = document.createElement("a");
@ -369,10 +394,13 @@ const VUEPLAYERCORE = (() => {
if (!this.meta['album-art']) { if (!this.meta['album-art']) {
return 'assets/img/default.png'; 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: { methods: {
getSongInfo: function() {
openMetadataModal(MSTREAMPLAYER.getCurrentSong().metadata, MSTREAMPLAYER.getCurrentSong().rawFilePath);
},
changeVol: function(event) { changeVol: function(event) {
const rect = this.$refs.volumeWrapper.getBoundingClientRect(); const rect = this.$refs.volumeWrapper.getBoundingClientRect();
const x = event.clientX - rect.left; //x position within the element. 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 // Escape filepath
var rawFilepath = filepath; const rawFilepath = filepath;
filepath = filepath.replace(/\%/g, "%25"); filepath = filepath.replace(/\%/g, "%25");
filepath = filepath.replace(/\#/g, "%23"); filepath = filepath.replace(/\#/g, "%23");
filepath = filepath.replace(/\?/g, "%3F");
if (filepath.charAt(0) === '/') { if (filepath.charAt(0) === '/') {
filepath = filepath.substr(1); filepath = filepath.substr(1);
} }
@ -535,8 +564,18 @@ const VUEPLAYERCORE = (() => {
if (position) { if (position) {
MSTREAMPLAYER.insertSongAt(newSong, position, true); 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 { } else {
MSTREAMPLAYER.addSong(newSong); MSTREAMPLAYER.addSong(newSong, autoPlayOff);
if (mstreamModule.livePlaylist.name && livePlaylist !== false) {
await MSTREAMAPI.addToPlaylist(mstreamModule.livePlaylist.name, newSong.filepath);
}
} }
// perform lookup // 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; return mstreamModule;
})() })()

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -22,27 +22,14 @@ const API = (() => {
return module.servers[module.selectedServer].url; 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 = () => { module.logout = () => {
localStorage.removeItem('token'); localStorage.removeItem('token');
Cookies.remove('x-access-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 = () => { module.goToPlayer = () => {
window.location.assign('../'); window.location.assign(window.location.href.replace('/admin', ''));
} }
module.axios = axios.create({ module.axios = axios.create({

View File

@ -177,6 +177,7 @@ var MSTREAMAPI = (function () {
var rawFilepath = filepath; var rawFilepath = filepath;
filepath = filepath.replace(/\%/g, "%25"); filepath = filepath.replace(/\%/g, "%25");
filepath = filepath.replace(/\#/g, "%23"); filepath = filepath.replace(/\#/g, "%23");
filepath = filepath.replace(/\?/g, "%3F");
if (filepath.charAt(0) === '/') { if (filepath.charAt(0) === '/') {
filepath = filepath.substr(1); filepath = filepath.substr(1);
} }

View File

@ -872,7 +872,7 @@ function getRatedSongs(el) {
}); });
files += createMusicFileHtml(value.filepath, 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"`, value.metadata['album-art'] ? `data-original="album-art/${value.metadata['album-art']}?token=${MSTREAMAPI.currentServer.token}"` : `src="assets/img/default.png"`,
rating, 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>` : undefined);
@ -1387,13 +1387,6 @@ function openPlaybackModal() {
myModal.open('#speedModal'); myModal.open('#speedModal');
} }
function logout(){
localStorage.removeItem('token');
Cookies.remove('x-access-token');
MSTREAMAPI.updateCurrentServer("", "", "");
window.location.href = 'login';
}
// Modals // Modals
const myModal = new HystModal({}); const myModal = new HystModal({});

View File

@ -34,8 +34,16 @@ var JUKEBOX = (function () {
// TODO: Check if websocket has already been created // TODO: Check if websocket has already been created
// open connection // open connection
var l = window.location; let wsLink = '';
var wsLink = ((l.protocol === "https:") ? "wss://" : "ws://") + l.host + '?'; 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) { if (accessKey) {
wsLink = wsLink + 'token=' + accessKey; wsLink = wsLink + 'token=' + accessKey;
if (code) { if (code) {

View File

@ -40,9 +40,11 @@ const MSTREAMPLAYER = (() => {
// Scrobble function // Scrobble function
// This is a placeholder function that the API layer can take hold of to implement the scrobble call // This is a placeholder function that the API layer can take hold of to implement the scrobble call
var scrobbleTimer; let scrobbleTimer;
mstreamModule.scrobble = function () { mstreamModule.scrobble = () => {
return false; MSTREAMAPI.scrobbleByFilePath(
mstreamModule.getCurrentSong().rawFilePath,
(response, error) => {});
} }
// The audioData looks like this // The audioData looks like this
@ -71,7 +73,9 @@ const MSTREAMPLAYER = (() => {
const params = { const params = {
ignoreList: autoDjIgnoreArray, ignoreList: autoDjIgnoreArray,
minRating: mstreamModule.minRating, minRating: mstreamModule.minRating,
ignoreVPaths: mstreamModule.ignoreVPaths ignoreVPaths: Object.keys(mstreamModule.ignoreVPaths).filter((vpath) => {
return mstreamModule.ignoreVPaths[vpath] === true;
})
}; };
const res = await MSTREAMAPI.getRandomSong(params); const res = await MSTREAMAPI.getRandomSong(params);
@ -158,11 +162,11 @@ const MSTREAMPLAYER = (() => {
// Stop the current song // Stop the current song
return goToNextSong(); return goToNextSong();
} }
mstreamModule.previousSong = function () { mstreamModule.previousSong = function () {
return goToPreviousSong(); return goToPreviousSong();
} }
mstreamModule.goToSongAtPosition = function (position) { mstreamModule.goToSongAtPosition = function (position) {
if (!mstreamModule.playlist[position]) { if (!mstreamModule.playlist[position]) {
return false; return false;
@ -471,7 +475,7 @@ const MSTREAMPLAYER = (() => {
// Scrobble song after 30 seconds // Scrobble song after 30 seconds
clearTimeout(scrobbleTimer); 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 // 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.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['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['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) { if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({ navigator.mediaSession.metadata = new MediaMetadata({
title: mstreamModule.playerStats.metadata.title, 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); mstreamModule.updateReplayGainFromSong(curSong);
} }
@ -564,8 +574,15 @@ const MSTREAMPLAYER = (() => {
if (localPlayer.playerObject.paused === false) { if (localPlayer.playerObject.paused === false) {
mstreamModule.playerStats.playing = false; mstreamModule.playerStats.playing = false;
localPlayer.playerObject.pause(); localPlayer.playerObject.pause();
document.title = "mStream Music"
} else { } else {
localPlayer.playerObject.play(); 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; mstreamModule.playerStats.playing = true;
} }
} }
@ -607,6 +624,7 @@ const MSTREAMPLAYER = (() => {
currentTime: 0, currentTime: 0,
playing: false, playing: false,
shouldLoop: false, shouldLoop: false,
shouldLoopOne: false,
shuffle: false, shuffle: false,
volume: 100, volume: 100,
metadata: { metadata: {
@ -680,7 +698,7 @@ const MSTREAMPLAYER = (() => {
url += `&algo=${mstreamModule.transcodeOptions.selectedAlgo}`; url += `&algo=${mstreamModule.transcodeOptions.selectedAlgo}`;
} }
} }
console.log(url)
player.playerObject.src = url; player.playerObject.src = url;
player.songObject = song; player.songObject = song;
player.playerObject.load(); player.playerObject.load();
@ -697,6 +715,9 @@ const MSTREAMPLAYER = (() => {
function callMeOnStreamEnd() { function callMeOnStreamEnd() {
mstreamModule.playerStats.playing = false; mstreamModule.playerStats.playing = false;
if (mstreamModule.playerStats.shouldLoopOne === true) {
return goToSong(mstreamModule.positionCache.val);
}
// Go to next song // Go to next song
goToNextSong(); goToNextSong();
} }
@ -746,18 +767,6 @@ const MSTREAMPLAYER = (() => {
lPlayer.playerObject.currentTime = seektime; 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 // Timer for caching. Helps prevent excess caching due to button mashing
var cacheTimer; var cacheTimer;
function setCachedSong(position) { function setCachedSong(position) {
@ -777,15 +786,19 @@ const MSTREAMPLAYER = (() => {
// Loop // Loop
mstreamModule.setRepeat = (newValue) => {
if (typeof (newValue) !== "boolean") { return; }
if (mstreamModule.playerStats.autoDJ === true) { return; }
mstreamModule.playerStats.shouldLoop = newValue;
}
mstreamModule.toggleRepeat = () => { mstreamModule.toggleRepeat = () => {
if (mstreamModule.playerStats.autoDJ === true) { return; } 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 // Random Song
@ -848,6 +861,7 @@ const MSTREAMPLAYER = (() => {
// Turn off shuffle & loop // Turn off shuffle & loop
mstreamModule.playerStats.shuffle = false; mstreamModule.playerStats.shuffle = false;
mstreamModule.playerStats.shouldLoop = false; mstreamModule.playerStats.shouldLoop = false;
mstreamModule.playerStats.shouldLoopOne = false;
// Add song if necessary // Add song if necessary
if (mstreamModule.playlist.length === 0 || mstreamModule.positionCache.val === mstreamModule.playlist.length - 1) { if (mstreamModule.playlist.length === 0 || mstreamModule.positionCache.val === mstreamModule.playlist.length - 1) {

View File

@ -42,7 +42,7 @@ const VUEPLAYERCORE = (() => {
if (!this.meta['album-art']) { if (!this.meta['album-art']) {
return '../assets/img/default.png'; 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: { methods: {

BIN
webapp/build/icon.icns Normal file

Binary file not shown.

BIN
webapp/build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -151,6 +151,57 @@
</div> </div>
</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 --> <!-- Edit Server Modal -->
<div class="hystmodal" id="editServer" aria-hidden="true"> <div class="hystmodal" id="editServer" aria-hidden="true">
<div class="hystmodal__wrap"> <div class="hystmodal__wrap">
@ -188,6 +239,29 @@
</div> </div>
</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 id="sidenav">
<div class="side-nav-header"> <div class="side-nav-header">
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
@ -248,7 +322,15 @@
</div> </div>
<div onclick="changeView(getRecentlyAdded, this)" class="side-nav-item my-waves"> <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> <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>
<div class="side-nav-item my-waves" onclick="changeView(getRatedSongs, this)"> <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> <?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> <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> <span>Layout</span>
</div> </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"> <div class="side-nav-header">
<span>Navigation</span> <span>Navigation</span>
</div> </div>
@ -313,7 +399,7 @@
<div class="col s12 m4 l3 h100-res"> <div class="col s12 m4 l3 h100-res">
<div class="card aa-card"> <div class="card aa-card">
<div v-if="altLayout.moveMeta === false" class="card-image hide-on-small-only"> <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>
<div class="card-action"> <div class="card-action">
@ -338,7 +424,7 @@
<div class="card-content"> <div class="card-content">
<p> <p>
<b>Title:</b> <b>Title:</b>
{{ (meta.title) ? meta.title : '' }} {{ (meta.title) ? meta.title : (meta.filepath ? meta.filepath.split('/').pop() : '') }}
</p> </p>
<p> <p>
<b>Artist:</b> <b>Artist:</b>
@ -360,7 +446,9 @@
</div> </div>
<div class="flex"> <div class="flex">
<div v-if="altLayout.audioBookCtrls === false" class="button-block"> <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="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> <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> </div>
@ -438,6 +526,10 @@
<h5>Now Playing</h5> <h5>Now Playing</h5>
</div> </div>
<div class="flex grow flex-end" id="playlist-buttons"> <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();"> <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> <?xml version="1.0" encoding="iso-8859-1"?><svg width="27" height="27" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 49 49" style="enable-background:new 0 0 49 49"><path d="M27.5 5h6v10h-6z"/><path d="M39.914 0H.5v49h48V8.586L39.914 0zM10.5 2h26v16h-26V2zm29 45h-31V26h31v21z"/><path d="M13.5 32h7a1 1 0 1 0 0-2h-7a1 1 0 1 0 0 2zM13.5 36h10a1 1 0 1 0 0-2h-10a1 1 0 1 0 0 2zM26.5 36c.27 0 .52-.11.71-.29.18-.19.29-.45.29-.71s-.11-.521-.29-.71c-.37-.37-1.04-.37-1.41 0a.996.996 0 0 0-.3.71c0 .27.109.52.29.71.189.18.439.29.71.29z"/></svg>
</a> </a>
@ -447,7 +539,7 @@
<a title="Share Playlist" onclick="openShareModal();"> <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> <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>
<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> <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> </a>
</div> </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 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="card-content flex">
<div class="custom-card-img"> <div class="custom-card-img">
<img :src="albumArtPath" class="block" /> <img :src="albumArtPath" class="block pointer" v-on:click="getSongInfo" />
</div> </div>
<div class="card-meta"> <div class="card-meta">
<p> <p>
<b>Title:</b> <b>Title:</b>
{{ (meta.title) ? meta.title : '' }} {{ (meta.title) ? meta.title : (meta.filepath ? meta.filepath.split('/').pop() : '') }}
</p> </p>
<p> <p>
<b>Artist:</b> <b>Artist:</b>
@ -484,6 +576,7 @@
<a v-show="showClear.val === true" v-on:click="clearRating">clear</a> <a v-show="showClear.val === true" v-on:click="clearRating">clear</a>
</div> </div>
<div style="visibility:hidden; position: absolute;" class="pop-d" id="pop-d" role="tooltip"> <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 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 v-for="(playlist, index) in playlists" is="popper-playlist-item" :key="index" :index="index" :playlist="playlist"></div>
</div> </div>

View File

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

View File

@ -16,10 +16,7 @@ document.getElementById("login").addEventListener("submit", async e => {
localStorage.setItem("token", res.data.token); localStorage.setItem("token", res.data.token);
const queryString = window.location.search; window.location.assign(window.location.href.replace('/login', ''));
const urlParams = new URLSearchParams(queryString);
const goTo = urlParams.get('redirect') ? '..' + urlParams.get('redirect') : '../';
window.location.href = goTo;
iziToast.success({ iziToast.success({
title: 'Login Success!', title: 'Login Success!',

View File

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

View File

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