Compare commits

..

No commits in common. "master" and "v5.9.1" have entirely different histories.

59 changed files with 381 additions and 9991 deletions

View File

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

View File

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

1
.gitignore vendored
View File

@ -21,6 +21,7 @@ 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,33 +28,32 @@ Main|Shared|Admin
## Installing mStream
* [Docker Instructions](https://github.com/linuxserver/docker-mstream)
* [Binaries for Win/OSX/Linux](https://mstream.io/server)
* [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
* [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
# Install dependencies and run
npm run-script wizard
# Boot mStream
node cli-boot-wrapper.js
```
## 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

1
bin/rpn/README.md Normal file
View File

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

BIN
bin/rpn/rpn-android64 Normal file

Binary file not shown.

BIN
bin/rpn/rpn-linux Normal file

Binary file not shown.

BIN
bin/rpn/rpn-osx Normal file

Binary file not shown.

BIN
bin/rpn/rpn-win.exe Normal file

Binary file not shown.

View File

@ -4,29 +4,27 @@
// Check if we are in an electron environment
if (process.versions["electron"]) {
// off to a separate electron boot environment
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);
return require("./build/electron");
}
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 dependencies and run
npm run-script wizard
# Install without dev dependencies
npm install --production
```
# 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 --only=prod
npm install --production
# Reboot mStream with PM2
pm2 restart all
```

8107
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -8,7 +8,6 @@ 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');
@ -108,16 +107,6 @@ 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()
@ -148,16 +137,6 @@ 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));
@ -174,17 +153,11 @@ 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),
isAudioBooks: Joi.boolean().default(false)
autoAccess: Joi.boolean().default(false)
});
const input = joiValidate(schema, req.body);
await admin.addDirectory(
input.value.directory,
input.value.vpath,
input.value.autoAccess,
input.value.isAudioBooks,
mstream);
await admin.addDirectory(input.value.directory, input.value.vpath, input.value.autoAccess, mstream);
res.json({});
try {
@ -221,10 +194,6 @@ 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();
@ -307,21 +276,10 @@ exports.setup = (mstream) => {
writeLogs: config.program.writeLogs,
secret: config.program.secret.slice(-4),
ssl: config.program.ssl,
storage: config.program.storage,
maxRequestSize: config.program.maxRequestSize
storage: config.program.storage
});
});
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,7 +22,6 @@ 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,23 +40,18 @@ 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, ignoreVPaths) {
function renderOrClause(vpaths) {
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 } })
}
@ -79,53 +74,28 @@ exports.setup = (mstream) => {
});
mstream.post('/api/v1/db/metadata', (req, res) => {
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 pathInfo = vpath.getVPathInfo(req.body.filepath, req.user);
if (!db.getFileCollection()) { return res.json({ "filepath": req.body.filepath, "metadata": {} }); }
const leftFun = (leftData) => {
return leftData.hash + '-' + user.username;
return leftData.hash + '-' + req.user.username;
};
const result = db.getFileCollection().chain().find({ '$and': [{'filepath': pathInfo.relativePath}, {'vpath': pathInfo.vpath}] }, true)
.eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data();
if (!result || !result[0]) {
return { "filepath": filepath, "metadata": null };
return res.json({ "filepath": req.body.filepath, "metadata": {} });
}
return renderMetadataObj(result[0]);
}
res.json(renderMetadataObj(result[0]));
});
// legacy enpoint, moved to POST
mstream.get('/api/v1/db/artists', (req, res) => {
const artists = getArtists(req);
res.json(artists);
});
mstream.post('/api/v1/db/artists', (req, res) => {
const artists = getArtists(req);
res.json(artists);
});
function getArtists(req) {
const artists = { "artists": [] };
if (!db.getFileCollection()) { res.json(artists); }
const results = db.getFileCollection().find(renderOrClause(req.user.vpaths, req.body.ignoreVPaths));
const results = db.getFileCollection().find(renderOrClause(req.user.vpaths));
const store = {};
for (let row of results) {
if (!store[row.artist] && !(row.artist === undefined || row.artist === null)) {
@ -137,8 +107,8 @@ exports.setup = (mstream) => {
return a.localeCompare(b);
});
return artists;
}
res.json(artists);
});
mstream.post('/api/v1/db/artists-albums', (req, res) => {
const albums = { "albums": [] };
@ -146,7 +116,7 @@ exports.setup = (mstream) => {
const results = db.getFileCollection().chain().find({
'$and': [
renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
renderOrClause(req.user.vpaths),
{'artist': { '$eq': String(req.body.artist) }}
]
}).simplesort('year', true).data();
@ -176,20 +146,10 @@ 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, req.body.ignoreVPaths));
const results = db.getFileCollection().find(renderOrClause(req.user.vpaths));
const store = {};
for (let row of results) {
if (store[`${row.album}${row.year}`] || (row.album === undefined || row.album === null)) {
@ -204,14 +164,14 @@ exports.setup = (mstream) => {
return a.name.localeCompare(b.name);
});
return albums;
}
res.json(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, req.body.ignoreVPaths),
renderOrClause(req.user.vpaths),
{'album': { '$eq': req.body.album ? String(req.body.album) : null }}
];
@ -245,7 +205,6 @@ 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);
@ -267,7 +226,7 @@ exports.setup = (mstream) => {
const findThis = {
'$and': [
renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
renderOrClause(req.user.vpaths),
{[searchCol]: {'$regex': [escapeStringRegexp(String(req.body.search)), 'i']}}
]
};
@ -300,16 +259,6 @@ 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) => {
@ -338,7 +287,7 @@ exports.setup = (mstream) => {
const results = db.getUserMetadataCollection().chain().eqJoin(db.getFileCollection().chain(), leftFun, rightFun, mapFun).find({
'$and': [
renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
renderOrClause(req.user.vpaths),
{ 'rating': { '$gt': 0 } }
]
}).simplesort('rating', true).data();
@ -347,9 +296,8 @@ exports.setup = (mstream) => {
for (const row of results) {
songs.push(renderMetadataObj(row));
}
return songs;
}
res.json(songs);
});
mstream.post('/api/v1/db/rate-song', (req, res) => {
const schema = Joi.object({
@ -381,10 +329,7 @@ exports.setup = (mstream) => {
});
mstream.post('/api/v1/db/recent/added', (req, res) => {
const schema = Joi.object({
limit: Joi.number().integer().min(1).required(),
ignoreVPaths: Joi.array().items(Joi.string()).optional()
});
const schema = Joi.object({ limit: Joi.number().integer().min(1).required() });
joiValidate(schema, req.body);
if (!db.getFileCollection()) { throw new Error('DB Not Ready'); }
@ -395,7 +340,7 @@ exports.setup = (mstream) => {
const results = db.getFileCollection().chain().find({
'$and': [
renderOrClause(req.user.vpaths, req.body.ignoreVPaths),
renderOrClause(req.user.vpaths),
{ 'ts': { '$gt': 0 } }
]
}).simplesort('ts', true).limit(req.body.limit).eqJoin(db.getUserMetadataCollection().chain(), leftFun, rightFunDefault, mapFunDefault).data();
@ -408,106 +353,6 @@ 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'); };
@ -524,7 +369,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.includes(vpath)) {
if (req.body.ignoreVPaths && typeof req.body.ignoreVPaths === 'object' && req.body.ignoreVPaths[vpath] === true) {
continue;
}
orClause['$or'].push({ 'vpath': { '$eq': vpath } });
@ -613,8 +458,4 @@ 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,8 +16,7 @@ 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),
pullMetadata: Joi.boolean().default(false)
sort: Joi.boolean().default(true)
});
const { value } = joiValidate(schema, req.body);
@ -49,7 +48,7 @@ exports.setup = (mstream) => {
}
// get directory contents
const folderContents = await fileExplorer.getDirectoryContents(pathInfo.fullPath, config.program.supportedAudioFiles, value.sort, value.pullMetadata, value.directory, req.user);
const folderContents = await fileExplorer.getDirectoryContents(pathInfo.fullPath, config.program.supportedAudioFiles, value.sort);
// Format directory string for return value
let returnDirectory = path.join(pathInfo.vpath, pathInfo.relativePath);
@ -107,16 +106,15 @@ exports.setup = (mstream) => {
const pathInfo = vpath.getVPathInfo(decodeURI(req.headers['data-location']), req.user);
mkdirp.sync(pathInfo.fullPath);
const bb = busboy({ headers: req.headers });
bb.on('file', (fieldname, file, info) => {
const { filename } = info;
const busboy = new Busboy({ headers: req.headers });
busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
const saveTo = path.join(pathInfo.fullPath, filename);
winston.info(`Uploading from ${req.user.username} to: ${saveTo}`);
file.pipe(fsOld.createWriteStream(saveTo));
});
bb.on('close', () => { res.json({}); });
req.pipe(bb);
busboy.on('finish', () => { res.json({}); });
req.pipe(busboy);
});
mstream.post("/api/v1/file-explorer/m3u", async (req, res) => {

View File

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

@ -1,35 +0,0 @@
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

@ -1,53 +0,0 @@
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,8 +4,6 @@ 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
@ -30,7 +28,6 @@ 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()
@ -62,8 +59,6 @@ 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`,
@ -171,32 +166,22 @@ async function parseFile(thisSong, modified) {
function calculateHash(filepath) {
return new Promise((resolve, reject) => {
try {
const hash = crypto.createHash('md5').setEncoding('hex');
const fileStream = fs.createReadStream(filepath);
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);
}
fileStream.on('end', () => {
hash.end();
fileStream.close();
resolve(hash.read());
});
fileStream.pipe(hash);
});
}
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
@ -206,23 +191,10 @@ 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 {
originalFileBuffer = await checkDirectoryForAlbumArt(songInfo);
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 = {};
@ -270,7 +242,6 @@ async function checkDirectoryForAlbumArt(songInfo) {
let imageBuffer;
let picFormat;
let newFileFlag = false;
// Search for a named file
for (var i = 0; i < imageArray.length; i++) {
@ -294,12 +265,9 @@ 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) {

View File

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

View File

@ -1,7 +1,6 @@
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');
@ -64,7 +63,7 @@ exports.serveIt = async configFile => {
// Magic Middleware Things
mstream.use(cookieParser());
mstream.use(express.json({ limit: config.program.maxRequestSize }));
mstream.use(express.json());
mstream.use(express.urlencoded({ extended: true }));
mstream.use((req, res, next) => { // CORS
res.header("Access-Control-Allow-Origin", "*");
@ -83,7 +82,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(302, req.path.slice(0, (matchEnd[0].length - 1)*-1) + queryString);
return res.redirect(301, req.path.slice(0, (matchEnd[0].length - 1)*-1) + queryString);
}
next();
});
@ -91,16 +90,7 @@ 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>'); }
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');
}
next();
});
mstream.get('/admin/index.html', (req, res, next) => {
@ -117,18 +107,18 @@ exports.serveIt = async configFile => {
jwt.verify(req.cookies['x-access-token'], config.program.secret);
next();
} catch(err) {
return res.redirect(302, '/login');
return res.redirect(301, 'login');
}
});
mstream.get('/login', (req, res, next) => {
if (Object.keys(config.program.users).length === 0){
return res.redirect(302, '..');
return res.redirect(301, '..');
}
try {
jwt.verify(req.cookies['x-access-token'], config.program.secret);
return res.redirect(302, '..');
return res.redirect(301, '..');
} catch(err) {
next();
}
@ -161,15 +151,7 @@ exports.serveIt = async configFile => {
mstream.get('/api/', (req, res) => res.json({ "server": require('../package.json').version, "apiVersions": ["1"] }));
// album art folder
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));
});
mstream.use('/album-art', express.static(config.program.storage.albumArtDirectory));
// TODO: determine if user has access to the exact file
// mstream.all('/media/*', (req, res, next) => {

View File

@ -17,9 +17,7 @@ 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),
compressImage: Joi.boolean().default(true),
newScan: Joi.boolean().default(false)
maxConcurrentTasks: Joi.number().integer().min(1).default(1)
});
const dbOptions = Joi.object({
@ -27,7 +25,7 @@ const dbOptions = Joi.object({
});
const transcodeOptions = Joi.object({
algorithm: Joi.string().valid(...getTransAlgos()).default('stream'),
algorithm: Joi.string().valid(...getTransAlgos()).default('buffer'),
enabled: Joi.boolean().default(false),
ffmpegDirectory: Joi.string().default(path.join(__dirname, '../../bin/ffmpeg')),
defaultCodec: Joi.string().valid(...getTransCodecs()).default('opus'),
@ -55,7 +53,7 @@ const federationOptions = Joi.object({
});
const schema = Joi.object({
address: Joi.string().ip({ cidr: 'forbidden' }).default('::'),
address: Joi.string().ip({ cidr: 'forbidden' }).default('0.0.0.0'),
port: Joi.number().default(3000),
supportedAudioFiles: Joi.object().pattern(
Joi.string(), Joi.boolean()
@ -74,13 +72,12 @@ 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', 'audio-books').default('music'),
type: Joi.string().valid('music', 'audiobook').default('music'),
})
).default({}),
users: Joi.object().pattern(

View File

@ -4,14 +4,13 @@ const nanoid = require('nanoid')
const winston = require('winston');
const path = require('path');
const { spawn } = require('child_process');
const { XMLParser } = require("fast-xml-parser");
const parser = 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, isAudioBooks, mstream) => {
exports.addDirectory = async (directory, vpath, autoAccess, mstream) => {
// confirm directory is real
const stat = await fs.stat(directory);
if (!stat.isDirectory()) { throw `${directory} is not a directory` };
@ -30,7 +30,6 @@ exports.addDirectory = async (directory, vpath, autoAccess, isAudioBooks, mstrea
// 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);
@ -45,7 +44,7 @@ exports.addDirectory = async (directory, vpath, autoAccess, isAudioBooks, mstrea
await this.saveFile(loadConfig, config.configFile);
// add directory to program
config.program.folders[vpath] = memClone[vpath];
config.program.folders[vpath] = { root: directory };
if (autoAccess === true) {
Object.values(config.program.users).forEach(user => {
@ -188,17 +187,6 @@ 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;
@ -254,15 +242,6 @@ 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 = {}; }
@ -290,15 +269,6 @@ 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;
@ -358,14 +328,12 @@ 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 = memClone;
loadConfig.federation.enabled = val;
await this.saveFile(loadConfig, config.configFile);
config.program.federation.enabled = val;
syncthing.setup();
}
@ -390,12 +358,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,8 +23,7 @@ function isAsync(fn) {
function wrapAsync(fn) {
return (req, res, next = noop) => {
fn(req, res, next)
.catch((err) => {
next(err);
});
.then(() => !res.finished && next())
.catch(next);
}
};

View File

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

View File

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

View File

@ -262,10 +262,6 @@ 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...'}}
@ -349,8 +345,7 @@ const foldersView = Vue.component('folders-view', {
data: {
directory: this.folder.value,
vpath: this.dirName,
autoAccess: document.getElementById('folder-auto-access').checked,
isAudioBooks: document.getElementById('folder-is-audiobooks').checked
autoAccess: document.getElementById('folder-auto-access').checked
}
});
@ -450,7 +445,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()" v-model="newUsername" id="new-username" required type="text" class="validate">
<input @blur="maybeResetForm()" pattern="[a-zA-Z0-9-]+" 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">
@ -630,7 +625,7 @@ const usersView = Vue.component('users-view', {
title: 'You will be taken the login page',
position: 'center',
buttons: [['<button>Go!</button>', (instance, toast) => {
API.logout();
API.checkAuthAndKickToLogin();
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
}, true]],
});
@ -701,12 +696,6 @@ 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>
@ -828,7 +817,7 @@ const advancedView = Vue.component('advanced-view', {
url: `${API.url()}/api/v1/admin/config/secret`,
data: { strength: 128 }
}).then(() => {
API.logout();
API.checkAuthAndKickToLogin();
}).catch(() => {
iziToast.error({
title: 'Failed',
@ -941,19 +930,6 @@ 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>
@ -1011,7 +987,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>{{new Date(v.expires * 1000).toLocaleString()}}</th>
<th>{{v.expires}}</th>
<th>[<a v-on:click="deletePlaylist(v)">delete</a>]</th>
</tr>
</tbody>
@ -1163,144 +1139,6 @@ 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,
@ -2533,74 +2371,6 @@ 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 {
@ -3444,7 +3214,6 @@ 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,12 +39,8 @@ const MSTREAMAPI = (() => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/file-explorer/recursive', { directory: directory });
}
mstreamModule.savePlaylist = (title, songs, live) => {
const postData = { title: title, songs: songs };
if (live !== undefined) {
postData.live = live;
}
return req('POST', mstreamModule.currentServer.host + 'api/v1/playlist/save', postData);
mstreamModule.savePlaylist = (title, songs) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/playlist/save', { title: title, songs: songs });
}
mstreamModule.newPlaylist = (title) => {
@ -75,20 +71,20 @@ const MSTREAMAPI = (() => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/db/search', postObject);
}
mstreamModule.artists = (postObject) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/db/artists', postObject);
mstreamModule.artists = () => {
return req('GET', mstreamModule.currentServer.host + 'api/v1/db/artists', false);
}
mstreamModule.albums = (postObject) => {
return req('POST', mstreamModule.currentServer.host + 'api/v1/db/albums', postObject);
mstreamModule.albums = () => {
return req('GET', mstreamModule.currentServer.host + 'api/v1/db/albums', false);
}
mstreamModule.artistAlbums = (postObject) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/artists-albums", postObject);
mstreamModule.artistAlbums = (artist) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/artists-albums", { artist: artist });
}
mstreamModule.albumSongs = (postObject) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/album-songs", postObject);
mstreamModule.albumSongs = (album, artist, year) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/album-songs", { album, artist, year });
}
mstreamModule.dbStatus = () => {
@ -103,20 +99,12 @@ const MSTREAMAPI = (() => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/rate-song", { filepath: filepath, rating: rating });
}
mstreamModule.getRated = (postObject) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/rated", postObject);
mstreamModule.getRated = () => {
return req('GET', mstreamModule.currentServer.host + "api/v1/db/rated", false);
}
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.getRecentlyAdded = (limit) => {
return req('POST', mstreamModule.currentServer.host + "api/v1/db/recent/added", { limit: limit });
}
mstreamModule.lookupMetadata = (filepath) => {
@ -132,10 +120,6 @@ 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 });
@ -148,7 +132,7 @@ const MSTREAMAPI = (() => {
mstreamModule.logout = () => {
localStorage.removeItem('token');
Cookies.remove('x-access-token');
document.location.assign(window.location.href + (window.location.href.slice(-1) === '/' ? '' : '/') + 'login');
window.location.href = './login';
}
return mstreamModule;

View File

@ -90,6 +90,13 @@ 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 = [];
@ -121,7 +128,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}?compress=s&token=${MSTREAMAPI.currentServer.token}">`:
`<img class="album-art-box" loading="lazy" src="${MSTREAMAPI.currentServer.host}album-art/${albumArtFile}?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>
@ -137,7 +144,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']}?compress=s&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']}?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>` : ''}
@ -157,9 +164,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 !== undefined ? `<b>` : ''}
${subtitle ? `<b>` : ''}
<span class="${aa ? '' : 'item-text'}">${rating ? `[${rating}] ` : ''}${title}</span>
${subtitle !== undefined ? `</b><br><span>${subtitle}</span>` : ''}
${subtitle ? `</b><br><span>${subtitle}</span>` : ''}
</span>
</div>
<div class="song-button-box">
@ -423,7 +430,6 @@ 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) {
@ -457,27 +463,6 @@ 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();
}
@ -552,10 +537,6 @@ function openSaveModal() {
myModal.open('#savePlaylist');
}
function openLivePlaylistModal() {
myModal.open('#livePlaylist');
}
function openNewPlaylistModal() {
myModal.open('#newPlaylist');
}
@ -564,38 +545,7 @@ 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');
}
@ -674,6 +624,42 @@ 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');
@ -734,7 +720,6 @@ 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">';
@ -744,7 +729,6 @@ 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>'
@ -854,7 +838,6 @@ 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();
@ -865,80 +848,6 @@ 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({
@ -974,11 +883,8 @@ 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;
}
}
@ -989,12 +895,7 @@ async function getAllArtists() {
programState = [{ state: 'allArtists' }];
try {
const response = await MSTREAMAPI.artists({
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
})
});
const response = await MSTREAMAPI.artists();
// parse through the json array and make an array of corresponding divs
let artists = '<ul class="collection">';
response.artists.forEach(value => {
@ -1028,13 +929,7 @@ async function getArtistsAlbums(artist) {
document.getElementById('filelist').innerHTML = getLoadingSvg();
try {
const response = await MSTREAMAPI.artistAlbums({
artist: artist,
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
})
});
const response = await MSTREAMAPI.artistAlbums(artist);
let albums = '<ul>';
response.albums.forEach(value => {
const albumString = value.name ? value.name : 'SINGLES';
@ -1061,12 +956,7 @@ async function getAllAlbums() {
programState = [{ state: 'allAlbums' }];
try {
const response = await MSTREAMAPI.albums({
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
})
});
const response = await MSTREAMAPI.albums();
//parse through the json array and make an array of corresponding divs
let albums = '<ul class="collection">';
response.albums.forEach(value => {
@ -1111,15 +1001,7 @@ async function getAlbumSongs(album, artist, year) {
document.getElementById('localSearchBar').value = '';
try {
const response = await MSTREAMAPI.albumSongs({
album,
artist,
year,
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
})
});
const response = await MSTREAMAPI.albumSongs(album, artist, year)
//parse through the json array and make an array of corresponding divs
let files = '<ul class="collection">';
response.forEach(song => {
@ -1142,11 +1024,7 @@ async function getRatedSongs() {
programState = [{ state: 'allRated' }];
try {
const response = await MSTREAMAPI.getRated({
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
})
});
const response = await MSTREAMAPI.getRated();
//parse through the json array and make an array of corresponding divs
let files = '';
response.forEach(value => {
@ -1162,10 +1040,10 @@ async function getRatedSongs() {
});
files += createMusicFileHtml(value.filepath,
value.metadata.title ? value.metadata.title : value.filepath.split('/').pop(),
value.metadata['album-art'] ? `src="${MSTREAMAPI.currentServer.host}album-art/${value.metadata['album-art']}?compress=s&token=${MSTREAMAPI.currentServer.token}"` : `src="assets/img/default.png"`,
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"`,
rating,
value.metadata.artist ? `<span style="font-size:15px;">${value.metadata.artist}</span>` : '');
value.metadata.artist ? `<span style="font-size:15px;">${value.metadata.artist}</span>` : undefined);
});
document.getElementById('filelist').innerHTML = files;
@ -1175,106 +1053,6 @@ 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');
@ -1289,12 +1067,7 @@ async function redoRecentlyAdded() {
programState = [{ state: 'recentlyAdded'}];
try {
const response = await MSTREAMAPI.getRecentlyAdded(
document.getElementById('recently-added-limit').value,
Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
}));
const response = await MSTREAMAPI.getRecentlyAdded(document.getElementById('recently-added-limit').value);
//parse through the json array and make an array of corresponding divs
let filelist = '<ul class="collection">';
response.forEach(el => {
@ -1305,9 +1078,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']}?compress=s&token=${MSTREAMAPI.currentServer.token}"` : `src="assets/img/default.png"`,
el.metadata['album-art'] ? `src="${MSTREAMAPI.currentServer.host}album-art/${el.metadata['album-art']}?token=${MSTREAMAPI.currentServer.token}"` : `src="assets/img/default.png"`,
undefined,
el.metadata.artist ? `<span style="font-size:15px;">${el.metadata.artist}</span>` : '');
el.metadata.artist ? `<span style="font-size:15px;">${el.metadata.artist}</span>` : undefined);
});
filelist += '</ul>'
@ -1428,13 +1201,8 @@ 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>`;
@ -1546,14 +1314,7 @@ function createJukeboxPanel() {
return '<div class="pad-6">An error occurred. Please refresh the page and try again</div>';
}
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}`;
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>
@ -1694,13 +1455,7 @@ 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>'
const postObject = {
search: document.getElementById('search-term').value,
ignoreVPaths: Object.keys(MSTREAMPLAYER.ignoreVPaths).filter((vpath) => {
return MSTREAMPLAYER.ignoreVPaths[vpath] === true;
})
};
var postObject = { search: document.getElementById('search-term').value};
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; }
@ -1755,31 +1510,6 @@ 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);
@ -1853,27 +1583,15 @@ 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,
host);
document.getElementById('server_address').value);
MSTREAMAPI.currentServer.host = host;
MSTREAMAPI.currentServer.host = document.getElementById('server_address').value;
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;
@ -1912,16 +1630,6 @@ 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,12 +997,4 @@ select {
.mobile-links img {
width: 100%;
}
.no-margin p {
margin: 0;
}
.make-white {
color: #FFF;
}

View File

@ -1,10 +1,6 @@
const VUEPLAYERCORE = (() => {
const mstreamModule = {};
mstreamModule.livePlaylist = {
name: false
};
mstreamModule.altLayout = {
'moveMeta': false,
'audioBookCtrls': false,
@ -90,24 +86,17 @@ const VUEPLAYERCORE = (() => {
playlists: mstreamModule.playlists,
showClear: showClearLink,
altLayout: mstreamModule.altLayout,
meta: MSTREAMPLAYER.playerStats.metadata,
livePlaylist: mstreamModule.livePlaylist
meta: MSTREAMPLAYER.playerStats.metadata
},
computed: {
albumArtPath: function () {
if (!this.meta['album-art']) {
return 'assets/img/default.png';
}
return MSTREAMAPI.currentServer.host + `album-art/${this.meta['album-art']}?compress=l&token=${MSTREAMPLAYER.getCurrentSong().authToken}`;
return MSTREAMAPI.currentServer.host + `album-art/${this.meta['album-art']}?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);
@ -122,13 +111,6 @@ 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 {
@ -188,13 +170,6 @@ 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");
@ -394,13 +369,10 @@ const VUEPLAYERCORE = (() => {
if (!this.meta['album-art']) {
return 'assets/img/default.png';
}
return MSTREAMAPI.currentServer.host + `album-art/${this.meta['album-art']}?compress=l&token=${MSTREAMPLAYER.getCurrentSong().authToken}`;
return MSTREAMAPI.currentServer.host + `album-art/${this.meta['album-art']}?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.
@ -534,12 +506,11 @@ const VUEPLAYERCORE = (() => {
}
});
mstreamModule.addSongWizard = async (filepath, metadata, lookupMetadata, position, livePlaylist, autoPlayOff) => {
mstreamModule.addSongWizard = async (filepath, metadata, lookupMetadata, position) => {
// Escape filepath
const rawFilepath = filepath;
var rawFilepath = filepath;
filepath = filepath.replace(/\%/g, "%25");
filepath = filepath.replace(/\#/g, "%23");
filepath = filepath.replace(/\?/g, "%3F");
if (filepath.charAt(0) === '/') {
filepath = filepath.substr(1);
}
@ -564,18 +535,8 @@ 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, autoPlayOff);
if (mstreamModule.livePlaylist.name && livePlaylist !== false) {
await MSTREAMAPI.addToPlaylist(mstreamModule.livePlaylist.name, newSong.filepath);
}
MSTREAMPLAYER.addSong(newSong);
}
// perform lookup
@ -589,16 +550,5 @@ 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.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -22,14 +22,27 @@ 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');
document.location.assign(window.location.href.replace('/admin', '') + (window.location.href.slice(-1) === '/' ? '' : '/') + 'login');
window.location.href = '../login';
}
module.goToPlayer = () => {
window.location.assign(window.location.href.replace('/admin', ''));
window.location.assign('../');
}
module.axios = axios.create({

View File

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

View File

@ -40,11 +40,9 @@ const MSTREAMPLAYER = (() => {
// Scrobble function
// This is a placeholder function that the API layer can take hold of to implement the scrobble call
let scrobbleTimer;
mstreamModule.scrobble = () => {
MSTREAMAPI.scrobbleByFilePath(
mstreamModule.getCurrentSong().rawFilePath,
(response, error) => {});
var scrobbleTimer;
mstreamModule.scrobble = function () {
return false;
}
// The audioData looks like this
@ -73,9 +71,7 @@ const MSTREAMPLAYER = (() => {
const params = {
ignoreList: autoDjIgnoreArray,
minRating: mstreamModule.minRating,
ignoreVPaths: Object.keys(mstreamModule.ignoreVPaths).filter((vpath) => {
return mstreamModule.ignoreVPaths[vpath] === true;
})
ignoreVPaths: mstreamModule.ignoreVPaths
};
const res = await MSTREAMAPI.getRandomSong(params);
@ -162,11 +158,11 @@ const MSTREAMPLAYER = (() => {
// Stop the current song
return goToNextSong();
}
mstreamModule.previousSong = function () {
return goToPreviousSong();
}
mstreamModule.goToSongAtPosition = function (position) {
if (!mstreamModule.playlist[position]) {
return false;
@ -475,7 +471,7 @@ const MSTREAMPLAYER = (() => {
// Scrobble song after 30 seconds
clearTimeout(scrobbleTimer);
scrobbleTimer = setTimeout(() => { mstreamModule.scrobble() }, 30000);
scrobbleTimer = setTimeout(function () { mstreamModule.scrobble() }, 30000);
}
// Should be called whenever the "metadata" field of the current song is changed, or
@ -489,8 +485,7 @@ 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,
@ -500,11 +495,6 @@ 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);
}
@ -574,15 +564,8 @@ 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;
}
}
@ -624,7 +607,6 @@ const MSTREAMPLAYER = (() => {
currentTime: 0,
playing: false,
shouldLoop: false,
shouldLoopOne: false,
shuffle: false,
volume: 100,
metadata: {
@ -698,7 +680,7 @@ const MSTREAMPLAYER = (() => {
url += `&algo=${mstreamModule.transcodeOptions.selectedAlgo}`;
}
}
console.log(url)
player.playerObject.src = url;
player.songObject = song;
player.playerObject.load();
@ -715,9 +697,6 @@ const MSTREAMPLAYER = (() => {
function callMeOnStreamEnd() {
mstreamModule.playerStats.playing = false;
if (mstreamModule.playerStats.shouldLoopOne === true) {
return goToSong(mstreamModule.positionCache.val);
}
// Go to next song
goToNextSong();
}
@ -767,6 +746,18 @@ 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) {
@ -786,19 +777,15 @@ 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; }
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;
}
mstreamModule.playerStats.shouldLoop = !mstreamModule.playerStats.shouldLoop;
return mstreamModule.playerStats.shouldLoop;
}
// Random Song
@ -861,7 +848,6 @@ 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']}?compress=l&token=${MSTREAMPLAYER.getCurrentSong().authToken}`;
return `../album-art/${this.meta['album-art']}?token=${MSTREAMPLAYER.getCurrentSong().authToken}`;
}
},
methods: {

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -151,57 +151,6 @@
</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">
@ -239,29 +188,6 @@
</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"?>
@ -322,15 +248,7 @@
</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 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>
<span>Recent</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>
@ -363,10 +281,6 @@
<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>
@ -399,7 +313,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 class="pointer" :src="albumArtPath" v-on:click="getSongInfo" />
<img :src="albumArtPath" />
</div>
<div class="card-action">
@ -424,7 +338,7 @@
<div class="card-content">
<p>
<b>Title:</b>
{{ (meta.title) ? meta.title : (meta.filepath ? meta.filepath.split('/').pop() : '') }}
{{ (meta.title) ? meta.title : '' }}
</p>
<p>
<b>Artist:</b>
@ -446,9 +360,7 @@
</div>
<div class="flex">
<div v-if="altLayout.audioBookCtrls === false" class="button-block">
<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="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 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>
@ -526,10 +438,6 @@
<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>
@ -539,7 +447,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="VUEPLAYERCORE.clearQueue();">
<a title="Clear Playlist" onclick="MSTREAMPLAYER.clearPlaylist();">
<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>
@ -548,12 +456,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 pointer" v-on:click="getSongInfo" />
<img :src="albumArtPath" class="block" />
</div>
<div class="card-meta">
<p>
<b>Title:</b>
{{ (meta.title) ? meta.title : (meta.filepath ? meta.filepath.split('/').pop() : '') }}
{{ (meta.title) ? meta.title : '' }}
</p>
<p>
<b>Artist:</b>
@ -576,7 +484,6 @@
<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,12 +1,15 @@
const { app, BrowserWindow } = require('electron')
const path = require('path')
function createWindow () {
const win = new BrowserWindow({
autoHideMenuBar: true,
backgroundColor: '#1e2228',
width: 1200,
height: 800
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.webContents.openDevTools()
win.loadFile('./index.html')
}

View File

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

View File

@ -1,63 +1,12 @@
{
"name": "mstream-desktop-app",
"version": "5.9.4",
"description": "mStream Desktop Player",
"name": "mstream-hybrid-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "electron .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"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"
}
"author": "",
"license": "ISC"
}

10
webapp/preload.js Normal file
View File

@ -0,0 +1,10 @@
// 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])
// }
// })