initial commit
This commit is contained in:
commit
b28a136c58
27 changed files with 4495 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
config.json
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
17
|
31
.vscode/launch.json
vendored
Normal file
31
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "pwa-node",
|
||||||
|
"runtimeVersion": "17",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "kannon-server",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"program": "${workspaceFolder}/kannon.js",
|
||||||
|
"args": [
|
||||||
|
"${workspaceFolder}/example_config_server.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pwa-node",
|
||||||
|
"runtimeVersion": "17",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "kannon-library",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"program": "${workspaceFolder}/kannon.js",
|
||||||
|
"args": [
|
||||||
|
"${workspaceFolder}/example_config_library.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
20
LICENSE.md
Normal file
20
LICENSE.md
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# MIT License
|
||||||
|
**Copyright (c) 2022 Daniel Sommer \<daniel.sommer@velvettear.de\>**
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice (including the next
|
||||||
|
paragraph) shall be included in all copies or substantial portions of the
|
||||||
|
Software.
|
||||||
|
|
||||||
|
**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||||
|
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||||
|
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.**
|
8
README.md
Normal file
8
README.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# kannon
|
||||||
|
|
||||||
|
a multi room audio player
|
||||||
|
|
||||||
|
## requirements
|
||||||
|
|
||||||
|
- node.js
|
||||||
|
- [nvm](https://github.com/nvm-sh/nvm)
|
174
classes/AudioServer.js
Normal file
174
classes/AudioServer.js
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
const sleep = require('../libs/util.js').sleep;
|
||||||
|
|
||||||
|
const net = require('net');
|
||||||
|
const fs = require('fs');
|
||||||
|
const stat = require('fs/promises').stat;
|
||||||
|
|
||||||
|
const Message = require('./Message.js');
|
||||||
|
const EventParser = require('./EventParser.js');
|
||||||
|
|
||||||
|
class AudioServer {
|
||||||
|
|
||||||
|
constructor(file) {
|
||||||
|
this.listen = config?.server?.listen || '0.0.0.0';
|
||||||
|
this.port = 0;
|
||||||
|
this.file = file;
|
||||||
|
this.clients = [];
|
||||||
|
this.broadcastClients = [];
|
||||||
|
this.server = net.createServer();
|
||||||
|
this.eventParser = new EventParser();
|
||||||
|
this.#prepare();
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
if (this.aborted === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const buffer = await this.#waitForBuffer();
|
||||||
|
await this.#waitForAllClients();
|
||||||
|
this.#handleClientConnections();
|
||||||
|
const promises = [];
|
||||||
|
for (let index = 0; index < this.clients.length; index++) {
|
||||||
|
const client = this.clients[index];
|
||||||
|
client.audiostart = Date.now();
|
||||||
|
promises.push(new Promise((resolve, reject) => {
|
||||||
|
client.audiosocket.end(buffer, () => {
|
||||||
|
logger.debug(client.getTag() + ' sent audio file \'' + this.file + '\' after ' + (Date.now() - client.audiostart) + 'ms...');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
await Promise.allSettled(promises);
|
||||||
|
await this.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
async #prepare() {
|
||||||
|
if (server?.clients === undefined || server.clients.length === 0) {
|
||||||
|
logger.warn('there are currently no clients connected, aborting preparation of audio server...')
|
||||||
|
this.aborted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
this.server.listen(this.port, this.listen).on('listening', () => {
|
||||||
|
this.port = this.server.address().port;
|
||||||
|
logger.info('audio server listening on ' + this.listen + ':' + this.port + '...');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
this.server.on('connection', (socket) => {
|
||||||
|
this.#handleConnection(socket);
|
||||||
|
});
|
||||||
|
this.server.on('error', (err) => {
|
||||||
|
reject('an error occured preparing the audio server for file \'' + this.file + '\' > ' + err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const stats = await stat(this.file);
|
||||||
|
const broadcastedTo = await new Message('audiostream-initialize', { port: this.server.address().port, size: stats.size }).broadcast(true);
|
||||||
|
for (let index = 0; index < broadcastedTo.length; index++) {
|
||||||
|
if (broadcastedTo[index]?.status !== 'fulfilled') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.broadcastClients.push(broadcastedTo[index].value);
|
||||||
|
}
|
||||||
|
logger.debug('sent broadcast for audio server to client(s) \'' + this.broadcastClients.toString() + '\'...');
|
||||||
|
this.#bufferFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleConnection(socket) {
|
||||||
|
socket.on('data', (data) => {
|
||||||
|
this.eventParser.parse(data, socket);
|
||||||
|
});
|
||||||
|
this.eventParser.on('audiostream-ready', (clientId, socket) => {
|
||||||
|
let client;
|
||||||
|
for (let index = 0; index < server.clients.length; index++) {
|
||||||
|
if (server.clients[index].id === clientId) {
|
||||||
|
client = server.clients[index];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (client === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
client.audiosocket = socket;
|
||||||
|
this.clients.push(client);
|
||||||
|
logger.debug(client.getTag() + ' connected to audio server...');
|
||||||
|
this.broadcastClients.splice(this.broadcastClients.indexOf(clientId), 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleClientConnections() {
|
||||||
|
for (let index = 0; index < this.clients.length; index++) {
|
||||||
|
const client = this.clients[index];
|
||||||
|
client.audiosocket.on('error', (error) => {
|
||||||
|
logger.error(client.getTag() + ' encountered an error: ' + error);
|
||||||
|
});
|
||||||
|
client.audiosocket.on('end', () => {
|
||||||
|
logger.debug(client.getTag() + ' ended audio socket');
|
||||||
|
});
|
||||||
|
client.audiosocket.on('close', (hadError) => {
|
||||||
|
let msg = client.getTag() + ' closed audio socket';
|
||||||
|
if (hadError === true) {
|
||||||
|
msg += ' after an error';
|
||||||
|
}
|
||||||
|
logger.debug(msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #waitForAllClients() {
|
||||||
|
while (this.broadcastClients.length > 0) {
|
||||||
|
await sleep(1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #waitForBuffer() {
|
||||||
|
while (this.buffer === undefined) {
|
||||||
|
await sleep(1);
|
||||||
|
}
|
||||||
|
return this.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #bufferFile() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const buffer = [];
|
||||||
|
const stream = fs.createReadStream(this.file);
|
||||||
|
stream.on('data', (data) => {
|
||||||
|
buffer.push(data);
|
||||||
|
});
|
||||||
|
stream.on('close', () => {
|
||||||
|
this.buffer = Buffer.concat(buffer);
|
||||||
|
logger.debug('buffering file \'' + this.file + '\' took ' + (Date.now() - timestamp) + 'ms (length: ' + this.buffer.length + ' bytes)');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
stream.on('error', (error) => {
|
||||||
|
// TODO: handle with try/catch
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy() {
|
||||||
|
this.eventParser.removeAllListeners('audiostream-ready');
|
||||||
|
for (let index = 0; index < this.clients.length; index++) {
|
||||||
|
const audiosocket = this.clients[index].audiosocket;
|
||||||
|
if (audiosocket.destroyed === true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
audiosocket.destroy();
|
||||||
|
}
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
this.server.close((err) => {
|
||||||
|
if (err !== undefined) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AudioServer;
|
94
classes/Client.js
Normal file
94
classes/Client.js
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
const Heartbeat = require('./Heartbeat.js');
|
||||||
|
const EventParser = require('./EventParser.js');
|
||||||
|
|
||||||
|
let clientId = -1;
|
||||||
|
|
||||||
|
class Client {
|
||||||
|
|
||||||
|
constructor(socket) {
|
||||||
|
clientId++;
|
||||||
|
this.id = clientId;
|
||||||
|
this.socket = socket;
|
||||||
|
this.eventParser = new EventParser();
|
||||||
|
this.heartbeat = new Heartbeat(this);
|
||||||
|
this.#listenForEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
getId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAddress() {
|
||||||
|
return this.socket.remoteAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPort() {
|
||||||
|
return this.socket.remotePort;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTag() {
|
||||||
|
return '[' + this.getId() + '] ' + this.getAddress() + ':' + this.getPort() + ' >>';
|
||||||
|
}
|
||||||
|
|
||||||
|
#listenForEvents() {
|
||||||
|
logger.debug(this.getTag() + ' connected to communication server...');
|
||||||
|
this.socket.on('timeout', () => {
|
||||||
|
this.#handleEventTimeout();
|
||||||
|
});
|
||||||
|
this.socket.on('close', () => {
|
||||||
|
this.#handleEventClose()
|
||||||
|
});
|
||||||
|
this.socket.on('end', () => {
|
||||||
|
this.#handleEventEnd()
|
||||||
|
});
|
||||||
|
this.socket.on('data', (data) => {
|
||||||
|
this.#handleEventData(data)
|
||||||
|
});
|
||||||
|
this.heartbeat.on('timeout', () => {
|
||||||
|
this.#handleEventHeartbeatTimeout();
|
||||||
|
});
|
||||||
|
this.heartbeat.on('latency', (data) => {
|
||||||
|
this.#handleEventLatency(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async #handleEventData(data) {
|
||||||
|
this.eventParser.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleEventTimeout() {
|
||||||
|
logger.warn(this.getTag() + ' timed out');
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleEventHeartbeatTimeout() {
|
||||||
|
logger.warn(this.getTag() + ' heartbeat timed out');
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleEventClose() {
|
||||||
|
logger.debug(this.getTag() + ' closed socket to communication server');
|
||||||
|
server.removeClient(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleEventEnd() {
|
||||||
|
logger.debug(this.getTag() + ' ended socket to communication server');
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleEventLatency(data) {
|
||||||
|
this.latency = data;
|
||||||
|
logger.debug(this.getTag() + ' latency: ' + JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.heartbeat.destroy();
|
||||||
|
this.heartbeat.removeAllListeners('timeout');
|
||||||
|
this.heartbeat.removeAllListeners('latency');
|
||||||
|
this.socket.end();
|
||||||
|
this.socket.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Client;
|
75
classes/Database.js
Normal file
75
classes/Database.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
const Sequelize = require('sequelize');
|
||||||
|
const path = require('path');
|
||||||
|
const readdir = require('fs/promises').readdir;
|
||||||
|
|
||||||
|
class Database {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
this.connection = await this.#connect();
|
||||||
|
this.models = await this.#getModels();
|
||||||
|
await this.#createTables();
|
||||||
|
}
|
||||||
|
|
||||||
|
async #connect() {
|
||||||
|
if (this.connection !== undefined) {
|
||||||
|
return this.connection;
|
||||||
|
}
|
||||||
|
if (config?.database === undefined) {
|
||||||
|
throw new Error('missing database config');
|
||||||
|
}
|
||||||
|
let connection = new Sequelize(config.database.database, config.database.username, config.database.password, {
|
||||||
|
dialect: config.database.dialect,
|
||||||
|
host: config.database.host,
|
||||||
|
port: config.database.port,
|
||||||
|
storage: config.database.storage,
|
||||||
|
logging: false
|
||||||
|
});
|
||||||
|
await connection.authenticate();
|
||||||
|
logger.info('successfully connected to the database');
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #getModels() {
|
||||||
|
let modelsDirectory = path.resolve(path.join(__dirname, '../models'));
|
||||||
|
const files = await readdir(modelsDirectory);
|
||||||
|
const models = {};
|
||||||
|
for (let index = 0; index < files.length; index++) {
|
||||||
|
let modelFile = path.join(modelsDirectory, files[index]);
|
||||||
|
if (path.extname(modelFile) !== '.js') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const model = require(modelFile);
|
||||||
|
if (model.tableName === undefined || model.sync === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
modelFile = path.basename(modelFile);
|
||||||
|
modelFile = modelFile.substring(0, modelFile.indexOf(path.extname(modelFile)));
|
||||||
|
models[modelFile] = model;
|
||||||
|
}
|
||||||
|
return models;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #createTables() {
|
||||||
|
if (this.models === undefined || this.models.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const models = Object.values(this.models);
|
||||||
|
for (let index = 0; index < models.length; index++) {
|
||||||
|
const model = models[index];
|
||||||
|
try {
|
||||||
|
const timestamp = new Date().getTime();
|
||||||
|
await model.sync({ alter: true });
|
||||||
|
logger.debug('creation/alteration of table \'' + model.tableName + '\' finished after ' + (new Date().getTime() - timestamp) + 'ms');
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('an error occured creating table \'' + model.tableName + '\' > ' + err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Database;
|
31
classes/EventParser.js
Normal file
31
classes/EventParser.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
const { EVENT_DELIMITER } = require('../libs/constants.js');
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
|
class EventParser extends EventEmitter {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.buffer = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(data, socket) {
|
||||||
|
if (data === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.buffer += data;
|
||||||
|
const indexOfEnd = this.buffer.indexOf(EVENT_DELIMITER);
|
||||||
|
if (indexOfEnd === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const event = JSON.parse(this.buffer.substring(0, indexOfEnd));
|
||||||
|
this.buffer = '';
|
||||||
|
if (event.id === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const eventId = event.id.toLowerCase();
|
||||||
|
this.emit(eventId, event.data, socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = EventParser;
|
60
classes/Heartbeat.js
Normal file
60
classes/Heartbeat.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
const sleep = require('../libs/util.js').sleep;
|
||||||
|
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
const Message = require('./Message.js');
|
||||||
|
|
||||||
|
class Heartbeat extends EventEmitter {
|
||||||
|
|
||||||
|
constructor(client) {
|
||||||
|
super();
|
||||||
|
this.interval = config?.server?.heartbeat || 10000;
|
||||||
|
this.client = client;
|
||||||
|
this.#listenForPingPong();
|
||||||
|
this.#sendPing();
|
||||||
|
}
|
||||||
|
|
||||||
|
async #sendPing() {
|
||||||
|
if (this.timeout !== undefined) {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
}
|
||||||
|
if (this.alive === false) {
|
||||||
|
this.emit('timeout');
|
||||||
|
return;
|
||||||
|
} else if (this.alive === undefined) {
|
||||||
|
await sleep(this.interval);
|
||||||
|
}
|
||||||
|
this.alive = false;
|
||||||
|
await new Message('ping', { server: Date.now() }).send(this.client);
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
this.#sendPing();
|
||||||
|
}, this.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #listenForPingPong() {
|
||||||
|
this.client.eventParser.on('ping', () => {
|
||||||
|
logger.debug(this.client.getTag() + ' handling event \'ping\', responding with \'pong\'...');
|
||||||
|
new Message('pong').send(this.client);
|
||||||
|
});
|
||||||
|
this.client.eventParser.on('pong', (data) => {
|
||||||
|
logger.debug(this.client.getTag() + ' handling event \'pong\'...');
|
||||||
|
const now = Date.now();
|
||||||
|
this.alive = true;
|
||||||
|
this.emit('latency', {
|
||||||
|
toClient: (data.client - data.server),
|
||||||
|
fromClient: (now - data.client),
|
||||||
|
roundtrip: (now - data.server)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this.timeout !== undefined) {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
}
|
||||||
|
this.client.eventParser.removeAllListeners('ping');
|
||||||
|
this.client.eventParser.removeAllListeners('pong');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Heartbeat;
|
145
classes/Logger.js
Normal file
145
classes/Logger.js
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
|
// constants
|
||||||
|
const LOG_PREFIX_DEBUG = 'debug';
|
||||||
|
const LOG_PREFIX_INFO = 'info';
|
||||||
|
const LOG_PREFIX_WARNING = 'warning';
|
||||||
|
const LOG_PREFIX_ERROR = 'error';
|
||||||
|
const LOGLEVEL_DEBUG = 0;
|
||||||
|
const LOGLEVEL_INFO = 1;
|
||||||
|
const LOGLEVEL_WARNING = 2;
|
||||||
|
const LOGLEVEL_ERROR = 3;
|
||||||
|
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
|
||||||
|
constructor(loglevel, timestamp) {
|
||||||
|
this.setLogLevel(loglevel);
|
||||||
|
this.setTimestamp(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the loglevel
|
||||||
|
setLogLevel(value) {
|
||||||
|
switch (value) {
|
||||||
|
case LOG_PREFIX_DEBUG:
|
||||||
|
case LOGLEVEL_DEBUG:
|
||||||
|
this.loglevel = LOGLEVEL_DEBUG;
|
||||||
|
break;
|
||||||
|
case LOG_PREFIX_INFO:
|
||||||
|
case LOGLEVEL_INFO:
|
||||||
|
this.loglevel = LOGLEVEL_INFO;
|
||||||
|
break;
|
||||||
|
case LOG_PREFIX_WARNING:
|
||||||
|
case LOGLEVEL_WARNING:
|
||||||
|
this.loglevel = LOGLEVEL_WARNING;
|
||||||
|
break;
|
||||||
|
case LOG_PREFIX_ERROR:
|
||||||
|
case LOGLEVEL_ERROR:
|
||||||
|
this.loglevel = LOGLEVEL_ERROR;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.loglevel = LOGLEVEL_INFO;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the timestamp format
|
||||||
|
setTimestamp(value) {
|
||||||
|
this.timestamp = value || 'DD.MM.YYYY HH:mm:ss:SS';
|
||||||
|
}
|
||||||
|
|
||||||
|
// log a http request - response object
|
||||||
|
http(object) {
|
||||||
|
if (object === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let message = '[' + object.request.method + ':' + object.code + '] url: \'' + object.request.url + '\'';
|
||||||
|
let counter = 1;
|
||||||
|
for (let param in object.request.body) {
|
||||||
|
message += ', parameter ' + counter + ': \'' + param + '=' + object.request.body[param] + '\'';
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
if (object.request.timestamp) {
|
||||||
|
message += ' > ' + (new Date().getTime() - object.request.timestamp) + 'ms';
|
||||||
|
}
|
||||||
|
if (object.data) {
|
||||||
|
message += ' > data: ' + object.data;
|
||||||
|
}
|
||||||
|
if (object.code != 200) {
|
||||||
|
error(message.trim());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.debug(message.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefix log with 'info'
|
||||||
|
info(message) {
|
||||||
|
if (this.loglevel > LOGLEVEL_INFO) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.trace(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefix log with 'info'
|
||||||
|
warn(message) {
|
||||||
|
if (this.loglevel > LOGLEVEL_WARNING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.trace(message, 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefix log with 'debug'
|
||||||
|
debug(message) {
|
||||||
|
if (this.loglevel > LOGLEVEL_DEBUG) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.trace(message, 'debug');
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefix log with 'error'
|
||||||
|
error(message) {
|
||||||
|
if (this.loglevel > LOGLEVEL_ERROR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (message.stack) {
|
||||||
|
this.trace(message.stack, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (message.errors !== undefined) {
|
||||||
|
for (let index = 0; index < message.errors.length; index++) {
|
||||||
|
this.trace(message.errors[index], 'error');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.trace(message.toString(), 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// default logging function
|
||||||
|
trace(message, prefix) {
|
||||||
|
if (message === undefined || message === null || message.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (prefix === undefined || prefix === null || prefix.length === 0) {
|
||||||
|
prefix = 'info';
|
||||||
|
}
|
||||||
|
let print;
|
||||||
|
switch (prefix) {
|
||||||
|
case 'error':
|
||||||
|
print = console.error;
|
||||||
|
break;
|
||||||
|
case 'debug':
|
||||||
|
print = console.debug;
|
||||||
|
break;
|
||||||
|
case 'warning':
|
||||||
|
print = console.warn;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
print = console.log;
|
||||||
|
}
|
||||||
|
message = moment().format(this.timestamp) + ' | ' + prefix + ' > ' + message;
|
||||||
|
print(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Logger;
|
47
classes/Message.js
Normal file
47
classes/Message.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
const { EVENT_DELIMITER } = require('../libs/constants.js');
|
||||||
|
|
||||||
|
class Message {
|
||||||
|
|
||||||
|
constructor(id, data) {
|
||||||
|
this.id = id;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getData() {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return JSON.stringify(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(client, addClientId) {
|
||||||
|
if (client === undefined) {
|
||||||
|
return this.broadcast();
|
||||||
|
}
|
||||||
|
if (addClientId) {
|
||||||
|
this.data.clientId = client.id;
|
||||||
|
}
|
||||||
|
const data = this.toString();
|
||||||
|
logger.debug(client.getTag() + ' sending data: ' + data);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
client.socket.write(this.toString() + EVENT_DELIMITER, resolve);
|
||||||
|
});
|
||||||
|
return client.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async broadcast(addClientId) {
|
||||||
|
const promises = [];
|
||||||
|
for (let index = 0; index < server.clients.length; index++) {
|
||||||
|
promises.push(this.send(server.clients[index], addClientId));
|
||||||
|
}
|
||||||
|
return await Promise.allSettled(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Message;
|
213
classes/Queue.js
Normal file
213
classes/Queue.js
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
const metadata = require('../libs/metadata.js');
|
||||||
|
const ext = require('path').extname;
|
||||||
|
|
||||||
|
const { FS_EVENT_ADD: EVENT_ADD, FS_EVENT_UNLINK: EVENT_UNLINK, FS_EVENT_CHANGE: EVENT_CHANGE } = require('../libs/constants.js');
|
||||||
|
|
||||||
|
class Queue {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.queue = [];
|
||||||
|
this.filter = this.getFilter();
|
||||||
|
this.handleQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
add(event, file, stats) {
|
||||||
|
if (file === undefined || !this.filter.includes(ext(file))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const element = { file: file };
|
||||||
|
switch (event) {
|
||||||
|
case EVENT_ADD:
|
||||||
|
element.event = EVENT_ADD;
|
||||||
|
break;
|
||||||
|
case EVENT_UNLINK:
|
||||||
|
element.event = EVENT_UNLINK;
|
||||||
|
break;
|
||||||
|
case EVENT_CHANGE:
|
||||||
|
element.event = EVENT_CHANGE;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.queue.push(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleQueue() {
|
||||||
|
if (this.queue.length === 0) {
|
||||||
|
if (this.timeout === undefined) {
|
||||||
|
this.timeout = 10;
|
||||||
|
} else {
|
||||||
|
if (this.timeout > 60000) {
|
||||||
|
this.timeout = 60000;
|
||||||
|
} else {
|
||||||
|
this.timeout += 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug('queue is currently empty - sleeping for ' + this.timeout + 'ms...');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.handleQueue()
|
||||||
|
}, this.timeout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.timeout !== undefined) {
|
||||||
|
this.timeout = undefined;
|
||||||
|
}
|
||||||
|
const element = this.queue[0];
|
||||||
|
this.queue.shift();
|
||||||
|
const timestamp = new Date().getTime();
|
||||||
|
logger.debug('handling event \'' + element.event + '\' for queued file \'' + element.file + '\'...');
|
||||||
|
switch (element.event) {
|
||||||
|
case EVENT_ADD:
|
||||||
|
await this.eventAdd(element.file);
|
||||||
|
break;
|
||||||
|
case EVENT_UNLINK:
|
||||||
|
await this.eventUnlink(element.file);
|
||||||
|
break;
|
||||||
|
case EVENT_CHANGE:
|
||||||
|
await this.eventChange(element.file);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
logger.debug('event \'' + element.event + '\' for file \'' + element.file + '\' handled after ' + (new Date().getTime() - timestamp) + 'ms');
|
||||||
|
this.handleQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
async eventAdd(file) {
|
||||||
|
if (file === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tags = await metadata.parseFile(file);
|
||||||
|
const artists = await this.addArtist(tags);
|
||||||
|
const album = await this.addAlbum(tags);
|
||||||
|
const track = await this.addTrack(tags, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
async eventUnlink(file) {
|
||||||
|
if (file === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await database.models.Track.destroy({
|
||||||
|
where: { file: file }
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async eventChange(file) {
|
||||||
|
if (file === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addArtist(tags) {
|
||||||
|
if (tags?.common?.artist === undefined && tags?.common?.artists === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let artist = tags.common.artist;
|
||||||
|
const artists = tags.common.artists || [];
|
||||||
|
if (artist !== undefined && !artists.includes(artist)) {
|
||||||
|
artists.push(artist);
|
||||||
|
}
|
||||||
|
for (let index = 0; index < artists.length; index++) {
|
||||||
|
artist = artists[index].trim();
|
||||||
|
try {
|
||||||
|
const [element, created] = await database.models.Artist.findOrCreate({
|
||||||
|
where: { name: artist }
|
||||||
|
});
|
||||||
|
if (!created) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
logger.debug('created artist ' + JSON.stringify(element));
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('error finding or creating artist \'' + JSON.stringify(tags) + '\' > ' + err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addAlbum(tags) {
|
||||||
|
if (tags?.common?.album === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const [element, created] = await database.models.Album.findOrCreate({
|
||||||
|
where: {
|
||||||
|
name: tags.common.album
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!created) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.debug('created album ' + JSON.stringify(element));
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('error finding or creating album \'' + JSON.stringify(tags) + '\' > ' + err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addTrack(tags, file) {
|
||||||
|
if (tags?.common?.title === undefined || file === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const where = {
|
||||||
|
file: file,
|
||||||
|
title: tags.common.title
|
||||||
|
};
|
||||||
|
if (tags?.common?.year !== undefined) {
|
||||||
|
where.year = tags.common.year;
|
||||||
|
}
|
||||||
|
if (tags?.common?.duration !== undefined) {
|
||||||
|
where.duration = tags.common.duration;
|
||||||
|
}
|
||||||
|
if (tags?.common?.comment !== undefined) {
|
||||||
|
let comment = '';
|
||||||
|
if (Array.isArray(tags.common.comment)) {
|
||||||
|
for (let index = 0; index < tags.common.comment.length; index++) {
|
||||||
|
if (comment.length > 0) {
|
||||||
|
comment += '\n';
|
||||||
|
}
|
||||||
|
comment += tags.common.comment[index];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
comment = tags.common.comment;
|
||||||
|
}
|
||||||
|
where.comment = comment;
|
||||||
|
}
|
||||||
|
if (tags?.common?.disk?.no !== undefined) {
|
||||||
|
where.diskno = tags.common.disk.no;
|
||||||
|
}
|
||||||
|
if (tags?.common?.disk?.of !== undefined) {
|
||||||
|
where.diskof = tags.common.disk.of;
|
||||||
|
}
|
||||||
|
if (tags?.common?.track?.no !== undefined) {
|
||||||
|
where.trackno = tags.common.track.no;
|
||||||
|
}
|
||||||
|
if (tags?.common?.track?.of !== undefined) {
|
||||||
|
where.trackof = tags.common.track.of;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const [element, created] = await database.models.Track.findOrCreate({
|
||||||
|
where: where
|
||||||
|
});
|
||||||
|
if (!created) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.debug('created track ' + JSON.stringify(element));
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('error finding or creating track \'' + JSON.stringify(tags) + '\' > ' + err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilter() {
|
||||||
|
let filter = config?.library?.formats || ['.mp3'];
|
||||||
|
for (let index = 0; index < filter.length; index++) {
|
||||||
|
if (filter[index].startsWith(".")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
filter[index] = '.' + filter[index];
|
||||||
|
}
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Queue;
|
62
classes/Server.js
Normal file
62
classes/Server.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
const net = require('net');
|
||||||
|
|
||||||
|
const Client = require('./Client.js');
|
||||||
|
|
||||||
|
const AudioServer = require('./AudioServer.js');
|
||||||
|
|
||||||
|
class Server {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.listen = config?.server?.listen || '0.0.0.0';
|
||||||
|
this.port = config?.server?.port || 0;
|
||||||
|
this.clients = [];
|
||||||
|
this.server = net.createServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
// setInterval(() => {
|
||||||
|
// const audioServer = new AudioServer('/mnt/kingston/downloads/DOPESMOKER.flac');
|
||||||
|
// audioServer.start();
|
||||||
|
// }, 10000);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.server.listen(this.port, this.listen).on('listening', () => {
|
||||||
|
this.port = this.server.address().port;
|
||||||
|
logger.info('communication server listening on ' + this.listen + ':' + this.port + '...');
|
||||||
|
});
|
||||||
|
this.server.on('connection', (socket) => {
|
||||||
|
this.#addClient(socket);
|
||||||
|
});
|
||||||
|
this.server.on('error', (err) => {
|
||||||
|
reject('an unexpected error occured: ' + err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.server === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let index = 0; index < this.connections.length; index++) {
|
||||||
|
this.connections[index].destroy();
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.server.close();
|
||||||
|
this.server = undefined;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#addClient(socket) {
|
||||||
|
this.clients.push(new Client(socket));
|
||||||
|
const audioServer = new AudioServer('/mnt/kingston/downloads/DOPESMOKER.flac');
|
||||||
|
audioServer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeClient(client) {
|
||||||
|
client.destroy();
|
||||||
|
this.clients.splice(this.clients.indexOf(client), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Server;
|
42
classes/Watcher.js
Normal file
42
classes/Watcher.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
const path = require('path');
|
||||||
|
const chokidar = require('chokidar');
|
||||||
|
|
||||||
|
const { FS_EVENT_ADD: EVENT_ADD, FS_EVENT_UNLINK: EVENT_UNLINK, FS_EVENT_CHANGE: EVENT_CHANGE } = require('../libs/constants.js');
|
||||||
|
|
||||||
|
class Watcher {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.#initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
#initialize() {
|
||||||
|
if (config?.library === undefined) {
|
||||||
|
throw new Error('library not defined');
|
||||||
|
}
|
||||||
|
if (config?.library?.sources === undefined || config?.library?.sources.length === 0) {
|
||||||
|
throw new Error('no library sources defined');
|
||||||
|
}
|
||||||
|
for (let index = 0; index < config.library.sources.length; index++) {
|
||||||
|
const directory = path.resolve(config.library.sources[index]);
|
||||||
|
logger.debug('watching directory \'' + directory + '\'...');
|
||||||
|
this.#handleEvents(chokidar.watch(directory));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleEvents(watcher) {
|
||||||
|
if (watcher === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
watcher.on(EVENT_ADD, (file, stats) => {
|
||||||
|
queue.add(EVENT_ADD, file, stats);
|
||||||
|
});
|
||||||
|
watcher.on(EVENT_UNLINK, (file, stats) => {
|
||||||
|
queue.add(EVENT_UNLINK, file, stats);
|
||||||
|
});
|
||||||
|
watcher.on(EVENT_CHANGE, (file, stats) => {
|
||||||
|
queue.add(EVENT_CHANGE, file, stats);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Watcher, EVENT_ADD, EVENT_UNLINK;
|
28
example_config_library.json
Normal file
28
example_config_library.json
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"level": "debug",
|
||||||
|
"timestamp": "DD.MM.YYYY HH:mm:ss:SS"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"enabled": true,
|
||||||
|
"sources": [
|
||||||
|
"/home/velvettear/downloads"
|
||||||
|
],
|
||||||
|
"formats": [
|
||||||
|
"mp3",
|
||||||
|
"flac"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"dialect": "postgres",
|
||||||
|
"storage": "/tmp/kannon.sqlite",
|
||||||
|
"host": "192.168.104.135",
|
||||||
|
"port": 5432,
|
||||||
|
"database": "kannon",
|
||||||
|
"username": "postgres",
|
||||||
|
"password": "$Velvet90"
|
||||||
|
}
|
||||||
|
}
|
24
example_config_server.json
Normal file
24
example_config_server.json
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"enabled": true,
|
||||||
|
"listen": "0.0.0.0",
|
||||||
|
"port": 3000,
|
||||||
|
"heartbeat": 10000
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"level": "debug",
|
||||||
|
"timestamp": "DD.MM.YYYY HH:mm:ss:SS"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"dialect": "postgres",
|
||||||
|
"storage": "/tmp/kannon.sqlite",
|
||||||
|
"host": "192.168.104.135",
|
||||||
|
"port": 5432,
|
||||||
|
"database": "kannon",
|
||||||
|
"username": "postgres",
|
||||||
|
"password": "$Velvet90"
|
||||||
|
}
|
||||||
|
}
|
68
kannon.js
Normal file
68
kannon.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
const util = require('./libs/util.js');
|
||||||
|
const packageJSON = require('./package.json');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const Logger = require('./classes/Logger.js');
|
||||||
|
const Server = require('./classes/Server.js');
|
||||||
|
const Database = require('./classes/Database.js');
|
||||||
|
const Queue = require('./classes/Queue.js');
|
||||||
|
const Watcher = require('./classes/Watcher.js');
|
||||||
|
|
||||||
|
const INTERRUPTS = ['beforeExit', 'SIGINT', 'SIGTERM'];
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
global.logger = new Logger();
|
||||||
|
let configPath = path.resolve(process.argv[2] || __dirname + '/config.json');
|
||||||
|
try {
|
||||||
|
global.config = require(configPath);
|
||||||
|
global.logger.setLogLevel(global.config?.log?.level);
|
||||||
|
global.logger.setTimestamp(global.config?.log?.timestamp);
|
||||||
|
} catch (err) {
|
||||||
|
exit('could not read config file at \'' + configPath + '\'');
|
||||||
|
}
|
||||||
|
handleExit();
|
||||||
|
global.logger.info("launching " + packageJSON.name + " " + packageJSON.version + "...");
|
||||||
|
try {
|
||||||
|
// connect to the database and create/alter tables
|
||||||
|
global.database = new Database();
|
||||||
|
await global.database.initialize();
|
||||||
|
// socket server
|
||||||
|
if (util.isEnabled(global.config.server)) {
|
||||||
|
global.server = new Server();
|
||||||
|
await global.server.start();
|
||||||
|
}
|
||||||
|
// queue and watcher
|
||||||
|
if (util.isEnabled(global.config.library)) {
|
||||||
|
global.queue = new Queue();
|
||||||
|
global.watcher = new Watcher();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
exit(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleExit() {
|
||||||
|
for (var index = 0; index < INTERRUPTS.length; index++) {
|
||||||
|
process.on(INTERRUPTS[index], async (code) => {
|
||||||
|
exit(undefined, code);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exit(err, code) {
|
||||||
|
if (code === undefined) {
|
||||||
|
code = 0;
|
||||||
|
if (err !== undefined) {
|
||||||
|
code = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (err) {
|
||||||
|
logger.error(err);
|
||||||
|
logger.error(packageJSON.name + ' ' + packageJSON.version + ' ended due to an error');
|
||||||
|
} else {
|
||||||
|
logger.info(packageJSON.name + ' ' + packageJSON.version + ' shutting down gracefully')
|
||||||
|
}
|
||||||
|
process.exit(code);
|
||||||
|
}
|
12
kannon.service
Normal file
12
kannon.service
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[Unit]
|
||||||
|
Description=kannon
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
WorkingDirectory=/opt/kannon
|
||||||
|
ExecStart=/opt/nvm/nvm-exec node kannon.js
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
10
libs/constants.js
Normal file
10
libs/constants.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
module.exports = {
|
||||||
|
FS_EVENT_ADD: 'add',
|
||||||
|
FS_EVENT_UNLINK: 'unlink',
|
||||||
|
FS_EVENT_CHANGE: 'change',
|
||||||
|
|
||||||
|
SOCKET_EVENT_PING: 'ping',
|
||||||
|
SOCKET_EVENT_PONG: 'pong',
|
||||||
|
|
||||||
|
EVENT_DELIMITER: '<<< kannon >>>'
|
||||||
|
}
|
16
libs/metadata.js
Normal file
16
libs/metadata.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
const metadata = require('music-metadata');
|
||||||
|
|
||||||
|
async function parseFile(file) {
|
||||||
|
if (file === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await metadata.parseFile(file);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('an error occurred parsing the file \'' + file + '\' > ' + err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
parseFile
|
||||||
|
}
|
32
libs/util.js
Normal file
32
libs/util.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
function isEnabled(parameter) {
|
||||||
|
return isSet(parameter?.enabled) && parameter.enabled === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDisabled(parameter) {
|
||||||
|
return isSet(parameter?.enabled) && parameter.enabled === false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSet(parameter) {
|
||||||
|
return parameter !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUnset(parameter) {
|
||||||
|
return !isSet(parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sleep(ms) {
|
||||||
|
if (isNaN(ms)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isEnabled,
|
||||||
|
isDisabled,
|
||||||
|
isSet,
|
||||||
|
isUnset,
|
||||||
|
sleep
|
||||||
|
}
|
7
models/Album.js
Normal file
7
models/Album.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
|
const Album = database.connection.define("album", {
|
||||||
|
name: DataTypes.TEXT
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Album;
|
7
models/Artist.js
Normal file
7
models/Artist.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
|
const Artist = database.connection.define("artist", {
|
||||||
|
name: DataTypes.TEXT
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Artist;
|
15
models/Track.js
Normal file
15
models/Track.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
|
const Track = database.connection.define("track", {
|
||||||
|
title: DataTypes.TEXT,
|
||||||
|
year: DataTypes.INTEGER,
|
||||||
|
duration: DataTypes.FLOAT,
|
||||||
|
comment: DataTypes.TEXT,
|
||||||
|
diskno: DataTypes.INTEGER,
|
||||||
|
diskof: DataTypes.INTEGER,
|
||||||
|
trackno: DataTypes.INTEGER,
|
||||||
|
trackof: DataTypes.INTEGER,
|
||||||
|
file: DataTypes.TEXT
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Track;
|
3243
package-lock.json
generated
Normal file
3243
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
27
package.json
Normal file
27
package.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "kannon",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "a multi room audio player",
|
||||||
|
"main": "kannon.js",
|
||||||
|
"scripts": {},
|
||||||
|
"keywords": [
|
||||||
|
"audio",
|
||||||
|
"player",
|
||||||
|
"multi room"
|
||||||
|
],
|
||||||
|
"author": "Daniel Sommer <daniel.sommer@velvettear.de>",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.velvettear.de/velvettear/kannon.git"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"chokidar": "^3.5.3",
|
||||||
|
"moment": "^2.29.1",
|
||||||
|
"music-metadata": "^7.12.3",
|
||||||
|
"pg": "^8.7.3",
|
||||||
|
"pg-hstore": "^2.3.4",
|
||||||
|
"sequelize": "^6.18.0",
|
||||||
|
"sqlite3": "^5.0.2"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue