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

View File

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

View File

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

1
.gitignore vendored
View File

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

View File

@ -8,7 +8,7 @@ Main|Shared|Admin
## Demo & Other Links
### [Check Out The Demo!](https://demo.mstream.io/)
#### [Check Out The Demo!](https://demo.mstream.io/)
#### [Discord Channel](https://discord.gg/AM896Rr)
@ -28,32 +28,33 @@ Main|Shared|Admin
## Installing mStream
* [Install From Source](docs/install.md)
* [Docker Instructions](https://github.com/linuxserver/docker-mstream)
* [Binaries for Win/OSX/Linux](https://mstream.io/server) - mStream binaries are compiled with Electron and have some extra features
- Runs in background and starts mStream on boot
- Automatic updates
- Adds a tray icon to manage mStream
* [Binaries for Win/OSX/Linux](https://mstream.io/server)
* [Install From Source](docs/install.md)
* [AWS Cloud using Terraform](https://gitlab.com/SiliconTao-Systems/nova)
## Mobile Apps
[<img src="/webapp/assets/img/app-store-logo.png" alt="mStream iOS App" width="200" />](https://apps.apple.com/us/app/mstream-player/id1605378892)
[<img src="/webapp/assets/img/play-store-logo.png" alt="mStream Android App" width="200" />](https://play.google.com/store/apps/details?id=com.nieratechinc.mstreamplayer&hl=en_US)
[Made by Niera Tech](https://mplayer.nieratech.com/)
## Quick Install from CLI
Deploying an mStream server is simple.
```shell
# Install From Git
git clone https://github.com/IrosTheBeggar/mStream.git
cd mStream
npm install --production
# Boot mStream
node cli-boot-wrapper.js
# Install dependencies and run
npm run-script wizard
```
## Android App
[![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
* **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
if (process.versions["electron"]) {
// off to a separate electron boot environment
return require("./build/electron");
require("./build/electron");
} else {
const version = require('./package.json').version;
const { Command } = require('commander');
const program = new Command();
program
.version(version)
.option('-j, --json <json>', 'Specify JSON Boot File', require('path').join(__dirname, 'save/conf/default.json'))
.parse(process.argv);
console.clear();
console.log(`
____ _
_ __ ___ / ___|| |_ _ __ ___ __ _ _ __ ___
| '_ \` _ \\\\___ \\| __| '__/ _ \\/ _\` | '_ \` _ \\
| | | | | |___) | |_| | | __/ (_| | | | | | |
|_| |_| |_|____/ \\__|_| \\___|\\__,_|_| |_| |_|`);
console.log(`v${program.version()}`);
console.log();
console.log('Check out our Discord server:');
console.log('https://discord.gg/AM896Rr');
console.log();
// Boot the server
require("./src/server").serveIt(program.opts().json);
}
const program = require('commander');
program
.version(require('./package.json').version)
.option('-j, --json <json>', 'Specify JSON Boot File', require('path').join(__dirname, 'save/conf/default.json'))
.parse(process.argv);
console.clear();
console.log(`
____ _
_ __ ___ / ___|| |_ _ __ ___ __ _ _ __ ___
| '_ \` _ \\\\___ \\| __| '__/ _ \\/ _\` | '_ \` _ \\
| | | | | |___) | |_| | | __/ (_| | | | | | |
|_| |_| |_|____/ \\__|_| \\___|\\__,_|_| |_| |_|`);
console.log(`v${program.version()}`);
console.log();
console.log('Check out our Discord server:');
console.log('https://discord.gg/AM896Rr');
console.log();
// Boot the server
require("./src/server").serveIt(program.json);

View File

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

8107
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -8,6 +8,7 @@ const fileExplorer = require('../util/file-explorer');
const admin = require('../util/admin');
const config = require('../state/config');
const dbQueue = require('../db/task-queue');
const imageCompress = require('../db/image-compress-manager');
const transcode = require('./transcode');
const db = require('../db/manager');
const { joiValidate } = require('../util/validation');
@ -107,6 +108,16 @@ exports.setup = (mstream) => {
res.json({});
});
mstream.post("/api/v1/admin/db/params/new-scan", async (req, res) => {
const schema = Joi.object({
newScan: Joi.boolean().required()
});
joiValidate(schema, req.body);
await admin.editNewScan(req.body.newScan);
res.json({});
});
mstream.post("/api/v1/admin/db/params/pause", async (req, res) => {
const schema = Joi.object({
pause: Joi.number().integer().min(0).required()
@ -137,6 +148,16 @@ exports.setup = (mstream) => {
res.json({});
});
mstream.post("/api/v1/admin/db/params/compress-image", async (req, res) => {
const schema = Joi.object({
compressImage: Joi.boolean().required()
});
joiValidate(schema, req.body);
await admin.editCompressImages(req.body.compressImage);
res.json({});
});
mstream.get("/api/v1/admin/users", (req, res) => {
// Scrub passwords
const memClone = JSON.parse(JSON.stringify(config.program.users));
@ -153,11 +174,17 @@ exports.setup = (mstream) => {
const schema = Joi.object({
directory: Joi.string().required(),
vpath: Joi.string().pattern(/[a-zA-Z0-9-]+/).required(),
autoAccess: Joi.boolean().default(false)
autoAccess: Joi.boolean().default(false),
isAudioBooks: Joi.boolean().default(false)
});
const input = joiValidate(schema, req.body);
await admin.addDirectory(input.value.directory, input.value.vpath, input.value.autoAccess, mstream);
await admin.addDirectory(
input.value.directory,
input.value.vpath,
input.value.autoAccess,
input.value.isAudioBooks,
mstream);
res.json({});
try {
@ -194,6 +221,10 @@ exports.setup = (mstream) => {
);
res.json({});
});
mstream.post("/api/v1/admin/db/force-compress-images", (req, res) => {
res.json({ started: imageCompress.run() });
});
mstream.post("/api/v1/admin/db/scan/all", (req, res) => {
dbQueue.scanAll();
@ -276,10 +307,21 @@ exports.setup = (mstream) => {
writeLogs: config.program.writeLogs,
secret: config.program.secret.slice(-4),
ssl: config.program.ssl,
storage: config.program.storage
storage: config.program.storage,
maxRequestSize: config.program.maxRequestSize
});
});
mstream.post("/api/v1/admin/config/max-request-size", async (req, res) => {
const schema = Joi.object({
maxRequestSize: Joi.string().pattern(/[0-9]+(KB|MB)/i).required()
});
joiValidate(schema, req.body);
await admin.editMaxRequestSize(req.body.maxRequestSize);
res.json({});
});
mstream.post("/api/v1/admin/config/port", async (req, res) => {
const schema = Joi.object({
port: Joi.number().required()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,9 @@ const Joi = require('joi');
const axios = require('axios');
const config = require('../state/config');
const scribble = require('../state/lastfm');
const db = require('../db/manager');
const { joiValidate } = require('../util/validation');
const { getVPathInfo } = require('../util/vpath');
const Scrobbler = new scribble();
@ -37,6 +39,60 @@ exports.setup = (mstream) => {
);
});
mstream.post('/api/v1/lastfm/scrobble-by-filepath', (req, res) => {
const schema = Joi.object({
filePath: Joi.string().required(),
});
joiValidate(schema, req.body);
// lookup metadata
const pathInfo = getVPathInfo(req.body.filePath, req.user);
const dbObj = { '$and': [
{ 'filepath': { '$eq': pathInfo.relativePath } },
{ 'vpath': { '$eq': pathInfo.vpath } }
]};
const dbFileInfo = db.getFileCollection().findOne(dbObj);
if (!dbFileInfo) {
return res.json({ scrobble: false });
}
// log play
const result = db.getUserMetadataCollection().findOne({ '$and':[{ 'hash': dbFileInfo.hash}, { 'user': req.user.username }] });
if (!result) {
db.getUserMetadataCollection().insert({
user: req.user.username,
hash: dbFileInfo.hash,
pc: 1,
lp: Date.now()
});
} else {
result.pc = result.pc && typeof result.pc === 'number'
? result.pc + 1 : 1;
result.lp = Date.now();
db.getUserMetadataCollection().update(result);
}
db.saveUserDB();
res.json({});
if (req.user['lastfm-user'] && req.user['lastfm-password']) {
// scrobble on last fm
Scrobbler.Scrobble(
{
artist: dbFileInfo.artist,
album: dbFileInfo.album,
track: dbFileInfo.title
},
req.user['lastfm-user'],
(post_return_data) => {}
);
}
});
mstream.post('/api/v1/lastfm/test-login', async (req, res) => {
const schema = Joi.object({
username: Joi.string().required(),

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -262,6 +262,10 @@ const foldersView = Vue.component('folders-view', {
<input id="folder-auto-access" type="checkbox" checked/>
<span>Give Access To All Users</span>
</label></div>
<div class="pad-checkbox"><label>
<input id="folder-is-audiobooks" type="checkbox"/>
<span>Audiobooks & Podcasts</span>
</label></div>
</div>
<button class="btn green waves-effect waves-light col m6 s12" type="submit" :disabled="submitPending === true">
{{submitPending === false ? 'Add Folder' : 'Adding...'}}
@ -345,7 +349,8 @@ const foldersView = Vue.component('folders-view', {
data: {
directory: this.folder.value,
vpath: this.dirName,
autoAccess: document.getElementById('folder-auto-access').checked
autoAccess: document.getElementById('folder-auto-access').checked,
isAudioBooks: document.getElementById('folder-is-audiobooks').checked
}
});
@ -445,7 +450,7 @@ const usersView = Vue.component('users-view', {
<form id="add-user-form" @submit.prevent="addUser">
<div class="row">
<div class="input-field directory-name-field col s12 m6">
<input @blur="maybeResetForm()" pattern="[a-zA-Z0-9-]+" v-model="newUsername" id="new-username" required type="text" class="validate">
<input @blur="maybeResetForm()" v-model="newUsername" id="new-username" required type="text" class="validate">
<label for="new-username">Username</label>
</div>
<div class="input-field directory-name-field col s12 m6">
@ -625,7 +630,7 @@ const usersView = Vue.component('users-view', {
title: 'You will be taken the login page',
position: 'center',
buttons: [['<button>Go!</button>', (instance, toast) => {
API.checkAuthAndKickToLogin();
API.logout();
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
}, true]],
});
@ -696,6 +701,12 @@ const advancedView = Vue.component('advanced-view', {
[<a v-on:click="openModal('edit-port-modal')">edit</a>]
</td>
</tr>
<tr>
<td><b>Max Request Size:</b> {{params.maxRequestSize}}</td>
<td>
[<a v-on:click="openModal('edit-request-size-modal')">edit</a>]
</td>
</tr>
<tr>
<td><b>Address:</b> {{params.address}}</td>
<td>
@ -817,7 +828,7 @@ const advancedView = Vue.component('advanced-view', {
url: `${API.url()}/api/v1/admin/config/secret`,
data: { strength: 128 }
}).then(() => {
API.checkAuthAndKickToLogin();
API.logout();
}).catch(() => {
iziToast.error({
title: 'Failed',
@ -930,6 +941,19 @@ const dbView = Vue.component('db-view', {
[<a v-on:click="toggleSkipImg()">edit</a>]
</td>
</tr>
<tr>
<td><b>Compress Images:</b> {{dbParams.compressImage}}</td>
<td>
[<a v-on:click="recompressImages()">re-compress</a>]
[<a v-on:click="toggleCompressImage()">edit</a>]
</td>
</tr>
<tr>
<td><b>Use New File Scanner:</b> {{dbParams.newScan}}</td>
<td>
[<a v-on:click="toggleNewScan()">edit</a>]
</td>
</tr>
<tr>
<td><b>Max Concurrent Scans:</b> {{dbParams.maxConcurrentTasks}}</td>
<td>
@ -987,7 +1011,7 @@ const dbView = Vue.component('db-view', {
<tr v-for="(v, k) in sharedPlaylists">
<th><a target="_blank" v-bind:href="'/shared/'+ v.playlistId">{{v.playlistId}}</a></th>
<th>{{v.user}}</th>
<th>{{v.expires}}</th>
<th>{{new Date(v.expires * 1000).toLocaleString()}}</th>
<th>[<a v-on:click="deletePlaylist(v)">delete</a>]</th>
</tr>
</tbody>
@ -1139,6 +1163,144 @@ const dbView = Vue.component('db-view', {
});
}
},
recompressImages: function() {
iziToast.question({
timeout: 20000,
close: false,
overlayClose: true,
overlay: true,
displayMode: 'once',
id: 'question',
zindex: 99999,
layout: 2,
maxWidth: 600,
title: `<b>Compress All Images?</b>`,
message: 'This process will run in the background',
position: 'center',
buttons: [
[`<button><b>Start</b></button>`, async (instance, toast) => {
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
try {
const res = await API.axios({
method: 'POST',
url: `${API.url()}/api/v1/admin/db/force-compress-images`,
});
if (res.data.started === true) {
iziToast.success({
title: 'Process Started',
position: 'topCenter',
timeout: 3500
});
} else {
iziToast.warning({
title: 'Image Compression In Progress',
position: 'topCenter',
timeout: 3500
});
}
} catch (err) {
iziToast.error({
title: 'Failed',
position: 'topCenter',
timeout: 3500
});
}
}, true],
['<button>Go Back</button>', (instance, toast) => {
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
}],
]
});
},
toggleNewScan: function() {
iziToast.question({
timeout: 20000,
close: false,
overlayClose: true,
overlay: true,
displayMode: 'once',
id: 'question',
zindex: 99999,
layout: 2,
maxWidth: 600,
title: `<b>${this.dbParams.newScan === true ? 'Disable' : 'Enable'} Use New Scanner?</b>`,
position: 'center',
buttons: [
[`<button><b>${this.dbParams.newScan === true ? 'Disable' : 'Enable'}</b></button>`, (instance, toast) => {
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
API.axios({
method: 'POST',
url: `${API.url()}/api/v1/admin/db/params/new-scan`,
data: { newScan: !this.dbParams.newScan }
}).then(() => {
// update fronted data
Vue.set(ADMINDATA.dbParams, 'newScan', !this.dbParams.newScan);
iziToast.success({
title: 'Updated Successfully',
position: 'topCenter',
timeout: 3500
});
}).catch(() => {
iziToast.error({
title: 'Failed',
position: 'topCenter',
timeout: 3500
});
});
}, true],
['<button>Go Back</button>', (instance, toast) => {
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
}],
]
});
},
toggleCompressImage: function() {
iziToast.question({
timeout: 20000,
close: false,
overlayClose: true,
overlay: true,
displayMode: 'once',
id: 'question',
zindex: 99999,
layout: 2,
maxWidth: 600,
title: `<b>${this.dbParams.compressImage === true ? 'Disable' : 'Enable'} Compress Images?</b>`,
position: 'center',
buttons: [
[`<button><b>${this.dbParams.compressImage === true ? 'Disable' : 'Enable'}</b></button>`, (instance, toast) => {
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
API.axios({
method: 'POST',
url: `${API.url()}/api/v1/admin/db/params/compress-image`,
data: { compressImage: !this.dbParams.compressImage }
}).then(() => {
// update fronted data
Vue.set(ADMINDATA.dbParams, 'compressImage', !this.dbParams.compressImage);
iziToast.success({
title: 'Updated Successfully',
position: 'topCenter',
timeout: 3500
});
}).catch(() => {
iziToast.error({
title: 'Failed',
position: 'topCenter',
timeout: 3500
});
});
}, true],
['<button>Go Back</button>', (instance, toast) => {
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
}],
]
});
},
toggleSkipImg: function() {
iziToast.question({
timeout: 20000,
@ -2371,6 +2533,74 @@ const userAccessView = Vue.component('user-access-view', {
}
});
const editRequestSizeModal = Vue.component('edit-request-size-modal', {
data() {
return {
params: ADMINDATA.serverParams,
submitPending: false,
maxRequestSize: ADMINDATA.serverParams.maxRequestSize
};
},
template: `
<form @submit.prevent="updatePort">
<div class="modal-content">
<h4>Change Max Request Size</h4>
<p>Accepts KB or MB</p>
<div class="input-field">
<input v-model="maxRequestSize" id="edit-max-request-size" required type="text">
<label for="edit-port">Edit Max Request Size</label>
</div>
<blockquote>
Requires a reboot.
</blockquote>
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn-flat">Go Back</a>
<button class="btn green waves-effect waves-light" type="submit" :disabled="submitPending === true">
{{submitPending === false ? 'Update' : 'Updating...'}}
</button>
</div>
</form>`,
mounted: function () {
M.updateTextFields();
},
methods: {
updatePort: async function() {
try {
this.submitPending = true;
this.maxRequestSize = this.maxRequestSize.replaceAll(' ', '');
await API.axios({
method: 'POST',
url: `${API.url()}/api/v1/admin/config/max-request-size`,
data: { maxRequestSize: this.maxRequestSize }
});
// update fronted data
Vue.set(ADMINDATA.serverParams, 'maxRequestSize', this.maxRequestSize);
// close & reset the modal
M.Modal.getInstance(document.getElementById('admin-modal')).close();
iziToast.success({
title: 'Success: Allow the server 30 seconds to reboot',
position: 'topCenter',
timeout: 3500
});
} catch(err) {
iziToast.error({
title: 'Failed to Update',
position: 'topCenter',
timeout: 3500
});
}finally {
this.submitPending = false;
}
}
}
});
const editPortModal = Vue.component('edit-port-modal', {
data() {
return {
@ -3214,6 +3444,7 @@ const modVM = new Vue({
'user-access-modal': userAccessView,
'file-explorer-modal': fileExplorerModal,
'edit-port-modal': editPortModal,
'edit-request-size-modal': editRequestSizeModal,
'edit-address-modal': editAddressModal,
'edit-scan-interval-modal': editScanIntervalView,
'edit-save-interval-modal': editSaveIntervalView,

View File

@ -39,8 +39,12 @@ const MSTREAMAPI = (() => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/file-explorer/recursive', { directory: directory });
}
mstreamModule.savePlaylist = (title, songs) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/playlist/save', { title: title, songs: songs });
mstreamModule.savePlaylist = (title, songs, live) => {
const postData = { title: title, songs: songs };
if (live !== undefined) {
postData.live = live;
}
return req('POST', mstreamModule.currentServer.host + 'api/v1/playlist/save', postData);
}
mstreamModule.newPlaylist = (title) => {
@ -71,20 +75,20 @@ const MSTREAMAPI = (() => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/db/search', postObject);
}
mstreamModule.artists = () => {
return req('GET', mstreamModule.currentServer.host + 'api/v1/db/artists', false);
mstreamModule.artists = (postObject) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/db/artists', postObject);
}
mstreamModule.albums = () => {
return req('GET', mstreamModule.currentServer.host + 'api/v1/db/albums', false);
mstreamModule.albums = (postObject) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/db/albums', postObject);
}
mstreamModule.artistAlbums = (artist) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/artists-albums", { artist: artist });
mstreamModule.artistAlbums = (postObject) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/artists-albums", postObject);
}
mstreamModule.albumSongs = (album, artist, year) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/album-songs", { album, artist, year });
mstreamModule.albumSongs = (postObject) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/album-songs", postObject);
}
mstreamModule.dbStatus = () => {
@ -99,12 +103,20 @@ const MSTREAMAPI = (() => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/rate-song", { filepath: filepath, rating: rating });
}
mstreamModule.getRated = () => {
return req('GET', mstreamModule.currentServer.host + "api/v1/db/rated", false);
mstreamModule.getRated = (postObject) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/rated", postObject);
}
mstreamModule.getRecentlyAdded = (limit) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/recent/added", { limit: limit });
mstreamModule.getRecentlyAdded = (limit, ignoreVPaths) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/recent/added", { limit: limit, ignoreVPaths });
}
mstreamModule.getRecentlyPlayed = (limit, ignoreVPaths) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/stats/recently-played", { limit: limit, ignoreVPaths });
}
mstreamModule.getMostPlayed = (limit, ignoreVPaths) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/stats/most-played", { limit: limit, ignoreVPaths });
}
mstreamModule.lookupMetadata = (filepath) => {
@ -120,6 +132,10 @@ const MSTREAMAPI = (() => {
return req('POST', mstreamModule.currentServer.host + "api/v1/lastfm/scrobble-by-metadata", { artist: artist, album: album, track: trackName });
}
mstreamModule.scrobbleByFilePath = (filePath) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/lastfm/scrobble-by-filepath", { filePath });
}
// LOGIN
mstreamModule.login = (username, password, url) => {
return req('POST', url ? url + "api/v1/auth/login" : "api/v1/auth/login", { username: username, password: password });
@ -132,7 +148,7 @@ const MSTREAMAPI = (() => {
mstreamModule.logout = () => {
localStorage.removeItem('token');
Cookies.remove('x-access-token');
window.location.href = './login';
document.location.assign(window.location.href + (window.location.href.slice(-1) === '/' ? '' : '/') + 'login');
}
return mstreamModule;

View File

@ -90,13 +90,6 @@ myDropzone.on('error', (err, msg, xhr) => {
iziToast.error(iziStuff);
});
// Setup scrobbling
MSTREAMPLAYER.scrobble = function () {
if (MSTREAMPLAYER.playerStats.metadata.artist && MSTREAMPLAYER.playerStats.metadata.title) {
MSTREAMAPI.scrobbleByMetadata(MSTREAMPLAYER.playerStats.metadata.artist, MSTREAMPLAYER.playerStats.metadata.album, MSTREAMPLAYER.playerStats.metadata.title);
}
}
////////////////////////////// Global Variables
// These vars track your position within the file explorer
var fileExplorerArray = [];
@ -128,7 +121,7 @@ function renderAlbum(id, artist, name, albumArtFile, year) {
return `<li class="collection-item">
<div ${year ? `data-year="${year}"` : '' } ${artist ? `data-artist="${artist}"` : '' } ${id ? `data-album="${id}"` : '' } class="albumz flex2" onclick="getAlbumsOnClick(this);">
${albumArtFile ?
`<img class="album-art-box" loading="lazy" src="${MSTREAMAPI.currentServer.host}album-art/${albumArtFile}?token=${MSTREAMAPI.currentServer.token}">`:
`<img class="album-art-box" loading="lazy" src="${MSTREAMAPI.currentServer.host}album-art/${albumArtFile}?compress=s&token=${MSTREAMAPI.currentServer.token}">`:
'<svg xmlns="http://www.w3.org/2000/svg" class="album-art-box" viewBox="0 0 512 512" xml:space="preserve"><path d="M437 75C390.7 28.6 326.7 0 256 0 114.6 0 0 114.6 0 256c0 70.7 28.6 134.7 75 181s110.3 75 181 75c141.4 0 256-114.6 256-256 0-70.7-28.6-134.7-75-181zM256 477.9c-122.3 0-221.9-99.5-221.9-221.9S133.7 34.1 256 34.1 477.9 133.7 477.9 256 378.3 477.9 256 477.9z"/><path d="M256 145.1c-61.3 0-110.9 49.7-110.9 110.9S194.7 366.9 256 366.9 366.9 317.3 366.9 256c0-61.2-49.7-110.9-110.9-110.9zm0 187.7c-42.4 0-76.8-34.4-76.8-76.8s34.4-76.8 76.8-76.8 76.8 34.4 76.8 76.8-34.4 76.8-76.8 76.8z"/><path d="M238.9 238.9H273V273h-34.1zM256 102.4V68.3h-.6c-31 0-60.1 7.6-85.8 21l1-.5c-26 13.5-47.7 31.9-64.5 54.2l-.3.5 27.3 20.5c28.1-37.5 72.4-61.5 122.3-61.5l.6-.1z"/></svg>'}
<span><b>${name}</b> ${year ? `<br>[${year}]` : ''}</span>
</div>
@ -144,7 +137,7 @@ function renderArtist(artist) {
function renderFileWithMetadataHtml(filepath, lokiId, metadata) {
return `<li data-lokiid="${lokiId}" class="collection-item">
<div data-file_location="${filepath}" class="filez flex" onclick="onFileClick(this);">
<img class="album-art-box" loading="lazy" ${metadata['album-art'] ? `src="${MSTREAMAPI.currentServer.host}album-art/${metadata['album-art']}?token=${MSTREAMAPI.currentServer.token}"` : 'src="assets/img/default.png"'}>
<img class="album-art-box" loading="lazy" ${metadata['album-art'] ? `src="${MSTREAMAPI.currentServer.host}album-art/${metadata['album-art']}?compress=s&token=${MSTREAMAPI.currentServer.token}"` : 'src="assets/img/default.png"'}>
<div>
<b><span>${(!metadata || !metadata.title) ? filepath.split("/").pop() : `${metadata.title}`}</span></b>
${metadata.artist ? `</b><br><span style="font-size:15px;">${metadata.artist}</span>` : ''}
@ -164,9 +157,9 @@ function createMusicFileHtml(fileLocation, title, aa, rating, subtitle) {
<div data-file_location="${fileLocation}" class="filez ${aa ? 'flex2' : ''}" onclick="onFileClick(this);">
${aa ? `<img loading="lazy" class="album-art-box" ${aa}>` : '<svg class="music-image" height="18" width="18" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path d="M9 37.5c-3.584 0-6.5-2.916-6.5-6.5s2.916-6.5 6.5-6.5a6.43 6.43 0 012.785.634l.715.34V5.429l25-3.846V29c0 3.584-2.916 6.5-6.5 6.5s-6.5-2.916-6.5-6.5 2.916-6.5 6.5-6.5a6.43 6.43 0 012.785.634l.715.34V11.023l-19 2.931V31c0 3.584-2.916 6.5-6.5 6.5z" fill="#8bb7f0"/><path d="M37 2.166V29c0 3.308-2.692 6-6 6s-6-2.692-6-6 2.692-6 6-6a5.93 5.93 0 012.57.586l1.43.68V10.441l-1.152.178-18 2.776-.848.13V31c0 3.308-2.692 6-6 6s-6-2.692-6-6 2.692-6 6-6a5.93 5.93 0 012.57.586l1.43.68V5.858l24-3.692M38 1L12 5v19.683A6.962 6.962 0 009 24a7 7 0 107 7V14.383l18-2.776v11.076A6.962 6.962 0 0031 22a7 7 0 107 7V1z" fill="#4e7ab5"/></svg>'}
<span>
${subtitle ? `<b>` : ''}
${subtitle !== undefined ? `<b>` : ''}
<span class="${aa ? '' : 'item-text'}">${rating ? `[${rating}] ` : ''}${title}</span>
${subtitle ? `</b><br><span>${subtitle}</span>` : ''}
${subtitle !== undefined ? `</b><br><span>${subtitle}</span>` : ''}
</span>
</div>
<div class="song-button-box">
@ -430,6 +423,7 @@ async function init() {
response.playlists.forEach(p => {
VUEPLAYERCORE.playlists.push(p);
document.getElementById('pop-f').innerHTML += `<div class="pop-list-item" onclick="addToPlaylistUI('${p.name}')">&#8226; ${p.name}</div>`;
document.getElementById('live-playlist-select').innerHTML += `<option value="${p.name}">${p.name}</option>`;
});
if (response.transcode) {
@ -463,6 +457,27 @@ async function init() {
MSTREAMPLAYER.transcodeOptions.selectedAlgo = localStorage.getItem('trans-algo-select');
} catch (e) {}
try{
VUEPLAYERCORE.livePlaylist.name = localStorage.getItem('live-playlist-auto-start') ? localStorage.getItem('live-playlist-auto-start') : false;
if (VUEPLAYERCORE.livePlaylist.name) {
// get current playlist
const response = await MSTREAMAPI.loadPlaylist(VUEPLAYERCORE.livePlaylist.name);
// set the queue to the current playlist
MSTREAMPLAYER.clearPlaylist();
response.forEach(value => {
VUEPLAYERCORE.addSongWizard(value.filepath, value.metadata, false, undefined, false, true);
});
document.getElementById('set_live_playlist').classList.remove('green');
document.getElementById('set_live_playlist').classList.add('blue');
document.getElementById('set_live_playlist').value = 'Disable Live Playlist';
document.getElementById('live-playlist-hide-these').hidden = true;
}
}catch(err) {}
dbStatus();
}
@ -537,6 +552,10 @@ function openSaveModal() {
myModal.open('#savePlaylist');
}
function openLivePlaylistModal() {
myModal.open('#livePlaylist');
}
function openNewPlaylistModal() {
myModal.open('#newPlaylist');
}
@ -545,7 +564,38 @@ function openPlaybackModal() {
myModal.open('#speedModal');
}
function openMetadataModal(metadata, fp) {
if (metadata === null) {
return iziToast.warning({
title: 'No Metadata Found',
position: 'topCenter',
timeout: 3500
});
}
document.getElementById('meta--title').innerHTML = metadata.title;
document.getElementById('meta--album').innerHTML = metadata.album;
document.getElementById('meta--artist').innerHTML = metadata.artist;
document.getElementById('meta--year').innerHTML = metadata.year;
document.getElementById('meta--disk').innerHTML = metadata.disk;
document.getElementById('meta--track').innerHTML = metadata.track;
document.getElementById('meta--rating').innerHTML = metadata.rating;
document.getElementById('meta--rg').innerHTML = metadata['replaygain-track'];
document.getElementById('meta--fp').innerHTML = fp;
document.getElementById('meta--fp').href = 'media' + fp;
document.getElementById('meta--aa').innerHTML = 'album-art/' + metadata['album-art'];
if (metadata['album-art']) {
document.getElementById('meta--aa').href = `album-art/${metadata['album-art']}`;
} else {
document.getElementById('meta--aa').href = '#';
}
myModal.open('#metadataModel');
}
function openEditModal() {
document.getElementById('server_address').value = MSTREAMAPI.currentServer.host;
document.getElementById('server_username').value = MSTREAMAPI.currentServer.username;
myModal.open('#editServer');
}
@ -624,42 +674,6 @@ function downloadFileplaylist(el) {
document.getElementById('downform').innerHTML = '';
}
function getArtistz(el) {
const artist = el.getAttribute('data-artist');
programState.push({
state: 'artist',
name: artist,
previousScroll: document.getElementById('filelist').scrollTop,
previousSearch: document.getElementById('localSearchBar').value
});
getArtistsAlbums(artist)
}
async function getArtistsAlbums(artist) {
setBrowserRootPanel('Albums');
document.getElementById('directoryName').innerHTML = 'Artist: ' + artist;
document.getElementById('filelist').innerHTML = getLoadingSvg();
try {
const response = await MSTREAMAPI.artistAlbums(artist);
let albums = '';
response.albums.forEach(value => {
const albumString = value.name ? value.name : 'SINGLES';
// 'value.name === null ? artist : null' is some clever shit that only passes in artist info when the album is null
// This is so we get the singles for this artist
// If the album is specified, we don't want to limit by artist
albums += renderAlbum(value.name, value.name === null ? artist : null, albumString, value.album_art_file, value.year);
currentBrowsingList.push({ type: 'album', name: value.name, artist: artist, album_art_file: value.album_art_file })
});
document.getElementById('filelist').innerHTML = albums;
}catch (err) {
document.getElementById('filelist').innerHTML = '<div>Server call failed</div>';
return boilerplateFailure(err);
}
}
function onSearchButtonClick() {
// Hide Filepath
document.getElementById('search_folders').classList.toggle('super-hide');
@ -720,6 +734,7 @@ async function getAllPlaylists() {
const response = await MSTREAMAPI.getAllPlaylists();
VUEPLAYERCORE.playlists.length = 0;
document.getElementById('pop-f').innerHTML = '<div class="pop-f pop-playlist">Add To Playlist:</div>';
document.getElementById('live-playlist-select').innerHTML = `<option value="" disabled selected>Select Playlist</option>`;
// loop through the json array and make an array of corresponding divs
let playlists = '<ul class="collection">';
@ -729,6 +744,7 @@ async function getAllPlaylists() {
currentBrowsingList.push(lol);
VUEPLAYERCORE.playlists.push(lol);
document.getElementById('pop-f').innerHTML += `<div class="pop-list-item" onclick="addToPlaylistUI('${p.name}')">&#8226; ${p.name}</div>`;
document.getElementById('live-playlist-select').innerHTML += `<option value="${p.name}">${p.name}</option>`;
});
playlists += '</ul>'
@ -838,6 +854,7 @@ async function newPlaylist() {
document.getElementById("newPlaylistForm").reset();
VUEPLAYERCORE.playlists.push({ name: title, type: 'playlist'});
document.getElementById('pop-f').innerHTML += `<div class="pop-list-item" onclick="addToPlaylistUI('${title}')">&#8226; ${title}</div>`;
document.getElementById('live-playlist-select').innerHTML += `<option value="${title}">${title}</option>`;
if (programState[0].state === 'allPlaylists') {
getAllPlaylists();
@ -848,6 +865,80 @@ async function newPlaylist() {
document.getElementById('new_playlist').disabled = false;
}
async function setLivePlaylist() {
try{
document.getElementById('set_live_playlist').disabled = true;
if (VUEPLAYERCORE.livePlaylist.name !== false) {
VUEPLAYERCORE.livePlaylist.name = false;
document.getElementById('set_live_playlist').classList.remove('blue');
document.getElementById('set_live_playlist').classList.add('green');
document.getElementById('set_live_playlist').value = 'Enable Live Playlist';
document.getElementById('live-playlist-hide-these').hidden = false;
myModal.close();
return;
}
let livePlaylistName;
if (document.getElementById('radio-use-existing').checked === true) {
if (document.getElementById('live-playlist-select').value === "") {
const err = new Error('No Playlist Selected');
err.responseJSON = { error: 'No Playlist Selected' };
throw err;
}
livePlaylistName = document.getElementById('live-playlist-select').value;
} else {
if (document.getElementById('new-live-playlist-name').value === "") {
const err = new Error('Playlist Name Required');
err.responseJSON = { error: 'Playlist Name Required' };
throw err;
}
livePlaylistName = document.getElementById('new-live-playlist-name').value;
}
// check if checkbox is checked
if(document.getElementById('persist_live_queue').checked === true) {
localStorage.setItem('live-playlist-auto-start', livePlaylistName)
} else {
localStorage.removeItem('live-playlist-auto-start');
}
// set live var
VUEPLAYERCORE.livePlaylist.name = livePlaylistName;
// get current playlist
const response = await MSTREAMAPI.loadPlaylist(VUEPLAYERCORE.livePlaylist.name);
// set the queue to the current playlist
if (response.length > 0) {
MSTREAMPLAYER.clearPlaylist();
response.forEach(value => {
VUEPLAYERCORE.addSongWizard(value.filepath, value.metadata, false, undefined, false, true);
});
} else {
// save current queue
const songs = [];
for (let i = 0; i < MSTREAMPLAYER.playlist.length; i++) {
songs.push(MSTREAMPLAYER.playlist[i].filepath);
}
MSTREAMAPI.savePlaylist(livePlaylistName, songs, true);
}
document.getElementById('set_live_playlist').classList.remove('green');
document.getElementById('set_live_playlist').classList.add('blue');
document.getElementById('set_live_playlist').value = 'Disable Live Playlist';
document.getElementById('live-playlist-hide-these').hidden = true;
// close modal
myModal.close();
} catch(err) {
boilerplateFailure(err);
} finally {
document.getElementById('set_live_playlist').disabled = false;
}
}
async function savePlaylist() {
if (MSTREAMPLAYER.playlist.length == 0) {
iziToast.warning({
@ -883,8 +974,11 @@ async function savePlaylist() {
VUEPLAYERCORE.playlists.push({ name: title, type: 'playlist'});
document.getElementById('pop-f').innerHTML += `<div class="pop-list-item" onclick="addToPlaylistUI('${title}')">&#8226; ${title}</div>`;
document.getElementById('live-playlist-select').innerHTML += `<option value="${title}">${title}</option>`;
}catch(err) {
boilerplateFailure(err);
} finally {
document.getElementById('save_playlist').disabled = false;
}
}
@ -895,7 +989,12 @@ async function getAllArtists() {
programState = [{ state: 'allArtists' }];
try {
const response = await MSTREAMAPI.artists();
const response = await MSTREAMAPI.artists({
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
})
});
// parse through the json array and make an array of corresponding divs
let artists = '<ul class="collection">';
response.artists.forEach(value => {
@ -929,7 +1028,13 @@ async function getArtistsAlbums(artist) {
document.getElementById('filelist').innerHTML = getLoadingSvg();
try {
const response = await MSTREAMAPI.artistAlbums(artist);
const response = await MSTREAMAPI.artistAlbums({
artist: artist,
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
})
});
let albums = '<ul>';
response.albums.forEach(value => {
const albumString = value.name ? value.name : 'SINGLES';
@ -956,7 +1061,12 @@ async function getAllAlbums() {
programState = [{ state: 'allAlbums' }];
try {
const response = await MSTREAMAPI.albums();
const response = await MSTREAMAPI.albums({
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
})
});
//parse through the json array and make an array of corresponding divs
let albums = '<ul class="collection">';
response.albums.forEach(value => {
@ -1001,7 +1111,15 @@ async function getAlbumSongs(album, artist, year) {
document.getElementById('localSearchBar').value = '';
try {
const response = await MSTREAMAPI.albumSongs(album, artist, year)
const response = await MSTREAMAPI.albumSongs({
album,
artist,
year,
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
})
});
//parse through the json array and make an array of corresponding divs
let files = '<ul class="collection">';
response.forEach(song => {
@ -1024,7 +1142,11 @@ async function getRatedSongs() {
programState = [{ state: 'allRated' }];
try {
const response = await MSTREAMAPI.getRated();
const response = await MSTREAMAPI.getRated({
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
})
});
//parse through the json array and make an array of corresponding divs
let files = '';
response.forEach(value => {
@ -1040,10 +1162,10 @@ async function getRatedSongs() {
});
files += createMusicFileHtml(value.filepath,
value.metadata.title ? value.metadata.title : value.filepath,
value.metadata['album-art'] ? `src="${MSTREAMAPI.currentServer.host}album-art/${value.metadata['album-art']}?token=${MSTREAMAPI.currentServer.token}"` : `src="assets/img/default.png"`,
value.metadata.title ? value.metadata.title : value.filepath.split('/').pop(),
value.metadata['album-art'] ? `src="${MSTREAMAPI.currentServer.host}album-art/${value.metadata['album-art']}?compress=s&token=${MSTREAMAPI.currentServer.token}"` : `src="assets/img/default.png"`,
rating,
value.metadata.artist ? `<span style="font-size:15px;">${value.metadata.artist}</span>` : undefined);
value.metadata.artist ? `<span style="font-size:15px;">${value.metadata.artist}</span>` : '');
});
document.getElementById('filelist').innerHTML = files;
@ -1053,6 +1175,106 @@ async function getRatedSongs() {
}
}
///////////////// Recently Played
function getRecentlyPlayed() {
setBrowserRootPanel('Recently Played');
document.getElementById('filelist').innerHTML = getLoadingSvg();
document.getElementById('directoryName').innerHTML = 'Get last &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
function getRecentlyAdded() {
setBrowserRootPanel('Recently Added');
@ -1067,7 +1289,12 @@ async function redoRecentlyAdded() {
programState = [{ state: 'recentlyAdded'}];
try {
const response = await MSTREAMAPI.getRecentlyAdded(document.getElementById('recently-added-limit').value);
const response = await MSTREAMAPI.getRecentlyAdded(
document.getElementById('recently-added-limit').value,
Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
}));
//parse through the json array and make an array of corresponding divs
let filelist = '<ul class="collection">';
response.forEach(el => {
@ -1078,9 +1305,9 @@ async function redoRecentlyAdded() {
filelist += createMusicFileHtml(el.filepath,
el.metadata.title ? `${el.metadata.title}`: el.filepath.split("/").pop(),
el.metadata['album-art'] ? `src="${MSTREAMAPI.currentServer.host}album-art/${el.metadata['album-art']}?token=${MSTREAMAPI.currentServer.token}"` : `src="assets/img/default.png"`,
el.metadata['album-art'] ? `src="${MSTREAMAPI.currentServer.host}album-art/${el.metadata['album-art']}?compress=s&token=${MSTREAMAPI.currentServer.token}"` : `src="assets/img/default.png"`,
undefined,
el.metadata.artist ? `<span style="font-size:15px;">${el.metadata.artist}</span>` : undefined);
el.metadata.artist ? `<span style="font-size:15px;">${el.metadata.artist}</span>` : '');
});
filelist += '</ul>'
@ -1201,8 +1428,13 @@ function getMobilePanel(){
<a target="_blank" href="https://play.google.com/store/apps/details?id=mstream.music&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1">
<img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png'/>
</a>
<div class="mobile-placeholder">&nbsp;</div>
</div>
<div class="mobile-links pad-6">
<a target="_blank" href="https://apps.apple.com/us/app/mstream-player/id1605378892">
<img alt='Get it on The App Store' src='assets/img/app-store-logo.png'/>
</a>
</div>
<br>
<div class="pad-6">
<a target="_blank" href="/qr"><b>Checkout the QR Code tool to help add your server to the app</b></a>
</div>`;
@ -1314,7 +1546,14 @@ function createJukeboxPanel() {
return '<div class="pad-6">An error occurred. Please refresh the page and try again</div>';
}
const address = `${window.location.protocol}//${window.location.host}/remote/${JUKEBOX.stats.adminCode}`;
let address = '';
if(MSTREAMAPI.currentServer.host) {
address = `${MSTREAMAPI.currentServer.host}remote/${JUKEBOX.stats.adminCode}`;
}else {
address = `${window.location.protocol}//${window.location.host}/remote/${JUKEBOX.stats.adminCode}`;
}
// const address = `${window.location.protocol}//${window.location.host}/remote/${JUKEBOX.stats.adminCode}`;
return `<div class="autoselect pad-6">
<h4>Code: ${JUKEBOX.stats.adminCode}</h4>
<h4><a target="_blank" href="${address}">${address}</a><h4>
@ -1455,7 +1694,13 @@ async function submitSearchForm() {
try {
document.getElementById('search-results').innerHTML += '<div class="loading-screen"><svg class="spinner" width="65px" height="65px" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg"><circle class="spinner-path" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33" r="30"></circle></svg></div>'
var postObject = { search: document.getElementById('search-term').value};
const postObject = {
search: document.getElementById('search-term').value,
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
})
};
if (document.getElementById("search-in-artists") && document.getElementById("search-in-artists").checked === false) { postObject.noArtists = true; }
searchToggles.artists = document.getElementById("search-in-artists").checked;
if (document.getElementById("search-in-albums") && document.getElementById("search-in-albums").checked === false) { postObject.noAlbums = true; }
@ -1510,6 +1755,31 @@ async function submitSearchForm() {
}
}
///////////////// Config
function advancedConfig() {
setBrowserRootPanel('Config', false);
let newHtml = `<div class="pad-6">
<h5>Use Folders</h5>
<p>Unchecked folders will be ignored in all DB queries (including Auto DJ)</p>`;
for (let i = 0; i < MSTREAMAPI.currentServer.vpaths.length; i++) {
let checkedString = '';
if (!MSTREAMPLAYER.ignoreVPaths[MSTREAMAPI.currentServer.vpaths[i]]) {
checkedString = 'checked';
}
newHtml += `
<label for="autodj-folder-${MSTREAMAPI.currentServer.vpaths[i]}">
<input ${checkedString} id="autodj-folder-${MSTREAMAPI.currentServer.vpaths[i]}" type="checkbox"
value="${MSTREAMAPI.currentServer.vpaths[i]}" name="autodj-folders" onchange="onAutoDJFolderChange(this)">
<span>${MSTREAMAPI.currentServer.vpaths[i]}</span>
</label><br>`;
}
document.getElementById('filelist').innerHTML = newHtml;
}
////////////////// Layout
function setupLayoutPanel() {
setBrowserRootPanel('Layout', false);
@ -1583,15 +1853,27 @@ async function updateServer() {
try {
document.getElementById('save_server').disabled = true;
let host = document.getElementById('server_address').value;
if (host.slice(-1) !== '/') {
host += '/';
}
const res = await MSTREAMAPI.login(document.getElementById('server_username').value,
document.getElementById('server_password').value,
document.getElementById('server_address').value);
host);
MSTREAMAPI.currentServer.host = document.getElementById('server_address').value;
MSTREAMAPI.currentServer.host = host;
MSTREAMAPI.currentServer.username = document.getElementById('server_username').value;
MSTREAMAPI.currentServer.token = res.token;
myModal.close();
init();
loadFileExplorer();
localStorage.setItem('current-server', JSON.stringify(MSTREAMAPI.currentServer));
document.getElementById('server_password').value = '';
}catch(err) {
console.log(err)
boilerplateFailure(err);
}finally {
document.getElementById('save_server').disabled = false;
@ -1630,6 +1912,16 @@ function initElectron() {
<span>Edit Server</span>
</div>`;
try {
const curServer = JSON.parse(localStorage.getItem("current-server"));
console.log(curServer);
if (curServer.host && curServer.token) {
MSTREAMAPI.currentServer.host = curServer.host;
MSTREAMAPI.currentServer.token = curServer.token;
MSTREAMAPI.currentServer.username = curServer.username;
}
}catch(err) {}
// check if server
if (!MSTREAMAPI.currentServer.host) {
openEditModal();

View File

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

View File

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

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;
}
module.checkAuthAndKickToLogin = async () => {
// Send request to server
try {
await axios({
method: 'GET',
url: `${module.url()}/api/`,
headers: { 'x-access-token': module.token() }
});
} catch (err) {
window.location.href = `../login?redirect=${encodeURIComponent(window.location.pathname)}`;
}
}
module.logout = () => {
localStorage.removeItem('token');
Cookies.remove('x-access-token');
window.location.href = '../login';
document.location.assign(window.location.href.replace('/admin', '') + (window.location.href.slice(-1) === '/' ? '' : '/') + 'login');
}
module.goToPlayer = () => {
window.location.assign('../');
window.location.assign(window.location.href.replace('/admin', ''));
}
module.axios = axios.create({

View File

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

View File

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

View File

@ -34,8 +34,16 @@ var JUKEBOX = (function () {
// TODO: Check if websocket has already been created
// open connection
var l = window.location;
var wsLink = ((l.protocol === "https:") ? "wss://" : "ws://") + l.host + '?';
let wsLink = '';
if (MSTREAMAPI.currentServer.host) {
wsLink = MSTREAMAPI.currentServer.host;
wsLink = wsLink.replace('https://', 'wss://');
wsLink = wsLink.replace('http://', 'ws://');
wsLink += '?';
}else {
wsLink = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + '?';
}
if (accessKey) {
wsLink = wsLink + 'token=' + accessKey;
if (code) {

View File

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

View File

@ -42,7 +42,7 @@ const VUEPLAYERCORE = (() => {
if (!this.meta['album-art']) {
return '../assets/img/default.png';
}
return `../album-art/${this.meta['album-art']}?token=${MSTREAMPLAYER.getCurrentSong().authToken}`;
return `../album-art/${this.meta['album-art']}?compress=l&token=${MSTREAMPLAYER.getCurrentSong().authToken}`;
}
},
methods: {

BIN
webapp/build/icon.icns Normal file

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>
<!-- Save Playlist Modal -->
<div class="hystmodal" id="livePlaylist" aria-hidden="true">
<div class="hystmodal__wrap">
<div class="hystmodal__window" role="dialog" aria-modal="true">
<a data-hystclose class="hystmodal__close">Close</a>
<div class="hystmodal__styled">
<h5>Live Playlist Mode</h5>
<p>
Live playlist mode keeps your current queue synced to the server. For now this only works on the web app. It will be available on mobile soon.
</p>
<p>
Using live playlist on two devices at the same time might result in some odd behavior
</p>
<form action="javascript:setLivePlaylist()">
<label for="persist_live_queue">
<input type="checkbox" checked class="filled-in" id="persist_live_queue" name="persist_live_queue" />
<span>Start Live Playlist Automatically</span>
</label>
<br>
<div id="live-playlist-hide-these">
<p>
<label>
<input class="with-gap" name="group1" type="radio" checked />
<span>Make a new playlist</span>
</label>
<input id="new-live-playlist-name" type="text" name="live-queue-name" placeholder="Playlist Name" autocomplete="off" class="make-white">
</p>
<p>
<label>
<input class="with-gap" name="group1" type="radio" id="radio-use-existing" />
<span>Use an existing playlist</span>
</label>
</p>
<p>
<select id="live-playlist-select" class="browser-default">
<option value="" disabled selected>Select Playlist</option>
</select>
<!-- <label>Materialize Select</label> -->
</p>
</div>
<input id="set_live_playlist" type="submit" class="btn green" value="Enable Live Playlist">
</form>
</div>
</div>
</div>
</div>
<!-- Edit Server Modal -->
<div class="hystmodal" id="editServer" aria-hidden="true">
<div class="hystmodal__wrap">
@ -188,6 +239,29 @@
</div>
</div>
<!-- Metadata Model -->
<div class="hystmodal" id="metadataModel" aria-hidden="true">
<div class="hystmodal__wrap">
<div class="hystmodal__window" role="dialog" aria-modal="true">
<a data-hystclose class="hystmodal__close">Close</a>
<!-- You modal HTML markup -->
<div class="hystmodal__styled no-margin">
<h5>Metadata</h5>
<p><b>Title:</b> <span id="meta--title"></span></p>
<p><b>Album:</b> <span id="meta--album"></span></p>
<p><b>Artist:</b> <span id="meta--artist"></span></p>
<p><b>Year:</b> <span id="meta--year"></span></p>
<p><b>Track #:</b> <span id="meta--track"></span></p>
<p><b>Disc #:</b><span id="meta--disk"></span></p>
<p><b>Rating:</b> <span id="meta--rating"></span></p>
<p><b>Replay Gain:</b> <span id="meta--rg"></span></p>
<p><b>File Path:</b> <a href="#" target="_blank" id="meta--fp"></a></p>
<p><b>Album Art:</b> <a href="#" target="_blank" id="meta--aa"></a></p>
</div>
</div>
</div>
</div>
<div id="sidenav">
<div class="side-nav-header">
<?xml version="1.0" encoding="utf-8"?>
@ -248,7 +322,15 @@
</div>
<div onclick="changeView(getRecentlyAdded, this)" class="side-nav-item my-waves">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="22" height="22"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm61.8-104.4l-84.9-61.7c-3.1-2.3-4.9-5.9-4.9-9.7V116c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v141.7l66.8 48.6c5.4 3.9 6.5 11.4 2.6 16.8L334.6 349c-3.9 5.3-11.4 6.5-16.8 2.6z"/></svg>
<span>Recent</span>
<span>Recent Added</span>
</div>
<div onclick="changeView(getRecentlyPlayed, this)" class="side-nav-item my-waves">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="22" height="22"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm61.8-104.4l-84.9-61.7c-3.1-2.3-4.9-5.9-4.9-9.7V116c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v141.7l66.8 48.6c5.4 3.9 6.5 11.4 2.6 16.8L334.6 349c-3.9 5.3-11.4 6.5-16.8 2.6z"/></svg>
<span>Recently Played</span>
</div>
<div onclick="changeView(getMostPlayed, this)" class="side-nav-item my-waves">
<svg xmlns="http://www.w3.org/2000/svg" height="22px" viewBox="0 0 24 24" width="22px" fill="#FFFFFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M3.5 18.49l6-6.01 4 4L22 6.92l-1.41-1.41-7.09 7.97-4-4L2 16.99z"/></svg>
<span>Most Played</span>
</div>
<div class="side-nav-item my-waves" onclick="changeView(getRatedSongs, this)">
<?xml version="1.0" encoding="iso-8859-1"?><svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="-3 0 62 62" style="enable-background:new 0 0 53.867 53.867"><path style="fill:#efce4a" d="M26.934 1.318l8.322 16.864 18.611 2.705L40.4 34.013l3.179 18.536-16.645-8.751-16.646 8.751 3.179-18.536L0 20.887l18.611-2.705z"/></svg>
@ -281,6 +363,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24" fill="#FFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 3h8v8H3zm10 0h8v8h-8zM3 13h8v8H3zm15 0h-2v3h-3v2h3v3h2v-3h3v-2h-3z"/></svg>
<span>Layout</span>
</div>
<!-- <div class="side-nav-item my-waves" onclick="changeView(advancedConfig, this);">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24" fill="#FFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 3h8v8H3zm10 0h8v8h-8zM3 13h8v8H3zm15 0h-2v3h-3v2h3v3h2v-3h3v-2h-3z"/></svg>
<span>Config</span>
</div> -->
<div class="side-nav-header">
<span>Navigation</span>
</div>
@ -313,7 +399,7 @@
<div class="col s12 m4 l3 h100-res">
<div class="card aa-card">
<div v-if="altLayout.moveMeta === false" class="card-image hide-on-small-only">
<img :src="albumArtPath" />
<img class="pointer" :src="albumArtPath" v-on:click="getSongInfo" />
</div>
<div class="card-action">
@ -338,7 +424,7 @@
<div class="card-content">
<p>
<b>Title:</b>
{{ (meta.title) ? meta.title : '' }}
{{ (meta.title) ? meta.title : (meta.filepath ? meta.filepath.split('/').pop() : '') }}
</p>
<p>
<b>Artist:</b>
@ -360,7 +446,9 @@
</div>
<div class="flex">
<div v-if="altLayout.audioBookCtrls === false" class="button-block">
<svg class="pointer margin-lr" v-on:click="toggleRepeat" v-bind:class="{ 'aux-button-active': playerStats.shouldLoop }" xmlns="http://www.w3.org/2000/svg" version="1" viewBox="0 0 24 24" width="25" height="25"><path d="M17 2v3H6C4.3 5 3 6.3 3 8v6.813l2-1.626V8c0-.6.4-1 1-1h11v3l5-4-5-4zm4 7.188l-2 1.624V16c0 .6-.4 1-1 1H7v-3l-5 4 5 4v-3h11c1.7 0 3-1.3 3-3V9.187z"/></svg>
<svg v-if="playerStats.shouldLoopOne === false" class="pointer margin-lr" v-on:click="toggleRepeat" v-bind:class="{ 'aux-button-active': playerStats.shouldLoop }" xmlns="http://www.w3.org/2000/svg" version="1" viewBox="0 0 24 24" width="25" height="25"><path d="M17 2v3H6C4.3 5 3 6.3 3 8v6.813l2-1.626V8c0-.6.4-1 1-1h11v3l5-4-5-4zm4 7.188l-2 1.624V16c0 .6-.4 1-1 1H7v-3l-5 4 5 4v-3h11c1.7 0 3-1.3 3-3V9.187z"/></svg>
<svg v-else class="pointer margin-lr aux-button-active" v-on:click="toggleRepeat" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="25" height="25"><path d="M23.3 29.9V20.45H20.5V18H25.75V29.9ZM14 44 6 36 14 28 16.1 30.2 11.8 34.5H35V26.5H38V37.5H11.8L16.1 41.8ZM10 21.5V10.5H36.2L31.9 6.2L34 4L42 12L34 20L31.9 17.8L36.2 13.5H13V21.5Z"/></svg>
<svg class="pointer margin-lr" v-on:click="toggleShuffle" v-bind:class="{ 'aux-button-active': playerStats.shuffle }" xmlns="http://www.w3.org/2000/svg" version="1" viewBox="0 0 24 24" width="25" height="25"><path d="M17 2v3h-2.813c-1.1 0-2.187.588-2.687 1.688L6.594 16.5c-.1.3-.482.5-.782.5H2v2h3.813c1.1 0 2.187-.587 2.687-1.688L13.406 7.5c.1-.3.481-.5.781-.5H17v3l5-4-5-4zM2 5v2h3.813c.3 0 .675.194.875.594l1.718 3.312L9.5 8.687l-1-2C7.9 5.588 6.912 5 5.812 5H2zm9.594 8.094L10.5 15.313l1 2c.5 1 1.488 1.687 2.688 1.687H17v3l5-4-5-4v3h-2.813c-.3 0-.675-.194-.874-.594l-1.72-3.312z"/></svg>
<svg class="auto-dj" v-on:click="toggleAutoDJ" v-bind:class="{ 'aux-button-active': playerStats.autoDJ }" class="center" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" style="width:25px;webkit-logical-width:25px;webkit-logical-height:25px;user-select:none;transform-origin:12.5px 12.5px;r:0;perspective-origin:12.5px 12.5px;overflow-y:hidden;overflow-x:hidden;inline-size:25px;height:25px;d:none;block-size:25px;background:0% 0%/auto padding-box border-box" overflow="hidden" display="block" ><g transform="translate(-.938 19.85)" style="y:0;user-select:none;transform:matrix(1,0,0,1,-.9375,19.85);perspective-origin:0 0"><g class="ld" style="y:0;x:0;user-select:none;transform-origin:7.9375px 0;transform:none;r:0;perspective-origin:0 0;line-height:31.4286px;font:400 22px/31.4286px 'Varela Round','century gothic',verdana;d:none"><text style="y:0;user-select:none;r:0;perspective-origin:7.9375px 0;line-height:31.4286px;font:400 22px/31.4286px Arial" white-space="nowrap" display="block">D</text></g></g><g transform="translate(14.938 19.85)" style="y:0;user-select:none;transform:matrix(1,0,0,1,14.9375,19.85);perspective-origin:0 0"><g class="ld" style="y:0;x:0;user-select:none;transform-origin:5.5px 0;transform:none;r:0;perspective-origin:0 0;line-height:31.4286px;font:400 22px/31.4286px 'Varela Round','century gothic',verdana;d:none"><text style="y:0;user-select:none;r:0;perspective-origin:5.5px 0;line-height:31.4286px;font:400 22px/31.4286px Arial" white-space="nowrap" display="block">J</text></g></g></svg>
</div>
@ -438,6 +526,10 @@
<h5>Now Playing</h5>
</div>
<div class="flex grow flex-end" id="playlist-buttons">
<a title="Live Playlist" onclick="openLivePlaylistModal();">
<svg xmlns="http://www.w3.org/2000/svg" v-bind:class="{ 'aux-button-active': livePlaylist.name }" xml:space="preserve" width="25" height="25" viewBox="0 0 512 512"><path d="M48 64h160c17.672 0 32-14.328 32-32 0-17.674-14.328-32-32-32H48C30.328 0 16 14.326 16 32c0 17.672 14.328 32 32 32zM48 160h160c17.672 0 32-14.328 32-32 0-17.674-14.328-32-32-32H48c-17.672 0-32 14.326-32 32 0 17.672 14.328 32 32 32zM240 224c0-17.674-14.328-32-32-32H48c-17.672 0-32 14.326-32 32 0 17.672 14.328 32 32 32h160c17.672 0 32-14.328 32-32z"/><path d="M411.328 75.914C393.043 61.805 368 42.477 368 32c0-17.672-14.328-32-32-32s-32 14.328-32 32v293.58c-10.023-3.549-20.762-5.58-32-5.58-53.02 0-96 42.98-96 96s42.98 96 96 96 96-42.98 96-96V123.293c1.414 1.094 2.82 2.203 4.23 3.293 36.105 27.852 59.77 48.078 59.77 74.305 0 40.766-21.684 63.516-22.305 64.164-12.672 12.32-12.961 32.578-.641 45.25 6.273 6.453 14.605 9.695 22.949 9.695 8.035 0 16.082-3.008 22.301-9.055 4.27-4.148 41.695-42.484 41.695-110.055.001-59.64-46.948-95.867-84.671-124.976z"/></svg>
</a>
<a title="Save Playlist" onclick="openSaveModal();">
<?xml version="1.0" encoding="iso-8859-1"?><svg width="27" height="27" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 49 49" style="enable-background:new 0 0 49 49"><path d="M27.5 5h6v10h-6z"/><path d="M39.914 0H.5v49h48V8.586L39.914 0zM10.5 2h26v16h-26V2zm29 45h-31V26h31v21z"/><path d="M13.5 32h7a1 1 0 1 0 0-2h-7a1 1 0 1 0 0 2zM13.5 36h10a1 1 0 1 0 0-2h-10a1 1 0 1 0 0 2zM26.5 36c.27 0 .52-.11.71-.29.18-.19.29-.45.29-.71s-.11-.521-.29-.71c-.37-.37-1.04-.37-1.41 0a.996.996 0 0 0-.3.71c0 .27.109.52.29.71.189.18.439.29.71.29z"/></svg>
</a>
@ -447,7 +539,7 @@
<a title="Share Playlist" onclick="openShareModal();">
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 50 50"><path d="M 40 0 C 34.536 0 30.079 4.39775 30 9.84375 C 29.999 9.89275 30 9.951 30 10 C 30 13.688 31.99675 16.891 34.96875 18.625 C 36.44575 19.49 38.167 20 40 20 C 45.514 20 50 15.514 50 10 C 50 4.486 45.514 0 40 0 z M 28.0625 10.84375 L 17.84375 15.96875 C 20.22275 18.03275 21.78375 20.99975 21.96875 24.34375 L 32.3125 19.15625 C 29.8965 17.12825 28.3005 14.17675 28.0625 10.84375 z M 10 15 C 4.486 15 0 19.486 0 25 C 0 30.514 4.486 35 10 35 C 12.051 35 13.94325 34.3765 15.53125 33.3125 C 18.21425 31.5205 20 28.471 20 25 C 20 21.411 18.09 18.264 15.25 16.5 C 13.72 15.546 11.931 15 10 15 z M 21.96875 25.65625 C 21.78475 28.99625 20.249 31.9975 17.875 34.0625 L 28.0625 39.15625 C 28.2995 35.82325 29.86925 32.87375 32.28125 30.84375 L 21.96875 25.65625 z M 40 30 C 37.937 30 36.0305 30.64475 34.4375 31.71875 C 31.7705 33.51475 30 36.543 30 40 C 30 40.015 30 40.01725 30 40.03125 C 29.956 40.03425 29.919 40.0595 29.875 40.0625 L 30 40.125 C 30.066 45.583 34.527 50 40 50 C 45.514 50 50 45.514 50 40 C 50 34.486 45.514 30 40 30 z"/></svg>
</a>
<a title="Clear Playlist" onclick="MSTREAMPLAYER.clearPlaylist();">
<a title="Clear Playlist" onclick="VUEPLAYERCORE.clearQueue();">
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="11 11 26 26" style="enable-background:new 0 0 48 48"><g fill="#C33"><path d="M32.662 15.338c-4.784-4.784-12.54-4.784-17.324 0s-4.784 12.54 0 17.324 12.54 4.784 17.324 0c4.783-4.784 4.784-12.54 0-17.324zm-5.127 14.318a.5.5 0 0 1-.707 0L24 26.828l-2.828 2.828a.5.5 0 0 1-.707 0l-2.121-2.121a.5.5 0 0 1 0-.707L21.172 24l-2.828-2.828a.5.5 0 0 1 0-.707l2.121-2.121a.5.5 0 0 1 .707 0L24 21.172l2.828-2.828a.5.5 0 0 1 .707 0l2.121 2.121a.5.5 0 0 1 0 .707L26.828 24l2.828 2.828a.5.5 0 0 1 0 .707l-2.121 2.121z"/></g></svg>
</a>
</div>
@ -456,12 +548,12 @@
<div v-if="altLayout.moveMeta === true" style="background-color: #1a1a1a !important;" class="card card-mod data-card hide-on-small-only">
<div class="card-content flex">
<div class="custom-card-img">
<img :src="albumArtPath" class="block" />
<img :src="albumArtPath" class="block pointer" v-on:click="getSongInfo" />
</div>
<div class="card-meta">
<p>
<b>Title:</b>
{{ (meta.title) ? meta.title : '' }}
{{ (meta.title) ? meta.title : (meta.filepath ? meta.filepath.split('/').pop() : '') }}
</p>
<p>
<b>Artist:</b>
@ -484,6 +576,7 @@
<a v-show="showClear.val === true" v-on:click="clearRating">clear</a>
</div>
<div style="visibility:hidden; position: absolute;" class="pop-d" id="pop-d" role="tooltip">
<div class="pop-list-item" v-on:click="gsi2" >Song Info</div>
<div class="pop-d pop-playlist">Add To Playlist:</div>
<div v-for="(playlist, index) in playlists" is="popper-playlist-item" :key="index" :index="index" :playlist="playlist"></div>
</div>

View File

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

View File

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

View File

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

View File

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