move
All checks were successful
continuous-integration/drone Build is passing

This commit is contained in:
Artem Anufrij 2023-02-08 12:30:56 +01:00
commit cff48aaada
44 changed files with 12200 additions and 0 deletions

8
.drone.yml Normal file
View File

@ -0,0 +1,8 @@
kind: pipeline
name: default
steps:
- name: build
image: node:latest
commands:
- npm install

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/
cache/

13
README.md Normal file
View File

@ -0,0 +1,13 @@
# WebPlay Server
[![Build Status](https://drone.anufrij.de/api/badges/WebPlay/server/status.svg)](https://drone.anufrij.de/WebPlay/server)
WebPlay Server provides REST-API for your media files. In then next step, you can use [WebPlay Client](/WebPlay/client) to access your content.
## How to install your onw instance
[Documentation](/WebPlay/docker#requirements)
## Support
Join our Matrix room: <a href="https://matrix.to/#/#WebPlay:matrix.anufrij.de">#WebPlay:matrix.anufrij.de</a>
## Donate
<a href="https://www.paypal.me/ArtemAnufrij">PayPal</a> | <a href="https://liberapay.com/Artem/donate">LiberaPay</a> | <a href="https://www.patreon.com/ArtemAnufrij">Patreon</a>

65
config.json Normal file
View File

@ -0,0 +1,65 @@
{
"port": 31204,
"music_folder": "/webplay/music",
"video_folder": "/webplay/videos",
"cache_folder": "/webplay/cache",
"upload_folder": "/webplay/uploads",
"domain": "",
"allowed_domains": [
"https://webplay.rocks",
"http://localhost"
],
"database": {
"host": "database",
"port": 27017,
"name": "webplay"
},
"redis": {
"host": "redis",
"port": 6379
},
"album_cover_files": [
"cover.jpg",
"Cover.jpg",
"cover.jpeg",
"Cover.jpeg",
"cover.png",
"Cover.png",
"folder.jpg",
"Folder.jpg",
"folder.png",
"Folder.png",
"front.jpg",
"Front.jpg",
"front.png",
"front.png",
"album.jpg",
"Album.jpg",
"album.png",
"Album.png"
],
"artist_cover_files": [
"artist.jpg",
"Artist.jpg",
"artist.jpeg",
"Artist.jpeg",
"artist.png",
"Artist.png",
"band.jpg",
"Band.jpg",
"band.jpeg",
"Band.jpeg",
"band.png",
"Band.png"
],
"box_cover_files": [
"cover.jpg",
"Cover.jpg",
"cover.png",
"Cover.png",
"poster.jpg",
"Poster.jpg",
"poster.png",
"Poster.png"
]
}

7215
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "webplay_server",
"version": "1.0.0",
"description": "WebPlay Backend",
"author": "Artem Anufrij <artem.anufrij@live.de>",
"license": "MIT",
"main": "server.js",
"scripts": {
"backend": "cross-env NODE_ENV=development npx nodemon ./server.js"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.1",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"express": "^4.17.2",
"express-fileupload": "^1.3.1",
"express-session": "^1.17.2",
"fluent-ffmpeg": "^2.1.2",
"jsonwebtoken": "^8.5.1",
"mime-types": "^2.1.34",
"mongodb": "^4.3.1",
"music-metadata": "^7.11.8",
"node-fdkaac": "^1.4.1",
"node-id3": "^0.2.3",
"node-lame": "^1.3.2",
"passport": "^0.5.2",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"redis": "^4.5.1",
"request": "^2.88.2",
"sharp": "0.27.2",
"socket.io": "^4.4.1",
"systeminformation": "^5.11.2"
},
"devDependencies": {
"cross-env": "^7.0.3",
"nodemon": "^2.0.15"
}
}

88
router/activitypub.js Normal file
View File

@ -0,0 +1,88 @@
const express = require("express");
const router = new express.Router();
const database = require("../services/database");
const server = require("../server");
const config = server.config;
router.route("/:user")
.get((req, res) => {
database.userByName(req.params.user.toLowerCase(), user => {
if (user == undefined) {
return req.status(404).end();
}
let id = "https://" + config.domain + "/users/" + user.name;
let inbox = "https://" + config.domain + "/users/" + user.name + "/inbox";
let outbox = "https://" + config.domain + "/users/" + user.name + "/outbox";
let followers = "https://" + config.domain + "/users/" + user.name + "/followers";
let following = "https://" + config.domain + "/users/" + user.name + "/followinig";
let liked = "https://" + config.domain + "/users/" + user.name + "/liked";
let response = {
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Person",
"id": id,
"name": "Artem",
"preferredUsername": user.name,
"summary": "cajon player and e-bass junkie",
"inbox": inbox,
"outbox": outbox,
"followers": followers,
"following": following,
"liked": liked
}
res.json(response).end();
});
});
router.route("/:user/inbox")
.get((req, res) => {
console.log(req);
res.end();
})
.post((req, res) => {
console.log(req);
res.end();
});
router.route("/:user/outbox")
.get((req, res) => {
})
.post((req, res) => {
console.log(req);
res.end();
});
router.route("/:user/followers")
.get((req, res) => {
console.log(req);
res.end();
})
.post((req, res) => {
console.log(req);
res.end();
});
router.route("/:user/following")
.get((req, res) => {
console.log(req);
res.end();
})
.post((req, res) => {
console.log(req);
res.end();
});
router.route("/:user/liked")
.get((req, res) => {
console.log(req);
res.end();
})
.post((req, res) => {
console.log(req);
res.end();
});
module.exports = router;

119
router/album.js Normal file
View File

@ -0,0 +1,119 @@
console.log("router/album INIT");
const express = require("express");
const database = require("../services/database");
const server = require("../server");
const passport = server.passport;
const checkGuest = server.checkGuest;
const resize_image_for_album = require("../services/cover/resizer").resize_image_for_album
const router = new express.Router();
router.route("/favourites")
.get(passport.authenticate("jwt", { session: false }), (req, res) => {
database.albums.favourites(req.user._id, result => {
res.json(result).end();
});
})
router.route("/page/:page")
.get(checkGuest, (req, res) => {
process.stdout.write("router/album GET albums page " + req.params.page + "\n");
let filter = undefined;
if (req.user._id == -1) {
filter = ['global'];
}
database.albums.collection(req.params.page, filter, result => {
process.stdout.write("router/album GET albums page " + req.params.page + " DB result\n");
res.json(result);
});
});
router.route("/newest/:count")
.get(checkGuest, (req, res) => {
process.stdout.write("router/album GET newest " + req.params.count + "\n");
let filter = undefined;
if (req.user._id == -1) {
filter = ['global'];
}
database.albums.newest(parseInt(req.params.count), filter, result => {
process.stdout.write("router/album GET newest " + req.params.count + " DB result\n");
res.json(result);
});
});
router.route("/:id")
.get(checkGuest, (req, res) => {
process.stdout.write("router/album GET album by id " + req.params.id + "\n");
let filter = undefined;
if (req.user._id == -1) {
filter = ['global'];
}
database.albums.byId(req.params.id, filter, result => {
process.stdout.write("router/album GET album by id " + req.params.id + " DB result\n");
res.json(result);
});
})
.put(passport.authenticate("jwt", { session: false }), (req, res) => {
if (req.user.roles.indexOf("admin") >= 0 || req.user.roles.indexOf("moderator") >= 0) {
database.albums.update(req.body);
}
res.end();
});
router.route("/filter/:term")
.get(passport.authenticate("jwt", { session: false }), (req, res) => {
process.stdout.write("router/album GET filter by term " + req.params.term + "\n");
database.albums.filter(req.params.term, result => {
process.stdout.write("router/album GET filter by term " + req.params.term + " DB result\n");
res.json(result);
});
})
router.route("/:id/cover")
.put(passport.authenticate("jwt", { session: false }), (req, res) => {
if (req.files) {
if (req.files.file) {
resize_image_for_album(req.files.file.data, (result) => {
database.albums.updateCovers({ _id: req.params.id }, result);
res.json(result);
});
}
}
})
.delete(passport.authenticate("jwt", { session: false }), (req, res) => {
database.albums.updateCovers({ _id: req.params.id }, {});
res.end();
});
router.route("/:id/move")
.put(passport.authenticate("jwt", { session: false }), (req, res) => {
let source = {};
let target = {};
database.albums.byId(req.body.source, undefined, (result) => {
if (result != undefined && (req.user.roles.indexOf("admin") > -1) || result.owner_id == req.user._id) {
source = result;
database.albums.byId(req.body.target, undefined, (result) => {
if (result && (req.user.roles.indexOf("admin") > -1 || result.owner_id == req.user._id)) {
target = result;
for (let i = 0; i < source.tracks.length; i++) {
let track = source.tracks[i]
track.album_id = target._id
database.tracks.moveTo(track);
}
database.albums.delete(source, () => {
res.status(200).end();
});
} else {
res.status(403).end();
}
});
} else {
res.status(403).end();
}
});
});
module.exports = router;

102
router/artist.js Normal file
View File

@ -0,0 +1,102 @@
console.log("router/artist INIT");
const express = require("express");
console.log("router/artist REQUIRE database");
const database = require("../services/database");
const server = require("../server");
const passport = server.passport;
const checkGuest = server.checkGuest;
const resize_image_for_artist = require("../services/cover/resizer").resize_image_for_artist
const router = new express.Router();
router.route("/favourites")
.get(passport.authenticate("jwt", { session: false }), (req, res) => {
database.artists.favourites(req.user._id, result => {
res.json(result).end();
});
})
router
.route("/page/:page")
.get(checkGuest, (req, res) => {
process.stdout.write("router/artist GET artists page " + req.params.page + "\n");
let filter = undefined;
if (req.user._id == -1) {
filter = ['global'];
}
database.artists.collection(req.params.page, filter, result => {
process.stdout.write("router/artist GET artists page " + req.params.page + " DB result\n");
res.json(result);
});
});
router
.route("/:id")
.get(checkGuest, (req, res) => {
process.stdout.write("router/artist GET by id " + req.params.id + "\n");
let filter = undefined;
if (req.user._id == -1) {
filter = ['global'];
}
database.artists.byId(req.params.id, filter, result => {
process.stdout.write("router/artist GET by id " + req.params.id + " DB result\n");
res.json(result);
});
});
router.route("/filter/:term")
.get(passport.authenticate("jwt", { session: false }), (req, res) => {
process.stdout.write("router/artist GET filter by term " + req.params.term + "\n");
database.artists.filter(req.params.term, result => {
process.stdout.write("router/artist GET filter by term " + req.params.term + " DB result\n");
res.json(result);
});
})
router.route("/:id/cover")
.put(passport.authenticate("jwt", { session: false }), (req, res) => {
if (req.files) {
if (req.files.file) {
resize_image_for_artist(req.files.file.data, (result) => {
database.artists.updateCovers({ _id: req.params.id }, result);
res.json(result);
});
}
}
})
.delete(passport.authenticate("jwt", { session: false }), (req, res) => {
database.artists.updateCovers({ _id: req.params.id }, {});
res.end();
});
router.route("/:id/move")
.put(passport.authenticate("jwt", { session: false }), (req, res) => {
let source = {};
let target = {};
database.artists.byId(req.body.source, undefined, (result) => {
if (result && req.user.roles.indexOf("admin") > -1) {
source = result;
database.artists.byId(req.body.target, undefined, (result) => {
if (result && req.user.roles.indexOf("admin") > -1) {
target = result;
for (let i = 0; i < source.albums.length; i++) {
let album = source.albums[i];
album.artist_id = target._id;
album.artist_name = target.name;
database.albums.moveTo(album);
}
database.artists.delete(source, () => {
res.status(200).end();
});
} else {
res.status(403).end();
}
});
} else {
res.status(403).end();
}
});
});
module.exports = router;

118
router/box.js Normal file
View File

@ -0,0 +1,118 @@
console.log("router/box INIT");
const express = require("express");
const database = require("../services/database");
const server = require("../server");
const passport = server.passport;
const checkGuest = server.checkGuest;
const resize_image_for_box = require("../services/cover/resizer").resize_image_for_box
const router = new express.Router();
router.route("/favourites")
.get(passport.authenticate("jwt", { session: false }), (req, res) => {
database.boxes.favourites(req.user._id, result => {
res.json(result).end();
});
})
router
.route("/page/:page")
.get(checkGuest, (req, res) => {
process.stdout.write("router/box GET boxes page " + req.params.page + "\n");
let filter = undefined;
if (req.user._id == -1) {
filter = ['global'];
}
database.boxes.collection(req.params.page, filter, result => {
process.stdout.write("router/box GET boxes page " + req.params.page + " DB result\n");
res.json(result);
});
});
router
.route("/newest/:count")
.get(checkGuest, (req, res) => {
process.stdout.write("router/box GET newest " + req.params.count + "\n");
let filter = undefined;
if (req.user._id == -1) {
filter = ['global'];
}
database.boxes.newest(parseInt(req.params.count), filter, result => {
process.stdout.write("router/box GET newest " + req.params.count + " DB result\n");
res.json(result);
});
});
router
.route("/:id")
.get(checkGuest, (req, res) => {
process.stdout.write("router/box GET by id " + req.params.id + "\n");
let filter = undefined;
if (req.user._id == -1) {
filter = ['global'];
}
database.boxes.byId(req.params.id, filter, result => {
process.stdout.write("router/box GET by id " + req.params.id + " DB result\n");
res.json(result).end();
});
}).put(passport.authenticate("jwt", { session: false }), (req, res) => {
if (req.user.roles.indexOf("admin") >= 0 || req.user.roles.indexOf("moderator") >= 0) {
database.boxes.update(req.body);
}
res.end();
});
router.route("/filter/:term")
.get(passport.authenticate("jwt", { session: false }), (req, res) => {
process.stdout.write("router/boxes GET filter by term " + req.params.term + "\n");
database.boxes.filter(req.params.term, result => {
process.stdout.write("router/boxes GET filter by term " + req.params.term + " DB result\n");
res.json(result);
});
})
router.route("/:id/cover")
.put(passport.authenticate("jwt", { session: false }), (req, res) => {
if (req.files) {
if (req.files.file) {
resize_image_for_box(req.files.file.data, (result) => {
database.boxes.updateCovers({ _id: req.params.id }, result);
res.json(result);
});
}
}
})
.delete(passport.authenticate("jwt", { session: false }), (req, res) => {
database.boxes.updateCovers({ _id: req.params.id }, {});
res.end();
});
router.route("/:id/move")
.put(passport.authenticate("jwt", { session: false }), (req, res) => {
let source = {};
let target = {};
database.boxes.byId(req.body.source, undefined, (result) => {
if (result && (req.user.roles.indexOf("admin") > -1 || result.owner_id == req.user._id)) {
source = result;
database.boxes.byId(req.body.target, undefined, (result) => {
if (result && (req.user.roles.indexOf("admin") > -1 || result.owner_id == req.user._id)) {
target = result;
for (let i = 0; i < source.videos.length; i++) {
let video = source.videos[i]
video.box_id = target._id
database.videos.moveTo(video);
}
database.boxes.delete(source, () => {
res.status(200).end();
});
} else {
res.status(403).end();
}
});
} else {
res.status(403).end();
}
});
});
module.exports = router;

16
router/index.js Normal file
View File

@ -0,0 +1,16 @@
exports.albumRouter = require("./album");
exports.artistRouter = require("./artist");
exports.trackRouter = require("./track");
exports.radioRouter = require("./radio")
exports.boxRouter = require("./box");
exports.videoRouter = require("./video");
exports.scanRouter = require("./scan");
exports.userRouter = require("./user");
exports.loginRouter = require("./login");
exports.infoRouter = require("./info");
exports.systemRouter = require("./system")
exports.statusRouter = require("./status");
exports.settingsRouter = require("./settings");
exports.searchRouter = require("./search");
// exports.activitypubRouter = require("./activitypub");
// exports.wellknownRouter = require("./well-known");

33
router/info.js Normal file
View File

@ -0,0 +1,33 @@
console.log("router/info INIT");
var express = require("express");
var router = new express.Router();
var database = require("../services/database");
let info = {}
info.stats = {}
router
.route("")
.get((req, res) => {
database.artist_count((q, c) => {
info.stats.artists = c;
database.album_count((q, c) => {
info.stats.albums = c;
database.track_count((q, c) => {
info.stats.tracks = c;
database.box_count((q, c) => {
info.stats.boxes = c;
database.video_count((q, c) => {
info.stats.videos = c;
database.users_count((q, c) => {
info.stats.users = c;
res.json(info);
});
});
});
});
});
});
});
module.exports = router;

59
router/login.js Normal file
View File

@ -0,0 +1,59 @@
console.log("router/login INIT");
var express = require("express");
var router = new express.Router();
var bcrypt = require("bcryptjs");
var database = require("../services/database");
var jwt = require("jsonwebtoken");
var server = require("../server");
var passport = server.passport;
var secret = server.secret;
router
.route("/login")
.get(passport.authenticate("jwt", { session: false }), (req, res) => {
let user = req.user;
database.historyList(user._id, history => {
user.history = history;
database.users.favourites(user._id, favourites => {
user.favourites = favourites;
res.status(200).json(req.user);
});
});
})
.post((req, res) => {
database.userByName(req.body.username, user => {
if (!user) {
process.stdout.write("no user in DB\n");
return res.status(401).send({
success: false,
msg: "Authentication failed. Wrong user."
});
}
bcrypt.compare(req.body.password, user.password, (err, isMatch) => {
if (err) throw err;
if (isMatch) {
var token = jwt.sign(user, secret);
user.token = "JWT " + token;
process.stdout.write("pass ok\n");
database.updateUserAccess(req.body.username);
database.historyList(user._id, history => {
user.history = history;
database.users.favourites(user._id, favourites => {
user.favourites = favourites;
res.status(200).json(user);
});
});
} else {
process.stdout.write("pass fail\n");
res.status(401).send({
success: false,
msg: "Authentication failed. Wrong password."
});
}
});
});
});
module.exports = router;

71
router/radio.js Normal file
View File

@ -0,0 +1,71 @@
console.log("router/radio INIT");
const express = require("express");
console.log("router/radio REQUIRE END");
const database = require("../services/database");
const server = require("../server");
const passport = server.passport;
const checkGuest = server.checkGuest;
const resize_image_for_radio = require("../services/cover/resizer").resize_image_for_radio;
console.log("router/radio CREATE instances");
var router = new express.Router();
console.log("router/radio READY");
router
.route("")
.get(checkGuest, (req, res) => {
database.radios.collection(result => {
res.json(result);
});
})
.post(passport.authenticate("jwt", { session: false }), (req, res) => {
process.stdout.write("add radio\n");
if (
req.body.name == "" ||
!req.body.url ||
!req.body.url.startsWith("http")
) {
return res.end();
}
let newRadio = {
name: req.body.name,
url: req.body.url
};
database.radios.add(newRadio, () => {
res.end();
});
})
.delete(passport.authenticate("jwt", { session: false }), (req, res) => {
process.stdout.write("delete radio\n");
database.radios.delete(req.query.id, () => {
database.radios.collection(result => {
res.json(result);
});
});
});
router
.route("/:id/cover")
.put(passport.authenticate("jwt", { session: false }), (req, res) => {
process.stdout.write("update radio cover\n");
if (req.files) {
database.radios.byId(req.params.id, radio => {
if (radio) {
if (req.files.file) {
resize_image_for_radio(req.files.file.data, (result) => {
radio.cover32 = result.cover32;
radio.cover64 = result.cover64;
radio.cover128 = result.cover128;
database.radios.update(radio);
res.json(radio).end();
});
}
}
});
}
});
module.exports = router;

48
router/scan.js Normal file
View File

@ -0,0 +1,48 @@
console.log("router/scan INIT");
var express = require("express");
var file_scanner = require("../services/files_scanner");
var cover_excluder = require("../services/cover_excluder");
console.log("router/scan LOAD server settings");
var server = require("../server");
var status = server.status;
var config = server.config;
var passport = server.passport;
var router = new express.Router();
console.log("router/scan SET routers");
router
.route("/music")
.post(passport.authenticate("jwt", { session: false }), (req, res) => {
if (!status.scanning_music) {
status.scanning_music = true;
process.stdout.write("music scann started…\n");
res.send("music scann started…");
file_scanner.scann_local_music_files(config.music_folder);
setTimeout(() => {
file_scanner.scann_local_music_files(config.upload_folder);
}, 1000);
} else {
res.send("please wait. server is scanning for music files…");
}
});
router
.route("/video")
.post(passport.authenticate("jwt", { session: false }), (req, res) => {
if (!status.scanning_video) {
status.scanning_video = true;
process.stdout.write("video scann started…\n");
res.send("video scann started…");
file_scanner.scann_local_video_files(config.video_folder);
setTimeout(() => {
file_scanner.scann_local_video_files(config.upload_folder);
}, 1000);
} else {
res.send("please wait. server is scanning for video files…");
}
});
console.log("router/scan EXPORT routers");
module.exports = router;

17
router/search.js Normal file
View File

@ -0,0 +1,17 @@
console.log("router/search INIT");
var express = require("express");
var server = require("../server");
var database = require("../services/database");
var passport = server.passport;
var router = new express.Router();
router
.route("/:term")
.get(passport.authenticate("jwt", { session: false }), (req, res) => {
database.search(req.params.term.toLowerCase(), result => {
res.json(result).end();
});
});
module.exports = router;

26
router/settings.js Normal file
View File

@ -0,0 +1,26 @@
console.log("router/settings INIT");
var express = require("express");
var database = require("../services/database");
var router = new express.Router();
var server = require("../server");
router.route("/").get((req, res) => {
database.system.allows((result) => {
console.log(result);
res.json(result)
});
});
router.route("/lists").get((req, res) => {
let lists = {
audio_quality: server.lists.audio_quality,
video_lang: server.lists.lang,
video_quality: server.lists.video_quality,
visibility: server.lists.visibility,
user_role: server.lists.user_role
}
res.json(lists);
});
module.exports = router;

18
router/status.js Normal file
View File

@ -0,0 +1,18 @@
console.log("router/status INIT");
var express = require("express");
var router = new express.Router();
var server = require("../server");
var status = server.status;
var tag_excluder = require("../services/tag_excluder");
router.route("").get((req, res) => {
status.scann_buffer = tag_excluder.get_buffer_size();
status.parsing_music_data = tag_excluder.parsing_music_data();
status.parsing_video_data = tag_excluder.parsing_video_data();
status.parsing_cover = tag_excluder.parsing_cover();
res.json(status);
});
module.exports = router;

97
router/system.js Normal file
View File

@ -0,0 +1,97 @@
console.log("router/system INIT");
const express = require("express");
const router = new express.Router();
const database = require("../services/database");
const redis = require("../services/redis")
const jwt = require("jsonwebtoken");
const server = require("../server");
const secret = server.secret;
const passport = server.passport;
const config = server.config
router
.route("")
.get((req, res) => {
database.system.allows((result) => {
res.json(result).end();
})
})
.post(passport.authenticate("jwt", { session: false }), (req, res) => {
if (req.user.roles.indexOf("admin") > -1) {
database.system.setAllows(req.body, () => {
res.status(200).end();
})
} else {
res.status(403).end();
}
});
router
.route("/domains")
.get(passport.authenticate("jwt", { session: false }), (req, res) => {
if (req.user.roles.indexOf("admin") > -1) {
let domains = {
const: config.allowed_domains,
dynamic: []
}
database.system.domains((result) => {
result.forEach(domain => {
domains.dynamic.push(domain);
});
res.json(domains).end();
});
} else {
res.status(403).end();
}
})
.post(passport.authenticate("jwt", { session: false }), (req, res) => {
if (req.user.roles.indexOf("admin") > -1) {
database.system.setDomains(req.body, () => {
res.status(200).end();
});
}
})
router
.route("/setup")
.get((req, res) => {
database.users.collection(users => {
if (users && users.length > 0) {
res.status(204).end();
} else {
res.status(200).end();
}
});
})
.post((req, res) => {
process.stdout.write("add admin user\n");
database.users.collection(users => {
if (users && users.length > 0) {
res.status(403).end();
} else {
let newUser = {
name: req.body.username,
password: req.body.password,
roles: ["admin"]
};
database.addUser(newUser, result => {
var token = jwt.sign(result, secret);
result.token = "JWT " + token;
result.history = [];
res.json(result).end();
});
}
});
});
router
.route("/reset/redis")
.post(passport.authenticate("jwt", { session: false }), (req, res) => {
if (req.user.roles.indexOf("admin") > -1) {
redis.flushAll();
}
});
module.exports = router;

222
router/track.js Normal file
View File

@ -0,0 +1,222 @@
console.log("router/track INIT");
const express = require("express");
const fs = require("fs");
const meta_data = require("music-metadata");
const database = require("../services/database");
const node_id3 = require("node-id3");
const Lame = require("node-lame").Lame;
const Fdkaac = require("node-fdkaac").Fdkaac;
const notifier = require("../services/notifier");
const server = require("../server");
const passport = server.passport;
const config = server.config;
const checkGuest = server.checkGuest;
const router = new express.Router();
const audio_cache_dir = config.cache_folder + "/audio_cache/";
const resizer = require("../services/cover/resizer");
if (!fs.existsSync(audio_cache_dir)) {
fs.mkdirSync(audio_cache_dir);
}
server.lists.audio_quality.forEach(bmp => {
let dir = audio_cache_dir + bmp;
if (!fs.existsSync(dir)) {
fs.mkdir(dir, () => { });
}
});
router.route("")
.post(passport.authenticate("jwt", { session: false }), (req, res) => {
if (req.files) {
let track = JSON.parse(req.body.track);
let url_user = config.upload_folder + "/" + req.user._id;
let folder = Math.floor(Date.now() / 1000 / 60);
let url_folder = url_user + "/" + folder
let file = url_folder + "/" + req.files.file.name;
req.files.file.mv(file).then(() => {
meta_data
.parseFile(file, { duration: true })
.then(meta => {
let item = {
path: file,
artist: track.artist,
album: track.album,
title: track.title,
duration: meta.format.duration,
bitrate: meta.format.bitrate,
mime: req.files.file.mimetype,
year: track.year,
track: track.track,
disk: meta.common.disk,
genre: track.genre || meta.common.genre,
owner: req.user
};
if (meta.common.picture && meta.common.picture.length > 0) {
resizer.resize_image_for_album(meta.common.picture[0].data, (covers) => {
item.covers = covers;
notifier.emit("metadata_excluded", item);
res.status(200).end();
});
} else {
notifier.emit("metadata_excluded", item);
res.status(200).end();
}
})
.catch(err => {
process.stdout.write(file);
console.log(err);
});
});
} else {
res.end();
}
});
router
.route("/most_listened")
.get(checkGuest, (req, res) => {
let filter = undefined;
if (req.user._id == -1) {
filter = ['global'];
}
database.tracks.mostListened(filter, (result) => {
res.json(result);
});
});
router
.route("/:id/stream")
.get((req, res) => {
database.tracks.byId(req.params.id, result => {
process.stdout.write("audio stream: " + result.title);
if (!fs.existsSync(config.music_folder + result.path)) {
return res.end();
}
stream(req, res, config.music_folder + result.path, result.mime);
});
});
router
.route("/:id/stream/:rate")
.get((req, res) => {
if (!server.lists.audio_quality.includes(req.params.rate)) {
res.end();
}
let music = audio_cache_dir + req.params.rate + "/" + req.params.id + ".mp3";
if (fs.existsSync(music)) {
stream(req, res, music, "audio/mpeg");
} else {
convert(req.params.id, req.params.rate).then((file) => {
if (file) {
stream(req, res, file.path, file.mime);
} else {
res.end();
}
});
}
});
router.route("/:id/convert/:rate")
.put((req, res) => {
let music = audio_cache_dir + req.params.rate + "/" + req.params.id + ".mp3";
if (!fs.existsSync(music)) {
convert(req.params.id, req.params.rate);
}
res.end();
});
function stream(req, res, music, mime) {
process.stdout.write("audio stream: " + music + "\n");
let stat = fs.statSync(music);
if (req.headers.range) {
var range = req.headers.range;
var parts = range.replace(/bytes=/, "").split("-");
var partialstart = parts[0];
var partialend = parts[1];
var start = parseInt(partialstart, 10);
var end = partialend ? parseInt(partialend, 10) : stat.size - 1;
var chunksize = end - start + 1;
var readStream = fs.createReadStream(music, { start: start, end: end });
res.writeHead(206, {
"Content-Range": "bytes " + start + "-" + end + "/" + stat.size,
"Accept-Ranges": "bytes",
"Content-Length": chunksize,
"Content-Type": mime
});
readStream.pipe(res);
} else {
res.writeHead(200, {
"Content-Length": stat.size,
"Content-Type": mime
});
fs.createReadStream(music).pipe(res);
}
}
function convert(id, rate) {
return new Promise((resolve) => {
database.tracks.byId(id, result => {
let full_path = config.music_folder + result.path;
let cache_path = audio_cache_dir + rate + "/" + id + ".mp3";
if (!fs.existsSync(full_path)) {
full_path = config.upload_folder + result.path;
if (!fs.existsSync(full_path)) {
resolve();
}
}
if (result.mime == "audio/mpeg" && result.bitrate / 1000 <= rate) {
process.stdout.write("stream non converted file: " + result.titel + "\n");
resolve({ path: full_path, mime: result.mime });
return;
}
process.stdout.write("audio convert: " + result.title + " (" + result.mime + ")\n");
if (result.mime.includes("mp4")) {
process.stdout.write("audio decode: " + result.title + " (" + result.mime + ")\n");
const decoder = new Fdkaac({ output: "buffer" }).setFile(full_path);
decoder
.decode()
.then(() => {
const buffer = decoder.getBuffer();
const lame_encoder = new Lame({
output: cache_path,
bitrate: rate
}).setBuffer(buffer);
lame_encoder
.encode()
.then(() => {
node_id3.removeTags(cache_path);
resolve({ path: cache_path, mime: "audio/mpeg" });
})
.catch(err => {
process.stdout.write(err);
});
})
.catch(err => {
process.stdout.write(err);
});
} else {
const lame_encoder = new Lame({ output: cache_path, bitrate: rate }).setFile(full_path);
lame_encoder
.encode()
.then(() => {
node_id3.removeTags(cache_path);
resolve({ path: cache_path, mime: "audio/mpeg" });
})
.catch(err => {
process.stdout.write(err);
process.stdout.write("stream original");
resolve({ path: full_path, mime: result.mime });
});
}
});
});
}
module.exports = router;

172
router/user.js Normal file
View File

@ -0,0 +1,172 @@
console.log("router/user INIT");
var express = require("express");
var database = require("../services/database");
var bcrypt = require("bcryptjs");
var router = new express.Router();
var server = require("../server");
var passport = server.passport;
router
.route("")
.get(passport.authenticate("jwt", { session: false }), (req, res) => {
database.users.collection(result => {
res.json(result).end();
});
})
.post(passport.authenticate("jwt", { session: false }), (req, res) => {
process.stdout.write("add user\n");
if (
req.user.roles.includes("admin") ||
req.user.roles.includes("moderator")
) {
let newUser = {
name: req.body.name,
password: req.body.password
};
database.addUser(newUser, () => {
res.end();
});
} else {
res.status(401).end();
}
})
.delete(passport.authenticate("jwt", { session: false }), (req, res) => {
process.stdout.write("delete user\n");
if (req.user.roles.includes("admin") || req.user._id == req.query.id) {
database.userById(req.query.id, (user) => {
if (user.roles.includes("admin")) {
res.status(403).end();
} else {
database.deleteUser(req.query.id, () => {
database.users.collection(result => {
res.json(result).end();
});
});
}
});
} else {
res.status(401).end();
}
})
.put(passport.authenticate("jwt", { session: false }), (req, res) => {
process.stdout.write("update user\n");
if (req.user.roles.includes("admin")) {
if (req.body.newPassword) {
database.updateUserPassword(req.body.name, req.body.newPassword);
} else {
database.updateUserRole(req.body, () => {
res.status(202).end();
});
}
res.end();
} else {
res.status(401).end();
}
});
router
.route("/:name/exists")
.get(passport.authenticate("jwt", { session: false }), (req, res) => {
if (
req.user.roles.includes("admin") ||
req.user.roles.includes("moderator")
) {
database.userByName(req.params.name, user => {
res.json({ exists: user != null }).end();
});
} else {
res.status(401).end();
}
});
router
.route("/update")
.post(passport.authenticate("jwt", { session: false }), (req, res) => {
if (!req.user) {
return res.status(401).end();
}
if (!req.body.oldPassword) {
database.updateUserConfig(req.user, req.body);
process.stdout.write("config changed\n");
}
if (req.body.oldPassword && req.user.password) {
bcrypt.compare(req.body.oldPassword, req.user.password, function (err, isMatch) {
if (err) throw err;
if (isMatch) {
database.updateUserPassword(req.user.name, req.body.newPassword);
process.stdout.write("password changed\n");
res.status(202);
} else {
process.stdout.write("no match\n");
res.status(422);
}
res.end();
});
} else {
res.end();
}
});
router
.route("/favourites")
.get(passport.authenticate("jwt", { session: false }), (req, res) => {
process.stdout.write("router/user GET favourites\n");
database.user.favourites(id, result => {
res.json(result).end();
});
})
.post(passport.authenticate("jwt", { session: false }), (req, res) => {
let item = req.body;
item.userId = req.user._id;
process.stdout.write("router/user POST favourites\n");
database.users.insertFavourite(item, () => {
res.status(200).end();
})
})
.delete(passport.authenticate("jwt", { session: false }), (req, res) => {
let item = {};
item.itemId = req.query.itemId;
item.userId = req.user._id;
process.stdout.write("router/user DELETE favourites " + req.query.itemId + "\n");
database.users.deleteFavourite(item, () => {
res.status(200).end();
});
});
router
.route("/history")
.get(passport.authenticate("jwt", { session: false }), (req, res) => {
database.historyList(req.user._id, result => {
res.json(result).end();
});
})
.post(passport.authenticate("jwt", { session: false }), (req, res) => {
let item = req.body;
item.userId = req.user._id;
database.updateHistory(item, () => {
database.historyList(req.user._id, result => {
res.json(result).end();
});
});
})
.delete(passport.authenticate("jwt", { session: false }), (req, res) => {
database.clearHistory(req.user._id, () => {
process.stdout.write("history cleared for '" + req.user.name + "'\n");
res.status(200).end();
});
});
router
.route("/settings")
.put(passport.authenticate("jwt", { session: false }), (req, res) => {
req.user.player.repeat = req.body.repeat;
req.user.player.shuffle = req.body.shuffle;
database.updateUserSettings(req.user, () => {
res.status(202).end();
});
});
module.exports = router;

209
router/video.js Normal file
View File

@ -0,0 +1,209 @@
console.log("router/video INIT");
const express = require("express");
const fs = require("fs");
const ffmpeg = require("fluent-ffmpeg");
const database = require("../services/database");
const notifier = require("../services/notifier");
const server = require("../server");
const passport = server.passport;
const config = server.config;
const router = new express.Router();
const video_cache_dir = config.cache_folder + "/video_cache/";
const checkGuest = server.checkGuest;
const resize_image_for_box = require("../services/cover/resizer").resize_image_for_box
if (!fs.existsSync(video_cache_dir)) {
fs.mkdirSync(video_cache_dir);
}
router.route("")
.post(passport.authenticate("jwt", { session: false }), (req, res) => {
if (req.files) {
let file = undefined;
let cover = undefined;
if (Array.isArray(req.files.file)) {
file = req.files.file[0];
cover = req.files.file[1];
} else {
file = req.files.file;
}
let box = JSON.parse(req.body.box);
let url_user = config.upload_folder + "/" + req.user._id;
let folder = Math.floor(Date.now() / 1000 / 60);
let url_folder = url_user + "/" + folder
let file_path = url_folder + "/" + file.name;
let item = {
path: file_path.replace(config.upload_folder, ""),
box: box.title,
box_path: url_folder,
title: req.body.title,
mime: file.mimetype,
year: box.year,
owner: req.user
};
//SAVE VIDEO FILE
function saveVideoFile() {
file.mv(file_path).then(() => {
notifier.emit("pathdata_excluded", item);
res.status(200).end();
});
}
if (cover) {
resize_image_for_box(cover.data, (result) => {
item.cover32 = result.cover32;
item.cover64 = result.cover64;
item.cover128 = result.cover128;
item.cover256 = result.cover256;
saveVideoFile();
});
} else {
saveVideoFile();
}
}
});
router.route("/most_viewed")
.get(checkGuest, (req, res) => {
let filter = undefined;
if (req.user._id == -1) {
filter = ['global'];
}
database.videos.mostViewed(filter, (result) => {
res.json(result);
});
});
router.route("/:id/stream")
.get((req, res) => {
database.videos.byId(req.params.id, result => {
process.stdout.write("video stream: " + result.title + "\n");
let full_path = config.video_folder + result.path;
if (!fs.existsSync(full_path)) {
full_path = config.upload_folder + result.path;
if (!fs.existsSync(full_path)) {
return res.end();
}
}
stream(req, res, full_path, result.mime);
});
});
router.route("/:id/stream/:rate/:audioIndex")
.get((req, res) => {
if (!server.lists.video_quality.includes(req.params.rate)) {
req.end();
}
let video = getFileName(req);
if (fs.existsSync(video)) {
stream(req, res, video, "video/mpeg");
} else {
convert(req, res, video, () => {
stream(req, res, video, "video/mp4");
});
}
});
router.route("/:id/convert/:rate/:audioIndex")
.get((req, res) => {
if (!server.lists.video_quality.includes(req.params.rate)) {
req.end();
}
let video = getFileName(req);
if (!fs.existsSync(video)) {
convert(req, res, video);
}
res.end();
});
function convert(req, res, video, callback = null) {
database.videos.byId(req.params.id, result => {
process.stdout.write("video stream: " + result.title + "\n");
let full_path = config.video_folder + result.path;
if (!fs.existsSync(full_path)) {
full_path = config.upload_folder + result.path;
if (!fs.existsSync(full_path)) {
return res.end();
}
}
ffmpeg(full_path)
.audioBitrate(128)
.audioChannels(2)
.videoCodec("libvpx")
.size("?x" + req.params.rate)
.videoBitrate(req.params.rate * 2)
.outputOptions([
"-cpu-used " + server.config.cpu_cores,
"-map 0:v:0",
"-map 0:a:" + req.params.audioIndex
])
.output(video)
.on("start", commandLine => {
process.stdout.write(
"Spawned Ffmpeg with command: " + commandLine + "\n"
);
if (callback) {
let interval = setInterval(() => {
if (fs.existsSync(video) && fs.statSync(video).size > 3000000) {
clearInterval(interval);
callback();
}
}, 4000);
}
})
.on("error", e => {
process.stdout.write(e);
})
.run();
});
}
function stream(req, res, video, mime) {
process.stdout.write("video stream: " + video + "\n");
let stat = fs.statSync(video);
if (req.headers.range) {
var range = req.headers.range;
var parts = range.replace(/bytes=/, "").split("-");
var partialstart = parts[0];
var partialend = parts[1];
var start = parseInt(partialstart, 10);
var end = partialend ? parseInt(partialend, 10) : stat.size - 1;
var chunksize = end - start + 1;
var readStream = fs.createReadStream(video, { start: start, end: end });
res.writeHead(206, {
"Content-Range": "bytes " + start + "-" + end + "/" + stat.size,
"Accept-Ranges": "bytes",
"Content-Length": chunksize,
"Content-Type": mime
});
readStream.pipe(res);
} else {
res.writeHead(200, {
"Content-Length": stat.size,
"Content-Type": mime
});
fs.createReadStream(video).pipe(res);
}
}
function getFileName(req) {
return (
video_cache_dir +
req.params.id +
"-" +
req.params.rate +
"-" +
req.params.audioIndex +
".webm"
);
}
module.exports = router;

49
router/well-known.js Normal file
View File

@ -0,0 +1,49 @@
const express = require("express");
const router = new express.Router();
const server = require("../server");
const config = server.config;
const database = require("../services/database");
router.route("/webfinger")
.get((req, res) => {
if (!req.query.resource) {
return res.status(400).end();
}
let resource = req.query.resource.toLowerCase();
let name = resource.replace("acct:", "").replace("@" + config.domain, "").toLowerCase();
if (name.length == 0) {
return res.status(400).end();
}
database.userByName(name, (user) => {
if (user == undefined) {
return res.status(404).end();
}
let response = {
"subject": resource,
"aliases": [
"https://" + config.domain + "/#/users/" + user.name
],
"links": [
{
"rel": "http://webfinger.net/rel/profile-page",
"type": "text/html",
"href": "https://" + config.domain + "/#/users/" + user.name
},
{
"rel": "self",
"type": "application/activity+json",
"href": "https://" + config.domain + "/users/" + user.name
}
]
}
res.json(response).end();
});
});
module.exports = router;

153
server.js Normal file
View File

@ -0,0 +1,153 @@
process.stdout.write("server INIT\n");
const express = require("express");
const fileUpload = require('express-fileupload');
const cors = require("cors");
const fs = require("fs");
const si = require("systeminformation");
const bodyParser = require("body-parser");
const passport = require("passport");
const JwtStrategy = require("passport-jwt").Strategy;
const ExtractJwt = require("passport-jwt").ExtractJwt;
/*
JWT
*/
const secret = "secret";
const jwt_opts = {
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme("JWT"),
secretOrKey: secret,
ignoreExpiration: true
};
passport.use(
new JwtStrategy(jwt_opts, (user, done) => {
database.userByName(user.name, res => {
if (res && user.name == res.name && user.password == res.password) {
delete res.password;
return done(null, res);
} else {
return done(null, false);
}
});
})
);
// CONFIG AND STATUS OBJECTS
process.stdout.write("server LOAD config\n");
const config = JSON.parse(fs.readFileSync("./config.json", "utf8"));
if (!fs.existsSync(config.cache_folder)) {
fs.mkdirSync(config.cache_folder);
}
if (process.env.DOMAIN) {
config.domain = process.env.DOMAIN;
}
config.base_path = __dirname;
const status = {
scanning_music: false,
scanning_video: false
};
process.stdout.write("server DEF arrays\n");
const lists = {
audio_quality: ["64", "96", "128", "192", "256", "320"],
video_quality: ["480", "720", "1080"],
user_role: ["admin", "moderator", "user"],
lang: ["ENG", "GER", "RUS"],
visibility: ["global", "instance", "owner", "hidden"]
};
process.stdout.write("server READ cpu cores\n");
si.cpu(function (data) {
config.cpu_cores = data.cores;
});
exports.config = config;
exports.status = status;
exports.lists = lists;
exports.passport = passport;
exports.secret = secret;
exports.checkGuest = function checkGuest(req, res, next) {
passport.authenticate('jwt', { session: false, }, async (error, user) => {
if (user) {
delete user.password;
req.user = user;
next();
} else {
database.system.allows((allows) => {
if (allows.guests) {
req.user = { _id: -1, name: "Guest" }
next();
}
else {
return res.status(403).end();
}
});
}
})(req, res, next);
}
require("./services/cleanup_utiles");
const database = require("./services/database");
const app = express();
const server = require("http").createServer(app);
app.use(bodyParser.json({ limit: "500mb" }));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(fileUpload({
createParentPath: true
}));
const corsOptions = {
origin: (origin, callback) => {
if (!origin || config.allowed_domains.indexOf(origin.replace(/:\d*$/g, "")) !== -1) {
callback(null, true);
} else {
database.system.domains((domains) => {
if (domains.indexOf(origin.replace(/:\d*$/g, "")) !== -1) {
callback(null, true);
} else {
callback("Origin not allowed: " + origin);
}
})
}
},
methods: "GET,POST,PUT,DELETE",
allowedHeaders: "*",
credentials: true
};
app.use(cors(corsOptions));
// ROUTERS
process.stdout.write("server LOAD routers\n");
const router = require("./router");
process.stdout.write("server USE routers\n");
app.use("/api/albums", router.albumRouter);
app.use("/api/artists", router.artistRouter);
app.use("/api/boxes", router.boxRouter);
app.use("/api/info", router.infoRouter);
app.use("/api/radios", router.radioRouter);
app.use("/api/scan", router.scanRouter);
app.use("/api/search", router.searchRouter);
app.use("/api/settings", router.settingsRouter);
app.use("/api/status", router.statusRouter);
app.use("/api/system", router.systemRouter);
app.use("/api/tracks", router.trackRouter);
app.use("/api/user", router.loginRouter);
app.use("/api/user", router.userRouter);
app.use("/api/videos", router.videoRouter);
// app.use("/well-known", router.wellknownRouter);
// app.use("/users", router.activitypubRouter);
process.stdout.write("server START listening on Port " + config.port + "\n");
server.listen(config.port, function () {
process.stdout.write("Server startet on Port " + config.port + "...\n");
process.stdout.write("Music Folder: " + config.music_folder + "\n");
process.stdout.write("Video Folder: " + config.video_folder + "\n");
process.stdout.write("Cache Folder: " + config.cache_folder + "\n");
process.stdout.write("Upload Folder: " + config.upload_folder + "\n");
process.stdout.write("Domain: " + config.domain + "\n");
});

View File

@ -0,0 +1,58 @@
var fs = require("fs");
var notifier = require("./notifier");
var server = require("../server");
var config = server.config;
var album_cover_dir = config.cache_folder + "/album_covers/";
var album_cover_32 = album_cover_dir + "32/";
var album_cover_96 = album_cover_dir + "96/";
var album_cover_64 = album_cover_dir + "64/";
var album_cover_128 = album_cover_dir + "128/";
var album_cover_256 = album_cover_dir + "256/";
var artist_cover_dir = config.cache_folder + "/artist_covers/";
var artist_cover_128 = artist_cover_dir + "128/";
var artist_cover_256 = artist_cover_dir + "256/";
var artist_cover_512 = artist_cover_dir + "512/";
var box_cover_dir = config.cache_folder + "/box_covers/";
var box_cover_32 = box_cover_dir + "32/";
var box_cover_64 = box_cover_dir + "64/";
var box_cover_128 = box_cover_dir + "128/";
var box_cover_256 = box_cover_dir + "256/";
var video_cover_dir = config.cache_folder + "/video_covers/";
var video_cache_dir = config.cache_folder + "/realtime/";
var audio_cache_dir = config.cache_folder + "/audio_cache/";
var audio_cache_128 = audio_cache_dir + "128/";
notifier.on("track_deleted", id => {
fs.unlink(audio_cache_128 + id + ".mp3", () => {});
});
notifier.on("artist_deleted", id => {
fs.unlink(artist_cover_128 + id, () => {});
fs.unlink(artist_cover_256 + id, () => {});
fs.unlink(artist_cover_512 + id, () => {});
});
notifier.on("album_deleted", id => {
fs.unlink(album_cover_32 + id, () => {});
fs.unlink(album_cover_64 + id, () => {});
fs.unlink(album_cover_96 + id, () => {});
fs.unlink(album_cover_128 + id, () => {});
fs.unlink(album_cover_256 + id, () => {});
});
notifier.on("box_deleted", id => {
fs.unlink(box_cover_32 + id, () => {});
fs.unlink(box_cover_64 + id, () => {});
fs.unlink(box_cover_128 + id, () => {});
fs.unlink(box_cover_256 + id, () => {});
});
notifier.on("video_deleted", id => {
fs.unlink(video_cover_dir + "/" + id + ".png", () => {});
});

96
services/cover/resizer.js Normal file
View File

@ -0,0 +1,96 @@
const sharp = require("sharp");
const fs = require("fs");
/*
RADIO COVER
*/
const radio_cover_sizes = [
{ width: 128, height: 128, fit: "cover" },
{ width: 64, height: 64, fit: "cover" },
{ width: 32, height: 32, fit: "cover" }
];
exports.resize_image_for_radio = function (image, callback) {
resizer(image, radio_cover_sizes, callback);
};
/*
ALBUM COVER
*/
const album_cover_sizes = [
{ width: 256, height: 256, fit: "cover" },
{ width: 128, height: 128, fit: "cover" },
{ width: 64, height: 64, fit: "cover" },
{ width: 32, height: 32, fit: "cover" }
];
exports.resize_image_for_album = function (image, callback) {
if (typeof image === 'string') {
let data = fs.readFileSync(image);
resizer(data, album_cover_sizes, callback);
} else {
resizer(image, album_cover_sizes, callback);
}
};
/*
ARTIST COVER
*/
const artist_cover_sizes = [
{ width: 1024, height: 512, fit: "cover" },
{ width: 512, height: 256, fit: "cover" },
{ width: 256, height: 128, fit: "cover" },
{ width: 128, height: 64, fit: "cover" },
{ width: 64, height: 32, fit: "cover" }
];
exports.resize_image_for_artist = function (image, callback) {
if (typeof image === 'string') {
console.log("resizing: " + image);
let data = fs.readFileSync(image);
resizer(data, artist_cover_sizes, callback);
} else {
resizer(image, artist_cover_sizes, callback);
}
};
/*
BOX COVER
*/
const box_cover_sizes = [
{ width: 256, height: 362, fit: "cover" },
{ width: 128, height: 181, fit: "cover" },
{ width: 64, height: 90, fit: "cover" },
{ width: 32, height: 45, fit: "cover" }
];
exports.resize_image_for_box = function (image, callback) {
if (typeof image === 'string') {
let data = fs.readFileSync(image);
resizer(data, box_cover_sizes, callback);
} else {
resizer(image, box_cover_sizes, callback);
}
};
function resizer(image, array, callback) {
let result = {}
let runner = (image, index) => {
if (index < array.length) {
let size = array[index];
sharp(image)
.resize(size)
.png()
.toBuffer()
.then(data => {
result["cover" + size.width] = "data:image/png;base64, " + Buffer.from(data).toString("base64");
runner(data, ++index);
}).catch(err=>{
console.log(err);
});
} else {
if (callback) {
callback(result);
}
}
}
runner(image, 0);
}

269
services/cover_excluder.js Normal file
View File

@ -0,0 +1,269 @@
console.log("service/cover_excluder INIT");
console.log("service/cover_excluder REQUIRE fs");
const fs = require("fs");
console.log("service/cover_excluder REQUIRE path");
const path = require("path");
console.log("service/cover_excluder REQUIRE request");
const request = require("request");
console.log("service/cover_excluder INCLUDE notifier");
const notifier = require("./notifier");
console.log("service/cover_excluder INCLUDE database");
const database = require("./database");
console.log("service/cover_excluder INCLUDE music_brainz");
const music_brainz = require("./music_brainz");
console.log("service/cover_excluder INCLUDE the_movie_db");
const the_movie_db = require("./the_movie_db");
console.log("service/cover_excluder INCLUDE resizer")
const resizer = require("../services/cover/resizer")
console.log("service/cover_excluder LOAD server settings")
const server = require("../server");
const status = server.status;
const config = server.config;
const cache_folder = config.cache_folder + "/";
console.log("service/cover_excluder SET notifiers")
notifier.on("exclude_metadata_finished", () => {
process.stdout.write("music scann finished…\n");
checkMissingAlbumCovers();
checkMissingArtistCovers();
status.scanning_music = false;
});
notifier.on("exclude_pathdata_finished", () => {
process.stdout.write("video scann finished…\n");
checkMissingBoxCovers();
status.scanning_video = false;
});
notifier.on("metadata_picture_excluded", album => {
resizer.resize_image_for_album(album.picture[0].data, covers => {
if (Object.keys(covers).length > 0) {
process.stdout.write("cover from metadata…\n");
database.albums.updateCovers(album, covers);
} else {
process.stdout.write("no cover from metadata…\n");
music_brainz.find_album_cover(album);
}
});
});
notifier.on("no_metadata_picture_excluded", album => {
music_brainz.find_album_cover(album);
});
notifier.on("found_music_brainz_album_cover", result => {
process.stdout.write("MB album result: " + result.mb + "\n");
let album_id = result.album._id.toString();
let tmp_file = cache_folder + album_id;
let writer = fs.createWriteStream(tmp_file);
writer.on("finish", () => {
resizer.resize_image_for_album(tmp_file, covers => {
if (Object.keys(covers).length > 0) {
process.stdout.write("album cover from musicbrainz…\n");
database.albums.updateCovers(result.album, covers);
}
fs.unlinkSync(tmp_file);
});
});
request(result.mb).pipe(writer);
});
notifier.on("found_music_brainz_artist_cover", result => {
process.stdout.write("MB artist result: " + result.mb + "\n");
let artist_id = result.artist._id.toString();
let tmp_file = cache_folder + artist_id;
let writer = fs.createWriteStream(tmp_file);
writer.on("finish", () => {
resizer.resize_image_for_artist(tmp_file, covers => {
if (Object.keys(covers).length > 0) {
process.stdout.write("artist cover from musicbrainz…\n");
database.artists.updateCovers(result.artist, covers);
}
fs.unlinkSync(tmp_file);
});
});
request(result.mb).pipe(writer);
});
notifier.on("found_movie_db_box_cover", box => {
process.stdout.write("TMVD box result: " + box.tmdb + "\n");
let box_id = box._id.toString();
let tmp_file = cache_folder + box_id;
let writer = fs.createWriteStream(tmp_file);
writer.on("finish", () => {
resizer.resize_image_for_box(tmp_file, covers => {
if (Object.keys(covers).length > 0) {
process.stdout.write("box covers from tmvd…\n");
database.boxes.updateCovers(box, covers);
}
fs.unlinkSync(tmp_file);
});
});
request(box.tmdb).pipe(writer);
});
function checkMissingAlbumCovers() {
process.stdout.write("started cover excluding for albums…\n");
database.albums.collection(-1, null, albums => {
albums.forEach(album => {
database.albums.tracks(album._id, album_tracks => {
if (!album_tracks || !album_tracks.tracks || album.covers) {
return;
}
album.tracks = album_tracks.tracks;
let cover_found = false;
album.tracks.forEach(track => {
if (cover_found) {
return;
}
let file_path = config.music_folder + track.path;
let directory = path.dirname(file_path);
config.album_cover_files.forEach(file => {
let img = directory + "/" + file;
if (fs.existsSync(img)) {
cover_found = true;
resizer.resize_image_for_album(img, covers => {
if (Object.keys(covers).length > 0) {
process.stdout.write("covers from file…\n");
database.albums.updateCovers(album, covers);
}
});
return;
}
});
});
if (!cover_found) {
export_from_id3(album);
}
});
});
});
}
function export_from_id3(album) {
album.tracks.forEach(track => {
track.parent = album;
});
notifier.emit("album_cover_request", album.tracks[0]);
}
function checkMissingArtistCovers() {
process.stdout.write("started cover excluding for artists…\n");
database.artists.collection(-1, undefined, artists => {
artists.forEach(artist => {
database.artists.tracks(artist._id.toString(), true, artist_full => {
if (!artist_full || !artist_full.albums || artist.covers) {
return;
}
let cover_found = false;
artist_full.albums.forEach(album => {
album.tracks.forEach(track => {
if (cover_found) {
return;
}
config.artist_cover_files.forEach(file => {
if (cover_found) {
return;
}
let file_path = config.music_folder + track.path;
let directory = path.dirname(file_path);
let img = directory + "/" + file;
if (fs.existsSync(img)) {
cover_found = true;
resizer.resize_image_for_artist(img, covers => {
if (Object.keys(covers).length > 0) {
process.stdout.write("artist covers from file…\n");
database.artists.updateCovers(artist, covers);
}
});
return;
}
directory = path.dirname(directory);
img = directory + "/" + file;
if (fs.existsSync(img)) {
cover_found = true;
resizer.resize_image_for_artist(img, covers => {
if (Object.keys(covers).length > 0) {
process.stdout.write("artist covers from file…\n");
database.artists.updateCovers(artist, covers);
}
});
return;
}
directory = path.dirname(directory);
img = directory + "/" + file;
if (fs.existsSync(img)) {
cover_found = true;
resizer.resize_image_for_artist(img, covers => {
if (Object.keys(covers).length > 0) {
process.stdout.write("artist covers from file…\n");
database.artists.updateCovers(artist, covers);
}
});
return;
}
});
});
});
if (!cover_found) {
music_brainz.find_artist_cover(artist_full);
}
});
});
});
}
function checkMissingBoxCovers() {
process.stdout.write("started cover excluding for boxes…\n");
database.boxes.collection(-1, undefined, boxes => {
boxes.forEach(box => {
database.boxes.videos(box._id.toString(), true, box_full => {
if (box_full.covers) {
return;
}
let cover_found = false;
let directory = config.video_folder + box_full.path;
config.box_cover_files.forEach(file => {
if (cover_found) {
return;
}
let img = directory + "/" + file;
if (fs.existsSync(img)) {
cover_found = true;
resizer.resize_image_for_box(img, covers => {
if (Object.keys(covers).length > 0) {
process.stdout.write("box covers from file…\n");
database.boxes.updateCovers(box_full, covers);
}
});
return;
}
});
if (!cover_found) {
the_movie_db.find_box_cover(box);
}
});
});
});
}

View File

@ -0,0 +1,25 @@
const { MongoClient } = require('mongodb');
const server = require("../../server");
const config = server.config;
var dbo;
const url = "mongodb://" + config.database.host + ":" + config.database.port + "/";
const database = config.database.name;
exports.connect = async function () {
if (dbo) {
console.log("DB CONNECTED")
return dbo;
}
else {
try {
console.log("DB CONNECTING:" + config.database.host + ":" + config.database.port)
const client = await MongoClient.connect(url);
dbo = client.db(database);
return dbo;
} catch (error) {
console.error(`MongoDB connection failed with > ${error}`);
throw error;
}
}
}

302
services/database/albums.js Normal file
View File

@ -0,0 +1,302 @@
const redis = require("../redis")
const { ObjectId } = require('mongodb');
const connector = require("./CONNECTOR");
var dbo;
connector.connect().then((ret) => {
dbo = ret;
dbo.collection("albums").createIndex({ artist_name: 1, year: 1, title: 1 });
// TEMPORARY
dbo.collection("albums").updateMany({}, { $unset: { cover32: 1, cover64: 1, cover128: 1, cover256: 1, cover512: 1 } });
updateArtistName();
});
function updateArtistName() {
dbo.collection("albums")
.find({ artist_name: undefined })
.toArray((err, result) => {
result.forEach(item => {
dbo.collection("artists")
.findOne({ _id: item.artist_id })
.then(artist => {
if (artist) {
dbo.collection("albums")
.updateOne(
{ _id: item._id },
{ $set: { artist_id: artist._id, artist_name: artist.name } },
{ upsert: false });
}
});
})
});
}
exports.collection = function (page, filter, callback) {
process.stdout.write("services/db_manager ALBUMS Collection: " + page + "\n");
let redis_key = "albumsCollection_" + (filter || '') + '_' + page;
redis.get(redis_key, (value) => {
if (value) {
process.stdout.write("services/db_manager ALBUMS Collection REDIS: " + page + "\n");
callback(value);
} else {
let aggregate = [
{ $project: { "covers.cover256": false, "covers.cover64": false, "covers.cover32": false } },
{ $match: { visibility: { $not: { $eq: 'hidden' } } } },
{ $sort: { artist_name: 1, year: 1, title: 1 } },
];
if (filter) {
aggregate.push({
$match: { visibility: { $in: filter } }
});
}
if (page > -1) {
let pageSize = 12;
let skip = (page - 1) * pageSize;
aggregate.push(
{ $skip: skip },
{ $limit: pageSize });
}
dbo
.collection("albums")
.aggregate(aggregate, { allowDiskUse: true })
.toArray((err, result) => {
if (err) throw err;
if (result) {
result.forEach(album => {
album.type = "album";
album.tracks = [];
});
}
process.stdout.write("services/db_manager ALBUMS Collection MONGO: " + page + "\n");
callback(result);
redis.set(redis_key, result);
});
}
});
};
exports.favourites = function (id, callback) {
dbo.collection("favourites")
.find({ userId: id, type: "album" })
.toArray((err, favourites) => {
if (err) throw err;
let aggregate = [
{ $match: { _id: { $in: favourites.map(m => m.itemId) } } },
{ $project: { "covers.cover256": false, "covers.cover64": false, "covers.cover32": false } },
]
dbo.collection("albums")
.aggregate(aggregate)
.toArray((err, result) => {
result.forEach(album => {
album.type = "album";
album.tracks = [];
});
callback(result);
});
})
};
exports.newest = function (count, filter, callback) {
let aggregate = [
{ $project: { "covers.cover256": false, "covers.cover128": false, "covers.cover32": false } },
{ $match: { visibility: { $not: { $eq: 'hidden' } } } },
{ $sort: { _id: -1 } },
];
if (filter) {
aggregate.push({
$match: { visibility: { $in: filter } }
});
}
aggregate.push({ $limit: count })
dbo
.collection("albums")
.aggregate(aggregate, {
allowDiskUse: true
})
.toArray((err, result) => {
if (err) throw err;
if (result) {
result.forEach(album => {
album.type = "album";
album.tracks = [];
});
}
callback(result);
});
};
exports.byId = function (id, filter, callback) {
process.stdout.write("services/db_manager ALBUM by id: " + id + "\n");
let redis_key = "albumId_" + (filter || '') + '_' + id;
redis.get(redis_key, (value) => {
if (value) {
process.stdout.write("services/db_manager ALBUM by id REDIS: " + id + "\n");
callback(value);
} else {
let aggregate = [
{
$lookup: {
from: "tracks",
localField: "_id",
foreignField: "album_id",
as: "tracks"
}
},
{ $match: { _id: ObjectId(id) } }
]
if (filter) {
aggregate.push({
$match: { visibility: { $in: filter } }
});
}
dbo
.collection("albums")
.aggregate(aggregate)
.toArray((err, result) => {
if (err) throw err;
if (result) {
result.forEach(album => {
album.type = "album";
});
}
process.stdout.write("services/db_manager ALBUM by id MONGO: " + id + "\n");
callback(result[0]);
redis.set(redis_key, result[0]);
});
}
});
};
exports.filter = function (term, callback) {
let aggregate = [
{ $project: { 'parent.covers': false, 'covers.cover32': false, 'covers.cover256': false } },
{ $match: { title: { $regex: term, $options: "i" } }, },
{ $limit: 6 }
]
dbo
.collection("albums")
.aggregate(aggregate)
.toArray((err, result) => {
if (err) throw err;
callback(result);
});
};
exports.tracks = function (id, callback) {
process.stdout.write("services/db_manager TRACKS by id: " + id + "\n");
let request = [
{
$lookup: {
from: "tracks",
localField: "_id",
foreignField: "album_id",
as: "tracks"
}
},
{ $match: { _id: ObjectId(id) } }
];
dbo
.collection("albums")
.aggregate(request)
.toArray((err, result) => {
if (result) {
callback(result[0]);
} else {
if (err) {
process.stderr("services/db_manager ALBUM by id ERROR: " + err.message);
}
callback(null);
}
});
};
exports.delete = function (album, callback) {
dbo.collection("albums")
.deleteOne({ _id: ObjectId(album._id) }, err => {
if (err) throw err;
if (callback) {
callback();
}
});
};
exports.update = function (album, callback) {
dbo.collection("albums")
.updateOne(
{ _id: ObjectId(album._id) },
{
$set: {
visibility: album.visibility
}
},
{ upsert: false },
err => {
if (err) throw err;
if (callback) {
callback();
}
}
);
}
exports.updateCovers = function (album, covers, callback) {
dbo.collection("albums")
.updateOne(
{ _id: ObjectId(album._id) },
{ $set: { covers: covers } },
{ upsert: false },
err => {
if (err) throw err;
if (callback) {
callback();
}
}
);
};
exports.moveTo = function (album, callback) {
process.stdout.write("services/db_manager ALBUM '" + album._id + "' move to '" + album.artist_id + "'\n");
dbo
.collection("albums")
.updateOne(
{ _id: ObjectId(album._id) },
{ $set: { artist_id: ObjectId(album.artist_id), artist_name: album.artist_name } },
{ upsert: false },
err => {
if (err) throw err;
if (callback) {
callback();
}
});
};
exports.empty = function (callback) {
dbo
.collection("albums")
.aggregate([
{
$lookup: {
from: "tracks",
localField: "_id",
foreignField: "album_id",
as: "tracks"
}
}
])
.toArray((err, result) => {
callback(result.filter(f => !f.tracks || f.tracks.length == 0));
});
};

View File

@ -0,0 +1,264 @@
const redis = require("../redis")
const { ObjectId } = require('mongodb');
const connector = require("./CONNECTOR");
var dbo;
connector.connect().then((ret) => {
dbo = ret;
dbo.collection("artists").createIndex({ name: 1 });
// TEMPORARY
dbo.collection("artists").updateMany({}, { $unset: { cover32: 1, cover64: 1, cover128: 1, cover256: 1, cover512: 1 } })
});
let artist_lookup_album = {
$lookup: {
from: "albums",
localField: "_id",
foreignField: "artist_id",
as: "albums"
}
}
let artist_lookup_tracks = {
$lookup: {
from: "tracks",
localField: "albums._id",
foreignField: "album_id",
as: "albums.tracks"
}
}
let artists_project = {
$project: {
"covers.cover64": false,
"covers.cover128": false,
"covers.cover512": false,
"covers.cover1024": false,
}
}
let artist_project = {
$project: {
"albums.tracks.path": false,
"albums.tracks.bitrate": false,
"albums.tracks.album_id": false,
"albums.tracks.mime": false,
"albums.artist_id": false,
"albums.covers.cover256": false,
"albums.covers.cover512": false
}
}
exports.collection = function (page, filter, callback) {
process.stdout.write("services/db_manager ARTISTS Collection: " + page + "\n");
let redis_key = "artistsCollection_" + (filter || '') + '_' + page;
redis.get(redis_key, (value) => {
if (value) {
process.stdout.write("services/db_manager ARTISTS Collection REDIS: " + page + "\n");
callback(value);
} else {
let aggregate = []
if (filter) {
aggregate.push(
artist_lookup_album, {
$match: { "albums.visibility": { $in: filter } }
});
}
aggregate.push(
artists_project,
{ $sort: { name: 1 } }
)
if (page > -1) {
let pageSize = 12;
let skip = (page - 1) * pageSize;
aggregate.push(
{ $skip: skip },
{ $limit: pageSize });
}
dbo
.collection("artists")
.aggregate(aggregate, { allowDiskUse: true })
.toArray((err, result) => {
if (err) throw err;
if (result) {
result.forEach(item => {
item.type = "artist";
item.albums = [];
item.tracks = [];
});
}
process.stdout.write("services/db_manager ARTISTS Collection MONGO: " + page + "\n");
callback(result);
redis.set(redis_key, result);
});
}
});
};
exports.favourites = function (id, callback) {
dbo.collection("favourites")
.find({ userId: id, type: "artist" })
.toArray((err, favourites) => {
if (err) throw err;
let aggregate = [
{ $match: { _id: { $in: favourites.map(m => m.itemId) } } },
artists_project,
]
dbo.collection("artists")
.aggregate(aggregate)
.toArray((err, result) => {
result.forEach(item => {
item.type = "artist";
item.albums = [];
item.tracks = [];
});
callback(result);
});
})
};
exports.byId = function (id, filter, callback) {
process.stdout.write("services/db_manager ARTIST by id: " + id + "\n");
let redis_key = "artistId_" + (filter || '') + '_' + id;
redis.get(redis_key, (value) => {
if (value) {
process.stdout.write("services/db_manager ARTIST by id REDIS: " + id + "\n");
callback(value);
} else {
let aggregate = [
artist_lookup_album,
{
$unwind: { path: "$albums" }
},
artist_lookup_tracks,
artist_project,
{
$group: {
_id: "$_id",
name: { $first: "$name" },
covers: { $first: "$covers" },
albums: { $push: "$albums" }
}
},
{ $match: { _id: ObjectId(id) } },
];
dbo
.collection("artists")
.aggregate(aggregate)
.toArray((err, result) => {
if (err) throw err;
if (result) {
result.forEach(item => {
item.type = "artist";
item.tracks = [];
if (filter) {
item.albums = item.albums.filter(album => { return filter.indexOf(album.visibility) > -1 });
}
});
}
process.stdout.write("services/db_manager ARTIST by id MONGO: " + id + "\n");
callback(result[0]);
redis.set(redis_key, result[0]);
});
}
});
};
exports.filter = function (term, callback) {
let aggregate = [
{ $project: { 'covers.cover64': false, 'covers.cover512': false } },
{ $match: { name: { $regex: term, $options: "i" } }, },
{ $limit: 6 }
]
dbo
.collection("artists")
.aggregate(aggregate)
.toArray((err, result) => {
if (err) throw err;
callback(result);
});
};
exports.tracks = function (id, showPath, callback) {
let request = [
artist_lookup_album,
{
$unwind: { path: "$albums" }
},
artist_lookup_tracks,
{ $match: { _id: ObjectId(id) } },
{
$group: {
_id: "$_id",
name: { $first: "$name" },
albums: { $push: "$albums" }
}
}
];
if (!showPath) {
request.push({
$project: {
"albums.tracks.path": false,
"albums.tracks.bitrate": false,
"albums.tracks.album_id": false,
"albums.tracks.mime": false,
"albums.artist_id": false,
"albums.covers": false,
}
});
}
dbo
.collection("artists")
.aggregate(request)
.toArray((err, result) => {
if (result) {
callback(result[0]);
} else {
callback(null);
}
});
};
exports.delete = function (artist, callback) {
dbo.collection("artists")
.deleteOne({ _id: ObjectId(artist._id) }, err => {
if (err) throw err;
if (callback) {
callback();
}
});
};
exports.updateCovers = function (artist, covers, callback) {
dbo.collection("artists").updateOne(
{ _id: ObjectId(artist._id) },
{ $set: { covers: covers } },
{ upsert: false },
err => {
if (err) throw err;
if (callback) {
callback();
}
}
);
};
exports.empty = function (callback) {
dbo
.collection("artists")
.aggregate([
{
$lookup: {
from: "albums",
localField: "_id",
foreignField: "artist_id",
as: "albums"
}
}
])
.toArray((err, result) => {
callback(result.filter(f => !f.albums || f.albums.length == 0));
});
};

273
services/database/boxes.js Normal file
View File

@ -0,0 +1,273 @@
const redis = require("../redis")
const { ObjectId } = require('mongodb');
const connector = require("./CONNECTOR");
var dbo;
connector.connect().then((ret) => {
dbo = ret;
dbo.collection("boxes").createIndex({ title: 1 });
// TEMPORARY
dbo.collection("boxes").updateMany({}, { $unset: { cover32: 1, cover64: 1, cover128: 1, cover256: 1, cover512: 1 } })
});
let box_project = {
$project: {
"videos.path": false,
"videos.box_id": false,
"videos.mime": false,
path: false,
"covers.cover32": false,
}
}
let boxes_project = {
$project: {
path: false,
"covers.cover32": false,
"covers.cover64": false,
"covers.cover256": false
}
}
let box_lookup_videos = {
$lookup: {
from: "videos",
localField: "_id",
foreignField: "box_id",
as: "videos"
}
}
exports.collection = function (page, filter, callback) {
process.stdout.write("services/db_manager BOXES Collection: " + page + "\n");
let redis_key = "boxesCollection_" + (filter || '') + '_' + page;
redis.get(redis_key, (value) => {
if (value) {
process.stdout.write("services/db_manager BOXES Collection REDIS: " + page + "\n");
callback(value);
} else {
let aggregate = [
boxes_project,
{ $match: { visibility: { $not: { $eq: 'hidden' } } } },
{ $sort: { title: 1 } },
];
if (filter) {
aggregate.push({
$match: { visibility: { $in: filter } }
});
}
if (page > -1) {
let pageSize = 12;
let skip = (page - 1) * pageSize;
aggregate.push(
{ $skip: skip },
{ $limit: pageSize });
}
dbo
.collection("boxes")
.aggregate(aggregate, {
allowDiskUse: true
})
.toArray((err, result) => {
if (err) throw err;
if (result) {
result.forEach(item => {
item.type = "box";
item.videos = [];
});
}
process.stdout.write("services/db_manager BOXES Collection MONGO: " + page + "\n");
callback(result);
redis.set(redis_key, result);
});
}
});
};
exports.favourites = function (id, callback) {
dbo.collection("favourites")
.find({ userId: id, type: "box" })
.toArray((err, favourites) => {
if (err) throw err;
let aggregate = [
{ $match: { _id: { $in: favourites.map(m => m.itemId) } } },
boxes_project,
]
dbo.collection("boxes")
.aggregate(aggregate)
.toArray((err, result) => {
result.forEach(item => {
item.type = "box";
item.videos = [];
});
callback(result);
});
})
};
exports.newest = function (count, filter, callback) {
let aggregate = [
boxes_project,
{ $sort: { _id: -1 } },
{ $match: { visibility: { $not: { $eq: 'hidden' } } } }
];
if (filter) {
aggregate.push({
$match: { visibility: { $in: filter } }
});
}
aggregate.push({ $limit: count })
dbo
.collection("boxes")
.aggregate(aggregate, {
allowDiskUse: true
})
.toArray((err, result) => {
if (err) throw err;
if (result) {
result.forEach(item => {
item.type = "box";
item.videos = [];
});
}
callback(result);
});
};
exports.byId = function (id, filter, callback) {
process.stdout.write("services/db_manager BOX by id: " + id + "\n");
let redis_key = "boxId_" + (filter || '') + '_' + id;
redis.get(redis_key, (value) => {
if (value) {
process.stdout.write("services/db_manager BOX by id REDIS: " + id + "\n");
callback(value);
} else {
let aggregate = [
box_lookup_videos,
box_project,
{ $match: { _id: ObjectId(id) } }
];
if (filter) {
aggregate.push({
$match: { visibility: { $in: filter } }
});
}
dbo
.collection("boxes")
.aggregate(aggregate)
.toArray((err, result) => {
if (err) throw err;
if (result) {
result.forEach(item => {
item.type = "box";
});
}
process.stdout.write("services/db_manager BOX by id MONGO: " + id + "\n");
callback(result[0]);
redis.set(redis_key, result[0]);
});
}
});
};
exports.filter = function (term, callback) {
let aggregate = [
{ $project: { 'covers.cover256': false } },
{ $match: { title: { $regex: term, $options: "i" } }, },
{ $limit: 6 }
]
dbo
.collection("boxes")
.aggregate(aggregate)
.toArray((err, result) => {
if (err) throw err;
callback(result);
});
};
exports.empty = function (callback) {
dbo
.collection("boxes")
.aggregate([box_lookup_videos])
.toArray((err, result) => {
callback(result.filter(f => !f.videos || f.videos.length == 0));
});
};
exports.videos = function (id, showPath, callback) {
let request = [
{
$lookup: {
from: "videos",
localField: "_id",
foreignField: "box_id",
as: "videos"
}
},
{ $match: { _id: ObjectId(id) } }
];
if (!showPath) {
request.push({
$project: {
"videos.mime": false,
"videos.path": false,
"videos.box_id": false,
path: false,
"covers.cover32": false,
"covers.cover64": false,
"covers.cover128": false
}
});
}
dbo
.collection("boxes")
.aggregate(request)
.toArray((err, result) => {
callback(result[0]);
});
};
exports.delete = function (box, callback) {
dbo.collection("boxes").deleteOne({ _id: box._id }, err => {
if (err) throw err;
callback();
});
};
exports.update = function (box, callback) {
dbo.collection("boxes").updateOne(
{ _id: ObjectId(box._id) },
{
$set: {
visibility: box.visibility
}
},
{ upsert: false },
err => {
if (err) throw err;
if (callback) {
callback();
}
}
);
}
exports.updateCovers = function (box, covers, callback) {
dbo.collection("boxes").updateOne(
{ _id: ObjectId(box._id) },
{ $set: { covers: covers } },
{ upsert: false },
err => {
if (err) throw err;
if (callback) {
callback();
}
}
);
};

734
services/database/index.js Normal file
View File

@ -0,0 +1,734 @@
const { ObjectId } = require('mongodb');
const bcrypt = require("bcryptjs");
const ffmpeg = require("fluent-ffmpeg");
const fs = require("fs");
const notifier = require("../notifier");
const server = require("../../server");
const config = server.config;
const connector = require("./CONNECTOR");
var dbo;
connector.connect().then((ret) => {
dbo = ret;
});
notifier.on("metadata_excluded", item => {
insert_artist_if_not_exists(item, artist => {
insert_album_if_not_exists(item, artist, album => {
insert_track_if_not_exists(item, album);
});
});
});
notifier.on("pathdata_excluded", item => {
insert_box_if_not_exists(item, box => {
insert_video_if_not_exists(item, box);
});
});
notifier.on("check_video_details", item => {
if (!item.tracks) {
get_video_tracks(item, tracks => {
item.tracks = tracks;
update_video(item);
});
}
if (!item.thumbnail) {
get_video_thumbnail(item, thumbnail => {
item.thumbnail = thumbnail;
update_video(item);
});
}
});
/*
MODULES
*/
const albums = require("./albums");
exports.albums = albums;
const radios = require("./radios");
exports.radios = radios;
const tracks = require("./tracks");
exports.tracks = tracks;
const artists = require("./artists");
exports.artists = artists;
const boxes = require("./boxes");
exports.boxes = boxes;
const videos = require("./videos");
exports.videos = videos;
const users = require("./users");
exports.users = users;
const system = require("./system");
exports.system = system;
exports.artist_count = function (callback) {
return dbo.collection("artists").countDocuments(callback);
};
exports.album_count = function (callback) {
return dbo.collection("albums").countDocuments(callback);
};
exports.track_count = function (callback) {
return dbo.collection("tracks").countDocuments(callback);
};
exports.box_count = function (callback) {
return dbo.collection("boxes").countDocuments(callback);
};
exports.video_count = function (callback) {
return dbo.collection("videos").countDocuments(callback);
};
exports.users_count = function (callback) {
return dbo.collection("users").countDocuments(callback);
};
/*
USER
*/
exports.addUser = function (user, callback) {
bcrypt.genSalt(10, function (err, salt) {
bcrypt.hash(user.password, salt, function (err, hash) {
user.password = hash;
if (!user.roles) {
user.roles = ["user"];
}
user.name = user.name.toLowerCase();
user.player = {};
user.fullname = user.name;
dbo.collection("users").insertOne(user, err => {
if (err) throw err;
dbo.collection("users").findOne({ name: user.name }, (err, result) => {
if (err) throw err;
delete result.password;
callback(result);
});
});
});
});
};
exports.deleteUser = function (id, callback) {
dbo
.collection("users")
.deleteOne({ _id: ObjectId(id) }, (err, result) => {
if (err) throw err;
callback(result);
});
};
exports.updateUserRole = function (user, callback) {
dbo
.collection("users")
.updateOne({ name: user.name }, { $set: { roles: user.roles } }, err => {
if (err) throw err;
callback();
});
};
exports.updateUserAccess = function (name) {
dbo
.collection("users")
.updateOne(
{ name: name },
{ $set: { last_access: new Date().toLocaleString() } }
);
};
exports.updateUserPassword = function (user, newPassowrd) {
bcrypt.genSalt(10, function (err, salt) {
bcrypt.hash(newPassowrd, salt, function (err, hash) {
dbo
.collection("users")
.updateOne({ name: user }, { $set: { password: hash } },
err => { if (err) throw err; });
});
});
};
exports.updateUserConfig = function (user, body, callback) {
dbo.collection("users").updateOne(
{ name: user.name },
{
$set: {
mobile_bpm: body.mobile_bpm,
desktop_bpm: body.desktop_bpm,
video_lang: body.video_lang,
video_quality: body.video_quality,
fullname: body.fullname
}
},
err => {
if (err) throw err;
if (callback) {
callback();
}
}
);
};
exports.updateUserSettings = function (user, callback) {
dbo
.collection("users")
.updateOne(
{ _id: user._id },
{ $set: { player: user.player } },
err => {
if (err) throw err;
if (callback) {
callback();
}
}
);
};
exports.userByName = function (name, callback) {
dbo.collection("users").findOne({ name: name.toLowerCase() }, (err, result) => {
if (err) throw err;
callback(result);
});
};
exports.userById = function (id, callback) {
dbo
.collection("users")
.findOne({ _id: ObjectId(id) }, (err, result) => {
if (err) throw err;
callback(result);
});
};
/*
HISTORY
*/
exports.historyList = function (id, callback) {
dbo
.collection("history")
.find(
{ userId: id, type: { $in: ['album', 'artist', 'box', 'radio'] } },
{ projection: { _id: false, userId: false } }
)
.sort({ _id: -1 })
.limit(12)
.toArray((err, result) => {
if (err) throw err;
callback(result);
});
};
exports.clearHistory = function (id, callback) {
dbo
.collection("history")
.deleteMany({ userId: id }, (err, result) => {
if (err) throw err;
callback(result);
});
};
exports.updateHistory = function (item, callback) {
dbo
.collection("history")
.deleteMany({ userId: item.userId, id: item.id, type: { $in: ['album', 'artist', 'box', 'radio'] } }, err => {
if (err) throw err;
dbo
.collection("history")
.insertOne(item, err => {
if (err) throw err;
callback();
}
);
});
};
exports.search = function (term, callback) {
let r = [];
dbo
.collection("artists")
.find(
{ name: { $regex: term, $options: "i" } },
{ projection: { _id: true, name: true, "covers.cover256": true } }
)
.toArray((err, result) => {
if (result && result.length > 0) {
result.forEach(item => {
item.type = "artist";
item.search_rank = item.name.toLowerCase().indexOf(term);
});
r = r.concat(result);
}
dbo
.collection("albums")
.find(
{ title: { $regex: term, $options: "i" } },
{ projection: { _id: true, title: true, "covers.cover128": true } }
)
.toArray((err, result) => {
if (result && result.length > 0) {
result.forEach(item => {
item.type = "album";
item.parent = {};
item.search_rank = item.title.toLowerCase().indexOf(term);
});
r = r.concat(result);
}
dbo
.collection("tracks")
.aggregate([
{
$lookup: {
from: "albums",
localField: "album_id",
foreignField: "_id",
as: "parent"
}
}, {
$unwind: "$parent"
}, {
$project: {
album_id: false,
bitrate: false,
disk: false,
duration: false,
mime: false,
path: false,
genre: false,
track: false,
"parent.artist_id": false,
"parent.year": false,
"parent.covers.cover32": false,
"parent.covers.cover128": false,
"parent.covers.cover256": false,
"parent.covers.cover512": false
}
}, {
$match: { title: { $regex: term, $options: "i" } }
}])
.toArray((err, result) => {
if (result && result.length > 0) {
result.forEach(item => {
item.type = "track";
item.search_rank = item.title.toLowerCase().indexOf(term);
});
r = r.concat(result);
}
dbo
.collection("boxes")
.find(
{ title: { $regex: term, $options: "i" } },
{ projection: { _id: true, title: true, "covers.cover128": true } }
)
.toArray((err, result) => {
if (result && result.length > 0) {
result.forEach(item => {
item.type = "box";
item.search_rank = item.title.toLowerCase().indexOf(term);
});
r = r.concat(result);
}
dbo
.collection("videos")
.aggregate([
{
$lookup: {
from: "boxes",
localField: "box_id",
foreignField: "_id",
as: "parent"
}
}, {
$unwind: "$parent"
}, {
$project: {
path: false,
mime: false,
tracks: false,
box_id: false,
"parent.box_id": false,
"parent.year": false,
"parent.path": false,
"parent.title": false,
"parent.covers.cover32": false,
"parent.covers.cover64": false,
"parent.covers.cover128": false,
"parent.covers.cover256": false,
"parent.covers.cover512": false
}
}, {
$match: { title: { $regex: term, $options: "i" } }
}]
)
.toArray((err, result) => {
if (result && result.length > 0) {
result.forEach(item => {
item.type = "video";
item.search_rank = item.title
.toLowerCase()
.indexOf(term);
});
r = r.concat(result);
}
dbo
.collection("radios")
.find(
{ name: { $regex: term, $options: "i" } },
{ projection: { _id: true, name: true } }
)
.toArray((err, result) => {
if (result && result.length > 0) {
result.forEach(item => {
item.type = "radio";
item.search_rank = item.name
.toLowerCase()
.indexOf(term);
});
r = r.concat(result);
}
callback(
r
.sort((a, b) => {
return a.search_rank - b.search_rank;
})
.splice(0, 50)
);
});
});
});
});
});
});
};
/*
CHAT
*/
exports.messageList = function (callback) {
dbo
.collection("chat")
.aggregate([
{
$lookup: {
from: "users",
localField: "user_id",
foreignField: "_id",
as: "user"
}
},
{
$project: {
"user.password": false,
"user.player": false
}
},
{ $unwind: "$user" },
{ $sort: { _id: 1 } }
])
.toArray((err, result) => {
if (err) throw err;
callback(result.splice(0, 100));
});
};
exports.writeMessage = function (user, message, callback) {
dbo
.collection("chat")
.insertOne(
{ user_id: user._id, message: message, timestamp: Date.now() },
(err, result) => {
if (err) throw err;
callback(result.ops);
}
);
};
exports.deleteMessage = function (id, user, callback) {
dbo.collection("chat").deleteOne({ _id: ObjectId(id), user_id: user._id }, err => {
if (err) throw err;
callback();
});
};
/*
FUNCTIONS
*/
function insert_artist_if_not_exists(item, callback) {
dbo.collection("artists").findOne({ name: item.artist }, (err, result) => {
if (err) throw err;
if (result) {
callback(result);
} else {
let artist = {
name: item.artist,
insert_on: Date.now()
}
if (item.owner) {
artist.owner_id = item.owner._id;
}
dbo
.collection("artists")
.updateOne(
{ name: item.artist },
{ $set: artist },
{ upsert: true },
err => {
if (err) throw err;
insert_artist_if_not_exists(item, callback);
}
);
}
});
}
function insert_album_if_not_exists(item, artist, callback) {
dbo
.collection("albums")
.findOne({ artist_id: artist._id, title: item.album }, (err, result) => {
if (err) throw err;
if (result) {
callback(result);
} else {
let album = {
artist_id: artist._id,
artist_name: artist.name,
title: item.album.trim(),
year: item.year,
insert_on: Date.now()
}
if (item.covers) {
album.cover32 = item.covers.cover32;
album.cover64 = item.covers.cover64;
album.cover128 = item.covers.cover128;
album.cover256 = item.covers.cover256;
}
if (item.owner) {
album.owner_id = item.owner._id;
}
dbo.collection("albums").updateOne(
{ artist_id: artist._id, title: item.album },
{ $set: album },
{ upsert: true },
err => {
if (err) throw err;
insert_album_if_not_exists(item, artist, callback);
}
);
}
});
}
function insert_track_if_not_exists(item, album, callback) {
let short_path = ""
if (item.path.indexOf(config.upload_folder) == 0) {
short_path = item.path.replace(config.upload_folder, "");
} else {
short_path = item.path.replace(config.music_folder, "");
}
dbo.collection("tracks").updateOne(
{ path: short_path },
{
$set: {
title: item.title,
path: short_path,
album_id: album._id,
duration: item.duration,
bitrate: item.bitrate,
mime: item.mime,
disk: item.disk,
track: item.track,
genre: item.genre,
insert_on: Date.now()
}
},
{ upsert: true },
err => {
if (err) throw err;
dbo.collection("tracks").findOne({ path: short_path }, (err, result) => {
if (err) throw err;
if (callback) {
callback(result);
}
});
}
);
}
function insert_box_if_not_exists(item, callback) {
let short_path = item.box_path.replace(config.video_folder, "");
dbo
.collection("boxes")
.findOne({ title: item.box, year: item.year }, (err, result) => {
if (err) throw err;
if (result) {
callback(result);
} else {
let box = {
title: item.box,
path: short_path,
year: item.year,
cover64: item.cover64,
cover128: item.cover128,
cover256: item.cover256,
cover512: item.cover512,
insert_on: Date.now()
}
if (item.owner) {
box.owner_id = item.owner._id;
}
dbo
.collection("boxes")
.updateOne(
{ title: item.box },
{ $set: box },
{ upsert: true },
err => {
if (err) throw err;
insert_box_if_not_exists(item, callback);
}
);
}
});
}
function insert_video_if_not_exists(item, box, callback) {
item.path = item.path.replace(config.video_folder, "");
dbo.collection("videos").updateOne(
{ path: item.path },
{
$set: {
title: item.title,
path: item.path,
box_id: box._id,
mime: item.mime,
insert_on: Date.now()
}
},
{ upsert: true },
err => {
if (err) throw err;
dbo.collection("videos").findOne({ path: item.path }, (err, result) => {
if (err) throw err;
if (callback) {
callback(result);
}
if (!result.tracks) {
get_video_tracks(item, tracks => {
result.tracks = tracks;
update_video(result);
});
}
if (!result.thumbnail) {
get_video_thumbnail(result, thumbnail => {
result.thumbnail = thumbnail;
update_video(result);
});
}
});
}
);
}
function update_video(video, callback) {
process.stdout.write("updating video: " + video.title + "\n");
let val = {};
if (video.tracks) {
val.tracks = video.tracks;
}
if (video.thumbnail) {
val.thumbnail = video.thumbnail;
}
if (!val) {
if (callback) {
callback();
}
return;
}
dbo
.collection("videos")
.updateOne({ _id: video._id }, { $set: val }, { upsert: false }, err => {
if (err) throw err;
if (callback) {
callback();
}
});
}
function get_video_thumbnail(video, callback) {
let path = config.video_folder + video.path;
if (!fs.existsSync(path)) {
path = config.upload_folder + video.path;
}
ffmpeg(path)
.on("end", () => {
process.stdout.write("Thumbnail for '" + video.title + "' created\n");
let thumbnail =
config.cache_folder + "/video_covers/" + video._id.toString() + ".png";
if (!fs.existsSync(thumbnail)) {
callback(null);
return;
}
let bitmap = fs.readFileSync(thumbnail);
callback("data:image/png;base64, " + bitmap.toString("base64"));
fs.unlinkSync(thumbnail);
})
.on("error", err => {
process.stdout.write(
"Thumbnail error for " + video.title + ": " + err.message + "\n"
);
callback(null);
})
.screenshot({
timestamps: ["20%"],
filename: video._id.toString(),
folder: config.cache_folder + "/video_covers",
size: "346x180"
});
}
function get_video_tracks(video, callback) {
let path = config.video_folder + video.path;
if (!fs.existsSync(path)) {
path = config.upload_folder + video.path;
}
ffmpeg(path).ffprobe((err, data) => {
if (!data) {
return callback([]);
}
let audio_streams = data.streams.filter(f => f.codec_type == "audio");
let return_value = audio_streams.map(m => {
return {
index: m.index,
lang: (m.tags && m.tags.language
? m.tags.language
: m.index
? m.index
: ""
).toString(),
title: (m.tags && m.tags.title
? m.tags.title
: m.index
? m.index
: ""
).toString()
};
});
callback(return_value);
});
}

View File

@ -0,0 +1,79 @@
const { ObjectId } = require('mongodb');
const connector = require("./CONNECTOR");
var dbo;
connector.connect().then((ret) => {
dbo = ret;
});
exports.collection = function (callback) {
dbo
.collection("radios")
.find({})
.sort({ name: 1 })
.toArray((err, result) => {
result.forEach(item => {
item.type = "radio";
});
callback(result);
});
};
exports.byId = function (id, callback) {
dbo
.collection("radios")
.findOne({ _id: ObjectId(id) }, (err, result) => {
if (err) throw err;
callback(result);
});
};
exports.add = function (radio, callback) {
dbo.collection("radios").updateOne(
{ url: radio.url },
{
$set: {
name: radio.name,
url: radio.url
}
},
{ upsert: true },
err => {
if (err) throw err;
dbo.collection("radios").findOne({ url: radio.url }, (err, result) => {
if (err) throw err;
callback(result);
});
}
);
};
exports.delete = function (id, callback) {
dbo
.collection("radios")
.deleteOne({ _id: ObjectId(id) }, (err, result) => {
if (err) throw err;
callback(result);
});
};
exports.update = function (radio, callback) {
dbo.collection("radios").updateOne(
{ _id: radio._id },
{
$set: {
name: radio.name,
url: radio.url,
cover32: radio.cover32,
cover64: radio.cover64,
cover128: radio.cover128
}
},
{ upsert: false },
err => {
if (err) throw err;
if (callback) {
callback();
}
}
);
};

View File

@ -0,0 +1,59 @@
const connector = require("./CONNECTOR");
var dbo;
connector.connect().then((ret) => {
dbo = ret;
});
exports.domains = function (callback) {
dbo
.collection("system")
.findOne({ key: 'domains' })
.then(result => {
if (result) {
callback(result.value);
} else {
callback([]);
}
});
};
exports.allows = function (callback) {
dbo.collection("system")
.findOne({ key: 'allows' })
.then(allows => {
if (!allows) {
callback({
"guests": false,
"register": false,
});
} else {
callback(allows.value);
}
})
}
exports.setAllows = function (allows, callback) {
dbo.collection("system")
.updateOne(
{ key: "allows" },
{ $set: { value: allows } },
{ upsert: true }, err => {
if (err) throw err;
if (callback) {
callback();
}
});
}
exports.setDomains = function (domains, callback) {
dbo.collection("system")
.updateOne(
{ key: "domains" },
{ $set: { value: domains } },
{ upsert: true }, err => {
if (err) throw err;
if (callback) {
callback();
}
});
}

113
services/database/tracks.js Normal file
View File

@ -0,0 +1,113 @@
const { ObjectId } = require('mongodb');
const connector = require("./CONNECTOR");
var dbo;
connector.connect().then((ret) => {
dbo = ret;
dbo.collection("tracks").createIndex({ path: 1, album_id: 1, "disk.no": 1, "track.no": 1 });
});
const server = require("../../server");
const config = server.config;
exports.collection = function (callback) {
dbo
.collection("tracks")
.find()
.toArray((err, result) => {
callback(result);
});
};
exports.byId = function (id, callback) {
dbo
.collection("tracks")
.findOne({ _id: ObjectId(id) }, (err, result) => {
if (err) throw err;
callback(result);
});
};
exports.delete = function (track, callback) {
dbo.collection("tracks").deleteOne(track, err => {
if (err) throw err;
callback();
});
};
exports.exists = function (path, callback) {
dbo
.collection("tracks")
.find({ path: path.replace(config.music_folder, "") })
.limit(1)
.toArray((err, result) => {
callback(result.length > 0);
});
};
exports.moveTo = function (track, callback) {
dbo
.collection("tracks")
.updateOne(
{ _id: ObjectId(track._id) },
{ $set: { album_id: ObjectId(track.album_id) } },
{ upsert: false },
err => {
if (err) throw err;
if (callback) {
callback();
}
});
};
exports.mostListened = function (filter, callback) {
let aggregate = [{
$group: {
_id: "$id",
title: { $last: "$title" },
covers: { $last: "$covers" },
parent: { $last: "$parent" },
counter: { $sum: 1 },
album: { $last: "$album" }
},
}]
if (filter) {
aggregate
.unshift({
$lookup: {
from: "albums",
let: { album_id: "$parent._id" },
pipeline: [{
$match: {
$expr: {
$eq: ["$_id", { $toObjectId: "$$album_id" }]
}
}
}
],
as: "album"
}
}, { $unwind: "$album" })
aggregate
.push({
$project: {
"album.cover256": false,
"album.cover128": false,
"album.cover64": false,
"album.cover32": false
}
}, { $match: { "album.visibility": { $in: filter } } });
} else {
aggregate.unshift({ $match: { type: 'track' } });
}
aggregate.push({ $sort: { counter: -1, _id: -1 } }, { $limit: 6 })
dbo
.collection("history")
.aggregate(aggregate, {
allowDiskUse: true
})
.toArray((err, result) => {
if (err) throw err;
callback(result);
});
};

View File

@ -0,0 +1,49 @@
const { ObjectId } = require('mongodb');
const connector = require("./CONNECTOR");
var dbo;
connector.connect().then((ret) => {
dbo = ret;
dbo.collection("favourites").createIndex({ userId: 1 });
});
exports.collection = function (callback) {
dbo
.collection("users")
.find({}, { password: false })
.toArray((err, result) => {
if (err) throw err;
callback(result);
});
};
exports.favourites = function (id, callback) {
dbo.collection("favourites")
.find({ userId: id })
.toArray((err, result) => {
if (err) throw err;
callback(result);
});
}
exports.insertFavourite = function (item, callback) {
item.itemId = ObjectId(item.itemId);
dbo
.collection("favourites")
.insertOne(item, err => {
if (err) throw err;
if (callback) {
callback();
}
});
}
exports.deleteFavourite = function (item, callback) {
dbo
.collection("favourites")
.deleteMany({ userId: item.userId, itemId: ObjectId(item.itemId) }, (err) => {
if (err) throw err;
if (callback) {
callback();
}
});
}

118
services/database/videos.js Normal file
View File

@ -0,0 +1,118 @@
const { ObjectId } = require('mongodb');
var connector = require("./CONNECTOR");
var dbo;
connector.connect().then((ret) => {
dbo = ret;
dbo.collection("videos").createIndex({ box_id: 1, title: 1 });
});
var server = require("../../server");
var config = server.config;
exports.exists = function (path, callback) {
let short_path = path.replace(config.video_folder, "");
dbo.collection("videos").findOne({ path: short_path }, (err, result) => {
if (err) throw err;
callback(result);
});
};
exports.collection = function (callback) {
dbo
.collection("videos")
.find()
.toArray((err, result) => {
result.forEach(item => {
item.thumbnail = "";
});
callback(result);
});
};
exports.byId = function (id, callback) {
dbo
.collection("videos")
.findOne({ _id: ObjectId(id) }, (err, result) => {
if (err) throw err;
callback(result);
});
};
exports.delete = function (video, callback) {
dbo.collection("videos")
.deleteOne({ _id: ObjectId(video._id) }, err => {
if (err) throw err;
callback();
});
};
exports.moveTo = function (video, callback) {
dbo
.collection("videos")
.updateOne(
{ _id: ObjectId(video._id) },
{ $set: { box_id: ObjectId(video.box_id) } },
{ upsert: false },
err => {
if (err) throw err;
if (callback) {
callback();
}
});
};
exports.mostViewed = function (filter, callback) {
let aggregate = [{
$group: {
_id: "$id",
title: { $last: "$title" },
thumbnail: { $last: "$thumbnail" },
parent: { $last: "$parent" },
counter: { $sum: 1 },
box: { $last: "$box" }
},
}]
if (filter) {
aggregate
.unshift({
$lookup: {
from: "boxes",
let: { box_id: "$parent._id" },
pipeline: [{
$match: {
$expr: {
$eq: ["$_id", { $toObjectId: "$$box_id" }]
}
}
}
],
as: "box"
}
}, { $unwind: "$box" })
aggregate.push({
$project: {
"box.cover256": false,
"box.cover128": false,
"box.cover64": false,
"box.cover32": false,
"box.path": false
}
}, { $match: { "box.visibility": { $in: filter } } });
}
else {
aggregate.unshift({ $match: { type: 'video' } });
}
aggregate.push(
{ $sort: { counter: -1, _id: -1 } },
{ $limit: 12 });
dbo
.collection("history")
.aggregate(aggregate, {
allowDiskUse: true
})
.toArray((err, result) => {
if (err) throw err;
callback(result);
});
};

135
services/files_scanner.js Normal file
View File

@ -0,0 +1,135 @@
var fs = require("fs");
var mime = require("mime-types");
var notifier = require("./notifier");
var database = require("./database");
var server = require("../server");
var config = server.config;
/*
MUSIC
*/
exports.scann_local_music_files = async function (path) {
remove_non_exists_tracks();
scann_local_music_files(path);
};
async function remove_non_exists_tracks() {
database.tracks.collection(tracks => {
tracks.forEach(track => {
let path = config.music_folder + track.path;
if (!fs.existsSync(path)) {
path = config.upload_folder + track.path;
if (!fs.existsSync(path)) {
database.tracks.delete(track, () => {
notifier.emit("track_deleted", track._id);
process.stdout.write("track deleted: " + path + "\n");
});
}
}
});
process.stdout.write("check empty albums\n");
database.albums.empty(albums => {
albums.forEach(album => {
database.albums.delete(album, () => {
notifier.emit("album_deleted", album._id);
process.stdout.write("album deleted: " + album.title + "\n");
});
});
process.stdout.write("check empty artists\n");
database.artists.empty(artists => {
artists.forEach(artist => {
database.artists.delete(artist, () => {
notifier.emit("artist_deleted", artist._id);
process.stdout.write("artist deleted: " + artist.name + "\n");
});
});
});
});
});
}
async function scann_local_music_files(path) {
if (!fs.existsSync(path)) {
return;
}
fs.readdirSync(path).forEach(child => {
if (!child.startsWith(".")) {
var full_path = path + "/" + child;
if (fs.lstatSync(full_path).isDirectory()) {
//setTimeout(() => {
scann_local_music_files(full_path);
//}, 1000);
} else {
var mime_type = mime.lookup(full_path);
if (
mime_type &&
mime_type.startsWith("audio/") &&
mime_type.indexOf("x-mpegurl") == -1 &&
mime_type.indexOf("x-scpls") == -1
) {
let item = { path: full_path, mime: mime_type };
notifier.emit("music_file_found", item);
}
}
}
});
}
/*
VIDEO
*/
exports.scann_local_video_files = async function (path) {
remove_non_exists_videos();
scann_local_video_files(path);
};
async function remove_non_exists_videos() {
database.videos.collection(videos => {
videos.forEach(video => {
let path = config.video_folder + video.path;
if (!fs.existsSync(path)) {
path = config.upload_folder + video.path;
if (!fs.existsSync(path)) {
database.videos.delete(video, () => {
notifier.emit("video_deleted", video._id);
process.stdout.write("video deleted: " + path + "\n");
});
}
}
});
process.stdout.write("check empty boxes\n");
database.boxes.empty(boxes => {
boxes.forEach(box => {
database.boxes.delete(box, () => {
notifier.emit("box_deleted", box._id);
process.stdout.write("box deleted: " + box.title + "\n");
});
});
});
});
}
async function scann_local_video_files(path) {
if (!fs.existsSync(path)) {
return;
}
fs.readdirSync(path).forEach(child => {
if (!child.startsWith(".")) {
var full_path = path + "/" + child;
if (fs.lstatSync(full_path).isDirectory()) {
//setTimeout(() => {
scann_local_video_files(full_path);
//}, 1000);
} else {
var mime_type = mime.lookup(full_path);
if (mime_type && mime_type.startsWith("video/")) {
let item = { path: full_path, mime: mime_type };
notifier.emit("video_file_found", item);
}
}
}
});
}

215
services/music_brainz.js Normal file
View File

@ -0,0 +1,215 @@
var request = require("request");
var notifier = require("./notifier");
var album_cover_queue = [];
var album_cover_requst_is_running = false;
var artist_cover_queue = [];
var artist_cover_queue_is_running = false;
let options = {
headers: {
"User-Agent":
"WebPlay/0.1.0 (https://gitea.com/WebPlay)"
}
};
exports.find_album_cover = function(album) {
album_cover_queue.push(album);
console.log("MB Album: " + album.title + " by " + album.artist_name);
run_album_cover_request();
};
exports.find_artist_cover = function(artist) {
artist_cover_queue.push(artist);
console.log("MB Artist: " + artist.name);
run_artist_cover_request();
};
// ARTIST COVER
async function run_artist_cover_request() {
console.log("started request for artist covers");
if (artist_cover_queue_is_running) {
return;
}
artist_cover_queue_is_running = true;
while (artist_cover_queue && artist_cover_queue.length > 0) {
await sleep(1500);
let artist = artist_cover_queue.shift();
console.log("SHIFT Artist: " + artist.name);
for (let i = 0; i < artist.albums.length; i++) {
await sleep(1500);
if (artist.image_downloaded) {
break;
}
let album = artist.albums[i];
let album_title = album.title.replace("&", "%26").replace("/", "_");
let artist_name = artist.name.replace("&", "%26").replace("/", "_");
let url = `https://musicbrainz.org/ws/2/release/?query=release:${album_title} AND artist:${artist_name}&fmt=json`;
options.url = url;
request(options, (err, res, body) => {
if (err) {
console.log(err);
return;
}
let json = json_parser(body, url);
if (!json) {
return;
}
if (json.releases && json.releases.length > 0) {
let release = json.releases[0];
if (release["artist-credit"] && release["artist-credit"].length > 0) {
let artist_id = release["artist-credit"][0].artist.id;
console.log(artist_id);
get_image_by_artist_id(artist, artist_id);
}
}
});
}
}
artist_cover_queue_is_running = false;
}
async function get_image_by_artist_id(artist, artist_id) {
let url = `https://musicbrainz.org/ws/2/artist/${artist_id}?inc=url-rels&fmt=json`;
console.log(url);
options.url = url;
request(options, (err, res, body) => {
let json = json_parser(body, url);
if (!json) {
return;
}
if (!json.relations) {
return;
}
json.relations.forEach(relation => {
if (relation.type == "image" && relation.url && relation.url.resource) {
let resource = relation.url.resource;
if (resource.includes("commons.wikimedia.org")) {
get_image_by_wikimedia(artist, resource);
} else {
artist.image_downloaded = true;
notifier.emit("found_music_brainz_artist_cover", {
artist: artist,
mb: resource
});
}
}
});
});
}
function get_image_by_wikimedia(artist, url) {
let regex = RegExp("(?<=File:)[^<]*", "g");
let result = regex.exec(url);
if (result) {
console.log(result[0]);
let file = result[0];
let url = `https://en.wikipedia.org/w/api.php?action=query&titles=File:${file}&prop=imageinfo&iiprop=url&iiurlwidth=600&iiurlheight=600&format=json`;
console.log(url);
options.url = url;
request(options, (err, res, body) => {
let json = json_parser(body, url);
if (!json) {
return;
}
if (json.query.pages["-1"].imageinfo[0].thumburl) {
console.log(json.query.pages["-1"].imageinfo[0].thumburl);
artist.image_downloaded = true;
notifier.emit("found_music_brainz_artist_cover", {
artist: artist,
mb: json.query.pages["-1"].imageinfo[0].thumburl
});
}
});
}
}
// ALBUM COVER
async function run_album_cover_request() {
console.log("started request for album covers");
if (album_cover_requst_is_running) {
return;
}
album_cover_requst_is_running = true;
while (album_cover_queue && album_cover_queue.length > 0) {
await sleep(1500);
let album = album_cover_queue.shift();
console.log("SHIFT Album: " + album.title);
let album_title = album.title.replace("&", "%26").replace("/", "_");
let artist_name = album.artist_name.replace("&", "%26").replace("/", "_");
let url = `https://musicbrainz.org/ws/2/release/?query=release:${album_title} AND artist:${artist_name}&fmt=json`;
console.log(url);
options.url = url;
request(options, (err, res, body) => {
let json;
try {
json = JSON.parse(body);
} catch (err) {
console.log(err);
return;
}
if (json.releases && json.releases.length > 0) {
let release_id = json.releases[0].id;
let title = json.releases[0].title;
console.log(release_id + ": " + title);
get_album_cover_url(album, release_id);
}
});
}
album_cover_requst_is_running = false;
}
function get_album_cover_url(album, id) {
let url = "https://coverartarchive.org/release/" + id;
options.url = url;
request(options, (err, res, body) => {
if (err) {
console.log(err);
return;
}
if (res.statusCode != 200) {
return;
}
let json = JSON.parse(body);
if (json.images && json.images.length > 0) {
if (json.images[0].thumbnails.large) {
notifier.emit("found_music_brainz_album_cover", {
album: album,
mb: json.images[0].thumbnails.large
});
} else if (json.images[0].image) {
notifier.emit("found_music_brainz_album_cover", {
album: album,
mb: json.images[0].image
});
}
}
});
}
function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
function json_parser(body, url) {
let json;
try {
json = JSON.parse(body);
} catch (err) {
console.log(err);
console.log(url);
console.log("BODY============BEGIN");
console.log(body);
console.log("BODY============END");
}
return json;
}

4
services/notifier.js Normal file
View File

@ -0,0 +1,4 @@
var events = require('events');
var notifier = new events.EventEmitter();
module.exports = notifier;

36
services/redis/index.js Normal file
View File

@ -0,0 +1,36 @@
const { createClient } = require('redis');
const server = require("../../server");
const config = server.config;
const redisUrl = "redis://" + config.redis.host + ":" + config.redis.port
const client = createClient({
url: redisUrl
});
client.on('error', (err) => console.log('Redis Client Error', err));
client.connect();
client.flushAll();
const expire = 57600; // 24h
exports.set = function (key, value) {
if (value) {
client.set(key, JSON.stringify(value));
client.expire(key, expire);
}
}
exports.get = function (key, callback) {
process.stdout.write("services/redis get '" + key + "'\n");
client.get(key).then(value => {
callback(JSON.parse(value));
});
client.expire(key, expire);
}
exports.flushAll = function () {
client.flushAll();
process.stdout.write("services/redis flushAll()\n");
}

262
services/tag_excluder.js Normal file
View File

@ -0,0 +1,262 @@
var path = require("path");
var notifier = require("./notifier");
var meta_data = require("music-metadata");
var db_manager = require("./database");
var server = require("../server");
var config = server.config;
var music_files_data = [];
var music_files_cover = [];
var parsing_music_data = 0;
var parsing_cover = 0;
var video_files_data = [];
var parsing_video_data = 0;
var exclude_meta_cover_is_running = false;
var exclude_meta_data_is_running = false;
var exclude_path_data_is_running = false;
var sleep_dur = 10;
exports.parsing_music_data = function() {
return parsing_music_data;
};
exports.parsing_video_data = function() {
return parsing_video_data;
};
exports.parsing_cover = function() {
return parsing_cover;
};
var music_timeout_handler_data;
var music_timeout_handler_cover;
var video_timeout_handler_data;
exports.get_buffer_size = () => {
return music_files_data.length;
};
notifier.on("music_file_found", file => {
music_files_data.push(file);
if (music_timeout_handler_data) {
clearTimeout(music_timeout_handler_data);
}
music_timeout_handler_data = setTimeout(exclude_meta_data, 1000);
});
notifier.on("album_cover_request", track => {
music_files_cover.push(track);
if (music_timeout_handler_cover) {
clearTimeout(music_timeout_handler_cover);
}
music_timeout_handler_cover = setTimeout(exclude_meta_cover, 1000);
});
async function exclude_meta_data() {
if (exclude_meta_data_is_running) {
return;
}
process.stdout.write("Start excluding meta data...\n");
exclude_meta_data_is_running = true;
while (music_files_data && music_files_data.length > 0) {
while (parsing_music_data >= 1) {
await sleep(sleep_dur);
}
parsing_music_data++;
let file = music_files_data.shift();
if (!file) {
parsing_music_data--;
continue;
}
db_manager.tracks.exists(file.path, exists => {
if (exists) {
parsing_music_data--;
} else {
let options = {
skipCovers: true,
skipPostHeaders: true,
duration: true
};
meta_data
.parseFile(file.path, options)
.then(meta => {
let title = (meta.common.title || "").trim();
let album = (meta.common.album || "").trim();
let artist = (
meta.common.albumartist ||
meta.common.artist ||
""
).trim();
if (title == "") {
let ext = path.extname(file.path);
title = path.basename(file.path, ext);
}
if (album == "") {
let dir = path.dirname(file.path);
album = path.basename(dir);
}
let item = {
path: file.path,
artist: artist,
album: album,
title: title,
duration: meta.format.duration,
bitrate: meta.format.bitrate,
mime: file.mime,
year: meta.common.year,
track: meta.common.track,
disk: meta.common.disk,
genre: meta.common.genre
};
notifier.emit("metadata_excluded", item);
parsing_music_data--;
})
.catch(err => {
process.stdout.write(file.path);
console.log(err);
parsing_music_data--;
});
}
});
}
process.stdout.write("End excluding meta data...\n");
exclude_meta_data_is_running = false;
notifier.emit("exclude_metadata_finished");
}
async function exclude_meta_cover() {
if (exclude_meta_cover_is_running) {
return;
}
process.stdout.write("Start excluding meta COVER...\n");
exclude_meta_cover_is_running = true;
while (music_files_cover && music_files_cover.length > 0) {
while (parsing_cover >= server.config.cpu_cores) {
await sleep(50);
}
parsing_cover++;
let track = music_files_cover.shift();
if (!track) {
parsing_cover--;
process.stdout.write("file is null");
continue;
}
let path = config.music_folder + track.path;
meta_data
.parseFile(path)
.then(meta => {
parsing_cover--;
if (meta.common.picture) {
track.parent.picture = meta.common.picture;
notifier.emit("metadata_picture_excluded", track.parent);
} else {
notifier.emit("no_metadata_picture_excluded", track.parent);
}
})
.catch(err => {
music_files_cover.splice(0, 1);
parsing_cover--;
notifier.emit("no_metadata_picture_excluded", track.parent);
console.log(err);
});
}
exclude_meta_cover_is_running = false;
process.stdout.write("End excluding meta COVER...\n");
notifier.emit("exclude_metacover_finished");
}
notifier.on("video_file_found", file => {
video_files_data.push(file);
if (video_timeout_handler_data) {
clearTimeout(video_timeout_handler_data);
}
video_timeout_handler_data = setTimeout(exclude_path_data, 1000);
});
async function exclude_path_data() {
if (exclude_path_data_is_running) {
return;
}
process.stdout.write("Start excluding path data...\n");
exclude_path_data_is_running = true;
while (video_files_data && video_files_data.length > 0) {
while (parsing_video_data >= 1) {
await sleep(sleep_dur);
}
parsing_video_data++;
let video = video_files_data.shift();
if (!video) {
parsing_video_data--;
continue;
}
let file_path = video.path;
db_manager.videos.exists(file_path, exists => {
if (exists) {
parsing_video_data--;
if (!exists.thumbnail || !exists.tracks) {
notifier.emit("check_video_details", exists);
}
} else {
let dir = path.dirname(file_path);
let box = path.basename(dir).trim();
let year = 0;
let regex_year = /\s?\([12]\d\d\d\)$/g;
let regex_season = /^seasons?\s*\d+$/gi;
let result = regex_season.exec(box);
if (result) {
let season_dir = path.dirname(dir);
let parent = path.basename(season_dir);
box = parent + " - " + box;
} else {
process.stdout.write("check box title: " + box + "\n");
result = regex_year.exec(box);
if (result) {
box = box.replace(result[0], "");
year = result[0]
.replace("(", "")
.replace(")", "")
.trim();
}
}
let ext = path.extname(file_path);
let title = path.basename(file_path, ext);
let item = {
path: file_path,
box: box,
box_path: dir,
title: title,
mime: video.mime,
year: year
};
notifier.emit("pathdata_excluded", item);
parsing_video_data--;
}
});
}
process.stdout.write("End excluding path data...\n");
exclude_path_data_is_running = false;
notifier.emit("exclude_pathdata_finished");
}
function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}

146
services/the_movie_db.js Normal file
View File

@ -0,0 +1,146 @@
var request = require("request");
var notifier = require("./notifier");
var box_cover_queue = [];
var box_cover_requst_is_running = false;
var key = "3f3692d9c336994625a838a95b9a2ed0";
var poster_root = "https://image.tmdb.org/t/p/w500";
let options = {
headers: {
"User-Agent":
"WebPlay/0.1.0 (https://gitea.com/WebPlay)"
}
};
exports.find_box_cover = function (box) {
box_cover_queue.push(box);
process.stdout.write("TMDB Box: " + box.title + "\n");
run_box_cover_request();
};
async function run_box_cover_request() {
process.stdout.write("started request for box covers\n");
if (box_cover_requst_is_running) {
return;
}
box_cover_requst_is_running = true;
while (box_cover_queue && box_cover_queue.length > 0) {
await sleep(1500);
let box = box_cover_queue.shift();
process.stdout.write("SHIFT box: " + box.title + "\n");
let elements = check_for_season(box.title);
if (elements.season > 0) {
process.stdout.write("Has SEASONS: " + box.title)
cover_by_season_number(box, elements);
} else {
process.stdout.write("Has NO SEASONS: " + box.title)
cover_by_movie_title(box);
}
}
box_cover_requst_is_running = false;
}
function cover_by_movie_title(box) {
options.url = `https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${box.title.replace(' ', '%20')}&page=1&include_adult=false`;
if (box.year > 0) {
options.url = `https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${box.title}&page=1&primary_release_year=${box.year}&include_adult=false`;
}
process.stdout.write("\nSTART: " + options.url + "\n");
try {
request(options, (err, res, body) => {
if (err) {
console.log(err);
return;
}
if (res.statusCode != 200) {
return;
}
process.stdout.write("PARSE BODY FOR: " + box.title + "\n");
process.stdout.write(body);
process.stdout.write("\n\n");
let json = JSON.parse(body);
if (
json.results &&
json.results.length > 0 &&
json.results[0].poster_path
) {
box.tmdb = poster_root + json.results[0].poster_path;
notifier.emit("found_movie_db_box_cover", box);
}
});
} catch (err) {
console.log(err);
process.stdout.write("\nERROR: " + box.title + "\n");
process.stdout.write(options.url + "\n\n");
}
}
function cover_by_season_number(box, elements) {
options.url = `https://api.themoviedb.org/3/search/tv?api_key=${key}&query=${elements.title
}&page=1`;
request(options, (err, res, body) => {
if (err) {
console.log(err);
return;
}
if (res.statusCode != 200) {
return;
}
let json = JSON.parse(body);
if (json.results && json.results.length > 0) {
let id = json.results[0].id;
cover_by_tv_id(id, elements.season, box);
}
});
}
function cover_by_tv_id(id, season, box) {
options.url = `https://api.themoviedb.org/3/tv/${id}/season/${season}?api_key=${key}`;
request(options, (err, res, body) => {
if (err) {
console.log(err);
return;
}
if (res.statusCode != 200) {
return;
}
let json = JSON.parse(body);
if (json.poster_path) {
box.tmdb = poster_root + json.poster_path;
notifier.emit("found_movie_db_box_cover", box);
}
});
}
function check_for_season(title) {
let return_value = { title: title, season: 0 };
let regex_season = /\s*\-?\s*seasons?\s*\d+$/gi;
let result = regex_season.exec(title);
if (result) {
return_value.title = title.replace(result[0], "");
let regex_season_no = /\d*$/g;
result = regex_season_no.exec(title);
return_value.season = parseInt(result[0]);
}
return return_value;
}
function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}

3
start.sh Normal file
View File

@ -0,0 +1,3 @@
#/bin/bash
nohup node ./server.js > output.log &