stagelinq 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +2 -0
- package/.prettierignore +7 -0
- package/.prettierrc.js +10 -0
- package/.vscode/launch.json +40 -0
- package/.vscode/tasks.json +24 -0
- package/Databases/Databases.ts +69 -0
- package/Databases/DbConnection.ts +42 -0
- package/Databases/index.ts +2 -0
- package/LICENSE +674 -0
- package/LogEmitter/index.ts +51 -0
- package/README.md +83 -0
- package/StageLinq/index.ts +58 -0
- package/TODO.md +5 -0
- package/albumArt/index.ts +2 -0
- package/albumArt/maybeDownloadFiles.ts +34 -0
- package/cli/index.ts +142 -0
- package/devices/Player.ts +182 -0
- package/devices/PlayerMessageQueue.ts +62 -0
- package/index.ts +5 -0
- package/network/NetworkDevice.ts +287 -0
- package/network/StageLinqDevices.ts +215 -0
- package/network/StageLinqListener.ts +50 -0
- package/network/announce.ts +133 -0
- package/network/index.ts +4 -0
- package/package.json +31 -0
- package/services/FileTransfer.ts +298 -0
- package/services/Service.ts +144 -0
- package/services/StateMap.ts +170 -0
- package/services/index.ts +3 -0
- package/stagelinq.code-workspace +16 -0
- package/tsconfig.json +13 -0
- package/types/common.ts +240 -0
- package/types/database.ts +50 -0
- package/types/index.ts +53 -0
- package/types/player.ts +41 -0
- package/types/tokens.ts +39 -0
- package/utils/Context.ts +45 -0
- package/utils/ReadContext.ts +114 -0
- package/utils/WriteContext.ts +105 -0
- package/utils/getTempFilePath.ts +23 -0
- package/utils/hex.ts +62 -0
- package/utils/index.ts +6 -0
- package/utils/log.ts +0 -0
- package/utils/sleep.ts +3 -0
- package/utils/tcp.ts +17 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { EventEmitter } from 'stream';
|
|
2
|
+
|
|
3
|
+
export declare interface Logger {
|
|
4
|
+
on(event: 'log', listener: (...args: any) => void): this;
|
|
5
|
+
on(event: 'error', listener: (...args: any) => void): this;
|
|
6
|
+
on(event: 'warn', listener: (...args: any) => void): this;
|
|
7
|
+
on(event: 'info', listener: (...args: any) => void): this;
|
|
8
|
+
on(event: 'debug', listener: (...args: any) => void): this;
|
|
9
|
+
on(event: 'silly', listener: (...args: any) => void): this;
|
|
10
|
+
on(event: 'any', listener: (...args: any) => void): this;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class Logger extends EventEmitter {
|
|
14
|
+
|
|
15
|
+
private static _instance: Logger;
|
|
16
|
+
|
|
17
|
+
static get instance() {
|
|
18
|
+
return this._instance || (this._instance = new this());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static log(...args: any) {
|
|
22
|
+
Logger.instance.emit('log', ...args);
|
|
23
|
+
Logger.instance.emit('any', ...args);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static error(...args: any) {
|
|
27
|
+
Logger.instance.emit('error', ...args);
|
|
28
|
+
Logger.instance.emit('any', ...args);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static warn(...args: any) {
|
|
32
|
+
Logger.instance.emit('warn', ...args);
|
|
33
|
+
Logger.instance.emit('any', ...args);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static info(...args: any) {
|
|
37
|
+
Logger.instance.emit('info', ...args);
|
|
38
|
+
Logger.instance.emit('any', ...args);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static debug(...args: any) {
|
|
42
|
+
Logger.instance.emit('debug', ...args);
|
|
43
|
+
Logger.instance.emit('any', ...args);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static silly(...args: any) {
|
|
47
|
+
Logger.instance.emit('silly', ...args);
|
|
48
|
+
Logger.instance.emit('any', ...args);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# StageLinq
|
|
2
|
+
|
|
3
|
+
NodeJS library implementation to access information through the Denon StageLinq protocol.
|
|
4
|
+
|
|
5
|
+
# Features
|
|
6
|
+
|
|
7
|
+
* Tested with Denon two SC6000s, X1850, Prime 4, Prime 2, and Prime Go.
|
|
8
|
+
* Event emitters for state changes, tracks getting loaded, and current playing track.
|
|
9
|
+
* Event emitter for debug logging.
|
|
10
|
+
* Downloads source databases for you.
|
|
11
|
+
* You can implement handling the database yourself or use this library's BetterSqlite3 dependency.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { StageLinq } from '../StageLinq';
|
|
19
|
+
|
|
20
|
+
const options = { downloadDbSources: true };
|
|
21
|
+
const stageLinq = new StageLinq(options);
|
|
22
|
+
|
|
23
|
+
stageLinq.devices.on('ready', (connectionInfo) => {
|
|
24
|
+
console.log(`Device ${connectionInfo.software.name} on ` +
|
|
25
|
+
`${connectionInfo.address}:${connectionInfo.port} is ready.`);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
stageLinq.devices.on('trackLoaded', (status) => {
|
|
29
|
+
console.log(`"${status.title}" - ${status.artist} loaded on player ` +
|
|
30
|
+
`${status.deck})`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
stageLinq.devices.on('nowPlaying', (status) => {
|
|
34
|
+
console.log(`Now Playing: "${status.title}" - ${status.artist})`);
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
A [complete example](https://github.com/chrisle/StageLinq/blob/main/cli/index.ts) with all events and options can be found in the CLI.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Overview
|
|
43
|
+
|
|
44
|
+
The idea behind this library is to have a structure something like this:
|
|
45
|
+
|
|
46
|
+
**StageLinq > Devices > Player > Deck**
|
|
47
|
+
|
|
48
|
+
A StageLinq sets up a device listener and a class that handles all the
|
|
49
|
+
devices (`StageLinqDevices`).
|
|
50
|
+
|
|
51
|
+
`StageLinqDevices` figures out if it wants to connect or not and handles
|
|
52
|
+
connections. There may be one or more device on the network. For each device it
|
|
53
|
+
will try to connect to it and subscribe to it's `StateMap`.
|
|
54
|
+
|
|
55
|
+
Currently there is only one type of device: `Player`. A `Player` may have up to
|
|
56
|
+
4 decks A, B, C, D (aka "layers"). The `Player` handles incoming messages,
|
|
57
|
+
parses them, groups them, and emits events. These events bubble up to the
|
|
58
|
+
`Device`.
|
|
59
|
+
|
|
60
|
+
## Database
|
|
61
|
+
|
|
62
|
+
You can use BetterSqlite3 bundled into this library or let this library
|
|
63
|
+
download the files for you, then choose your own Sqlite library to
|
|
64
|
+
query the database. See CLI example.
|
|
65
|
+
|
|
66
|
+
## Logging
|
|
67
|
+
|
|
68
|
+
I needed the logging to be used outside of the library so I made them events
|
|
69
|
+
that you can listen to.
|
|
70
|
+
|
|
71
|
+
* `error`: When something bad happens.
|
|
72
|
+
* `warn`: When something happens but doesn't affect anything.
|
|
73
|
+
* `info`/`log`: When we have something to say
|
|
74
|
+
* `debug`: Spits out the parsed version of the packets.
|
|
75
|
+
* `silly`: Dumps all kinds of internal stuff
|
|
76
|
+
|
|
77
|
+
## About
|
|
78
|
+
|
|
79
|
+
Forked from @MarByteBeep's code.
|
|
80
|
+
|
|
81
|
+
Additional reverse engineering work: https://github.com/chrisle/stagelinq-pcap
|
|
82
|
+
|
|
83
|
+
Used in my app Now Playing https://www.nowplaying2.com
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { announce, createDiscoveryMessage, StageLinqListener, unannounce } from '../network';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { StageLinqDevices } from '../network/StageLinqDevices';
|
|
4
|
+
import { Logger } from '../LogEmitter';
|
|
5
|
+
import { Action, ActingAsDevice, StageLinqOptions } from '../types';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_OPTIONS: StageLinqOptions = {
|
|
8
|
+
maxRetries: 3,
|
|
9
|
+
actingAs: ActingAsDevice.NowPlaying,
|
|
10
|
+
downloadDbSources: true
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Main StageLinq class.
|
|
15
|
+
*/
|
|
16
|
+
export class StageLinq extends EventEmitter {
|
|
17
|
+
|
|
18
|
+
devices: StageLinqDevices;
|
|
19
|
+
logger: Logger = Logger.instance;
|
|
20
|
+
options: StageLinqOptions;
|
|
21
|
+
|
|
22
|
+
private listener: StageLinqListener = new StageLinqListener();
|
|
23
|
+
|
|
24
|
+
constructor(options?: StageLinqOptions) {
|
|
25
|
+
super();
|
|
26
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
27
|
+
this.devices = new StageLinqDevices(this.options);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Connect to the StageLinq network.
|
|
32
|
+
*/
|
|
33
|
+
async connect() {
|
|
34
|
+
const msg = createDiscoveryMessage(Action.Login, this.options.actingAs);
|
|
35
|
+
await announce(msg);
|
|
36
|
+
this.listener.listenForDevices(async (connectionInfo) => {
|
|
37
|
+
await this.devices.handleDevice(connectionInfo);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Disconnect from the StageLinq network.
|
|
43
|
+
*/
|
|
44
|
+
async disconnect() {
|
|
45
|
+
try {
|
|
46
|
+
this.devices.disconnectAll();
|
|
47
|
+
const msg = createDiscoveryMessage(Action.Logout, this.options.actingAs)
|
|
48
|
+
await unannounce(msg);
|
|
49
|
+
} catch(e) {
|
|
50
|
+
throw new Error(e);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get databases() {
|
|
55
|
+
return this.devices.databases;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
}
|
package/TODO.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NetworkDevice } from '../network';
|
|
2
|
+
import { FileTransfer } from '../services';
|
|
3
|
+
import { getTempFilePath } from "../utils/getTempFilePath";
|
|
4
|
+
import { strict as assert } from 'assert';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import minimist = require('minimist');
|
|
7
|
+
import { Logger } from '../LogEmitter';
|
|
8
|
+
|
|
9
|
+
export async function maybeDownloadFiles(controller: NetworkDevice) {
|
|
10
|
+
const args = minimist(process.argv.slice(2));
|
|
11
|
+
if (!args.disableFileTransfer) {
|
|
12
|
+
const ftx = await controller.connectToService(FileTransfer);
|
|
13
|
+
assert(ftx);
|
|
14
|
+
const sources = await ftx.getSources();
|
|
15
|
+
{
|
|
16
|
+
const sync = !args.skipsync;
|
|
17
|
+
for (const source of sources) {
|
|
18
|
+
const dbPath = getTempFilePath(source.database.location);
|
|
19
|
+
// FIXME: Move all this away from main
|
|
20
|
+
if (sync) {
|
|
21
|
+
const file = await ftx.getFile(source.database.location);
|
|
22
|
+
fs.writeFileSync(dbPath, file);
|
|
23
|
+
Logger.info(`downloaded: '${source.database.location}' and stored in '${dbPath}'`);
|
|
24
|
+
}
|
|
25
|
+
await controller.addSource(source.name, dbPath, getTempFilePath(`${source.name}/Album Art/`));
|
|
26
|
+
|
|
27
|
+
if (sync) {
|
|
28
|
+
await controller.dumpAlbumArt(source.name);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
ftx.disconnect();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
package/cli/index.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { ActingAsDevice } from '../types';
|
|
2
|
+
import { DbConnection } from "../Databases";
|
|
3
|
+
import { sleep } from '../utils/sleep';
|
|
4
|
+
import { StageLinq } from '../StageLinq';
|
|
5
|
+
require('console-stamp')(console, {
|
|
6
|
+
format: ':date(HH:MM:ss) :label',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
(async () => {
|
|
11
|
+
|
|
12
|
+
console.log('Starting CLI');
|
|
13
|
+
|
|
14
|
+
const stageLinqOptions = {
|
|
15
|
+
|
|
16
|
+
// If set to true, download the source DBs in a temporary location.
|
|
17
|
+
// (default: true)
|
|
18
|
+
downloadDbSources: true,
|
|
19
|
+
|
|
20
|
+
// Max number of attempts to connect to a StageLinq device.
|
|
21
|
+
// (default: 3)
|
|
22
|
+
maxRetries: 3,
|
|
23
|
+
|
|
24
|
+
// What device to emulate on the network.
|
|
25
|
+
// (default: Now Playing)
|
|
26
|
+
actingAs: ActingAsDevice.NowPlaying
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const stageLinq = new StageLinq(stageLinqOptions);
|
|
30
|
+
|
|
31
|
+
// Setup how you want to handle logs coming from StageLinq
|
|
32
|
+
stageLinq.logger.on('error', (...args: any) => {
|
|
33
|
+
console.error(...args);
|
|
34
|
+
});
|
|
35
|
+
stageLinq.logger.on('warn', (...args: any) => {
|
|
36
|
+
console.warn(...args);
|
|
37
|
+
});
|
|
38
|
+
stageLinq.logger.on('info', (...args: any) => {
|
|
39
|
+
console.info(...args);
|
|
40
|
+
});
|
|
41
|
+
stageLinq.logger.on('log', (...args: any) => {
|
|
42
|
+
console.log(...args);
|
|
43
|
+
});
|
|
44
|
+
stageLinq.logger.on('debug', (...args: any) => {
|
|
45
|
+
console.debug(...args);
|
|
46
|
+
});
|
|
47
|
+
// Note: Silly is very verbose!
|
|
48
|
+
// stageLinq.logger.on('silly', (...args: any) => {
|
|
49
|
+
// console.debug(...args);
|
|
50
|
+
// });
|
|
51
|
+
|
|
52
|
+
// Fires when we connect to any device
|
|
53
|
+
stageLinq.devices.on('connected', async (connectionInfo) => {
|
|
54
|
+
console.log(`Successfully connected to ${connectionInfo.software.name}`);
|
|
55
|
+
|
|
56
|
+
// Fires when the database source starts downloading.
|
|
57
|
+
stageLinq.databases.on('dbDownloading', (sourceName, dbPath) => {
|
|
58
|
+
console.log(`Downloading ${sourceName} to ${dbPath}`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Fires while the database source is being read
|
|
62
|
+
stageLinq.databases.on('dbProgress', (sourceName, total, bytes, percent) => {
|
|
63
|
+
console.debug(`Reading ${sourceName}: ${bytes}/${total} (${Math.ceil(percent)}%)`);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Fires when the database source has been read and saved to a temporary path.
|
|
67
|
+
stageLinq.databases.on('dbDownloaded', (sourceName, dbPath) => {
|
|
68
|
+
console.log(`Database (${sourceName}) has been downloaded to ${dbPath}`);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Fires when StageLinq has successfully connect to at least one device and is ready to use.
|
|
73
|
+
stageLinq.devices.on('ready', (connectionInfo) => {
|
|
74
|
+
console.log(`Device ${connectionInfo.software.name} is ready!`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Fires when a new track is loaded on to a player.
|
|
78
|
+
stageLinq.devices.on('trackLoaded', async (status) => {
|
|
79
|
+
|
|
80
|
+
// Example of how to connect to the database using this library's
|
|
81
|
+
// implementation of BetterSqlite3 to get additional information.
|
|
82
|
+
if (stageLinq.options.downloadDbSources && status.dbSourceName) {
|
|
83
|
+
try {
|
|
84
|
+
const connection = new DbConnection(stageLinq.databases.getDbPath(status.dbSourceName));
|
|
85
|
+
const result = connection.getTrackInfo(status.trackPath);
|
|
86
|
+
connection.close();
|
|
87
|
+
console.log('Database entry:', result);
|
|
88
|
+
} catch(e) {
|
|
89
|
+
console.error(e);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
console.log('New track loaded:', status);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Fires when a track has started playing.
|
|
96
|
+
stageLinq.devices.on('nowPlaying', (status) => {
|
|
97
|
+
console.log(`Now Playing on [${status.deck}]: ${status.title} - ${status.artist}`)
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Fires when StageLinq receives messages from a device.
|
|
101
|
+
stageLinq.devices.on('message', (connectionInfo, data) => {
|
|
102
|
+
const msg = data.message.json
|
|
103
|
+
? JSON.stringify(data.message.json)
|
|
104
|
+
: data.message.interval;
|
|
105
|
+
console.debug(`${connectionInfo.address}:${connectionInfo.port} ` +
|
|
106
|
+
`${data.message.name} => ${msg}`);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Fires when the state of a device has changed.
|
|
110
|
+
stageLinq.devices.on('stateChanged', (status) => {
|
|
111
|
+
console.log(`State changed on [${status.deck}]`, status)
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
let returnCode = 0;
|
|
115
|
+
try {
|
|
116
|
+
process.on('SIGINT', async function () {
|
|
117
|
+
console.info('... exiting');
|
|
118
|
+
// Ensure SIGINT won't be impeded by some error
|
|
119
|
+
try {
|
|
120
|
+
await stageLinq.disconnect();
|
|
121
|
+
} catch (err: any) {
|
|
122
|
+
const message = err.stack.toString();
|
|
123
|
+
console.error(message);
|
|
124
|
+
}
|
|
125
|
+
process.exit(returnCode);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await stageLinq.connect();
|
|
129
|
+
|
|
130
|
+
while (true) {
|
|
131
|
+
await sleep(250);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
} catch (err: any) {
|
|
135
|
+
const message = err.stack.toString();
|
|
136
|
+
console.error(message);
|
|
137
|
+
returnCode = 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await stageLinq.disconnect();
|
|
141
|
+
process.exit(returnCode);
|
|
142
|
+
})();
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { PlayerLayerState, PlayerStatus, ServiceMessage } from '../types';
|
|
3
|
+
import { PlayerMessageQueue } from './PlayerMessageQueue';
|
|
4
|
+
import { StateData, StateMap } from '../services';
|
|
5
|
+
|
|
6
|
+
export declare interface Player {
|
|
7
|
+
on(event: 'trackLoaded', listener: (status: PlayerStatus) => void): this;
|
|
8
|
+
on(event: 'stateChanged', listener: (status: PlayerStatus) => void): this;
|
|
9
|
+
on(event: 'nowPlaying', listener: (status: PlayerStatus) => void): this;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
13
|
+
|
|
14
|
+
interface PlayerOptions {
|
|
15
|
+
stateMap: StateMap;
|
|
16
|
+
address: string,
|
|
17
|
+
port: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface SourceAndTrackPath {
|
|
21
|
+
source: string;
|
|
22
|
+
trackPath: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A player represents a device on the StageLinq network.
|
|
27
|
+
*
|
|
28
|
+
* A player on the network may have up to 4 decks (or "layers" as they're
|
|
29
|
+
* called on the harware). A player may also be given a player number.
|
|
30
|
+
*
|
|
31
|
+
* If you're using a Denon Prime Go/2/4 then you should only get one number.
|
|
32
|
+
* If you're using a Denon SC5000/SC6000 then you assign the numbers in the
|
|
33
|
+
* Denon's settings screen.
|
|
34
|
+
*
|
|
35
|
+
* Master tempo and master status only apply if you are using SC5000/SC6000
|
|
36
|
+
* and if they're both on the network.
|
|
37
|
+
*
|
|
38
|
+
* A queue is used to group all the incoming messages from StageLinq to give us
|
|
39
|
+
* a single updated PlayerStatus object.
|
|
40
|
+
*/
|
|
41
|
+
export class Player extends EventEmitter {
|
|
42
|
+
|
|
43
|
+
private player: number; // Player number as reported by the device.
|
|
44
|
+
private address: string; // IP address
|
|
45
|
+
private port: number; // Port
|
|
46
|
+
private masterTempo: number; // Current master tempo BPM
|
|
47
|
+
private masterStatus: boolean; // If this device has the matser tempo
|
|
48
|
+
private decks: Map<string, PlayerLayerState> = new Map();
|
|
49
|
+
private queue: {[layer: string]: PlayerMessageQueue} = {};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Initialize a player device.
|
|
53
|
+
*
|
|
54
|
+
* @param networkDevice Network device
|
|
55
|
+
* @param stateMap Statemap service
|
|
56
|
+
*/
|
|
57
|
+
constructor(options: PlayerOptions) {
|
|
58
|
+
super();
|
|
59
|
+
options.stateMap.on('message', this.messageHandler.bind(this));
|
|
60
|
+
this.address = options.address;
|
|
61
|
+
this.port = options.port;
|
|
62
|
+
this.queue = {
|
|
63
|
+
A: new PlayerMessageQueue('A').onDataReady(this.handleUpdate.bind(this)),
|
|
64
|
+
B: new PlayerMessageQueue('B').onDataReady(this.handleUpdate.bind(this)),
|
|
65
|
+
C: new PlayerMessageQueue('C').onDataReady(this.handleUpdate.bind(this)),
|
|
66
|
+
D: new PlayerMessageQueue('D').onDataReady(this.handleUpdate.bind(this)),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse the state data and push it into the update queue.
|
|
72
|
+
*
|
|
73
|
+
* @param data State data from Denon.
|
|
74
|
+
* @returns
|
|
75
|
+
*/
|
|
76
|
+
private messageHandler(data: ServiceMessage<StateData>) {
|
|
77
|
+
const message = data.message
|
|
78
|
+
if (!message.json) return;
|
|
79
|
+
const name = message.name;
|
|
80
|
+
const json = message.json as any;
|
|
81
|
+
|
|
82
|
+
if (/Client\/Preferences\/Player$/.test(name)) {
|
|
83
|
+
this.player = parseInt(json.string);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (/Engine\/Master\/MasterTempo/.test(name)) {
|
|
87
|
+
this.masterTempo = json.value;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (/Engine\/Sync\/Network\/MasterStatus/.test(name)) {
|
|
91
|
+
this.masterStatus = json.state;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const split = message.name.split('/');
|
|
96
|
+
|
|
97
|
+
const deck =
|
|
98
|
+
(/PlayerJogColor[A-D]$/.test(name)) ? split[3].replace('PlayerJogColor', '')
|
|
99
|
+
: (/Engine\/Deck\d\//.test(name)) ? this.deckNumberToLayer(split[2])
|
|
100
|
+
: null;
|
|
101
|
+
|
|
102
|
+
const cueData =
|
|
103
|
+
(/PlayState$/.test(name)) ? { playState: json.state }
|
|
104
|
+
: (/Track\/TrackNetworkPath$/.test(name)) ? {
|
|
105
|
+
trackNetworkPath: json.string,
|
|
106
|
+
source: this.getSourceAndTrackPath(json.string).source,
|
|
107
|
+
trackPath: this.getSourceAndTrackPath(json.string).trackPath
|
|
108
|
+
}
|
|
109
|
+
: (/Track\/SongLoaded$/.test(name)) ? { songLoaded: json.state }
|
|
110
|
+
: (/Track\/SongName$/.test(name)) ? { title: json.string }
|
|
111
|
+
: (/Track\/ArtistName$/.test(name)) ? { artist: json.string }
|
|
112
|
+
: (/Track\/TrackData$/.test(name)) ? { hasTrackData: json.state }
|
|
113
|
+
: (/Track\/TrackName$/.test(name)) ? { fileLocation: json.string }
|
|
114
|
+
: (/CurrentBPM$/.test(name)) ? { currentBpm: json.value }
|
|
115
|
+
: (/ExternalMixerVolume$/.test(name)) ? { externalMixerVolume: json.value }
|
|
116
|
+
: (/Play$/.test(name)) ? { play: json.state }
|
|
117
|
+
: (/PlayerJogColor[A-D]$/.test(name)) ? { jogColor: json.color }
|
|
118
|
+
: null;
|
|
119
|
+
|
|
120
|
+
if (cueData) {
|
|
121
|
+
this.queue[deck].push({ layer: deck, ...cueData });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Update current state and emit.
|
|
127
|
+
* @param data
|
|
128
|
+
*/
|
|
129
|
+
private handleUpdate(data: PlayerLayerState) {
|
|
130
|
+
const layer = data.layer;
|
|
131
|
+
const songLoadedSignalPresent = data.hasOwnProperty('songLoaded');
|
|
132
|
+
|
|
133
|
+
// If a new song is loaded drop all the previous track data.
|
|
134
|
+
if (songLoadedSignalPresent) {
|
|
135
|
+
this.decks.set(layer, data);
|
|
136
|
+
} else {
|
|
137
|
+
this.decks.set(layer, { ...this.decks.get(layer), ...data });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const result = this.decks.get(layer);
|
|
141
|
+
const deck = `${this.player}${result.layer}`;
|
|
142
|
+
|
|
143
|
+
const output = {
|
|
144
|
+
deck: deck,
|
|
145
|
+
player: this.player,
|
|
146
|
+
layer: layer,
|
|
147
|
+
address: this.address,
|
|
148
|
+
port: this.port,
|
|
149
|
+
masterTempo: this.masterTempo,
|
|
150
|
+
masterStatus: this.masterStatus,
|
|
151
|
+
...result
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// We're casting it because we originally built it up piecemeal.
|
|
155
|
+
const currentState = output as PlayerStatus;
|
|
156
|
+
currentState.dbSourceName = currentState.source ? `${this.address}_${this.port}_${currentState.source}` : '';
|
|
157
|
+
if (songLoadedSignalPresent && currentState.trackNetworkPath) this.emit('trackLoaded', currentState);
|
|
158
|
+
if (result.playState) this.emit('nowPlaying', currentState);
|
|
159
|
+
this.emit('stateChanged', currentState);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private deckNumberToLayer(deck: string) {
|
|
163
|
+
const index = parseInt(deck.replace('Deck', '')) - 1;
|
|
164
|
+
return 'ABCD'[index];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private getSourceAndTrackPath(p_path: string): SourceAndTrackPath {
|
|
168
|
+
if (!p_path || p_path.length === 0) return { source: '', trackPath: '' };
|
|
169
|
+
const parts = p_path.split('/');
|
|
170
|
+
const source = parts[3];
|
|
171
|
+
let trackPath = parts.slice(5).join('/');
|
|
172
|
+
if (parts[4] !== 'Engine Library') {
|
|
173
|
+
// This probably occurs with RekordBox conversions; tracks are outside Engine Library folder
|
|
174
|
+
trackPath = `../${parts[4]}/${trackPath}`;
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
source: source,
|
|
178
|
+
trackPath: trackPath,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { PlayerLayerState } from '../types';
|
|
2
|
+
|
|
3
|
+
// How long to wait for all the messages to come in before firing the callback.
|
|
4
|
+
export const UPDATE_RATE_MS = 500;
|
|
5
|
+
|
|
6
|
+
export type DataQueueCallback = (data: PlayerLayerState) => void;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Collect all the messages from a player together and return it as one object.
|
|
10
|
+
*
|
|
11
|
+
* The Denon hardware will fire several messages in quick succession. This will
|
|
12
|
+
* take them all in, then after UPDATE_RATE_MS will merge all the data
|
|
13
|
+
* as a single update to the `onDataReady` callback.
|
|
14
|
+
*
|
|
15
|
+
* For example, when you move the fader up you get several ExternalMixerVolume
|
|
16
|
+
* messages where the value goes up from 0 to 1. Instead firing off loads
|
|
17
|
+
* of updates we're only interested in the last one.
|
|
18
|
+
*/
|
|
19
|
+
export class PlayerMessageQueue {
|
|
20
|
+
|
|
21
|
+
private callback: DataQueueCallback;
|
|
22
|
+
private data: PlayerLayerState[] = [];
|
|
23
|
+
private timeout: NodeJS.Timer | null = null;
|
|
24
|
+
private layer: string;
|
|
25
|
+
|
|
26
|
+
constructor(layer: string) {
|
|
27
|
+
this.layer = layer;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Push data into the queue.
|
|
32
|
+
* @param data Parsed data from a player.
|
|
33
|
+
*/
|
|
34
|
+
push(data: PlayerLayerState) {
|
|
35
|
+
this.data.push(data);
|
|
36
|
+
if (!this.timeout) {
|
|
37
|
+
this.timeout = setTimeout(this.emptyCue.bind(this), UPDATE_RATE_MS);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Merge data, empty the queue, clear the timeout, and fire the callback.
|
|
43
|
+
*/
|
|
44
|
+
emptyCue() {
|
|
45
|
+
let output: any = { layer: this.layer };
|
|
46
|
+
this.data.map(d => { output = { ...output, ...d }; });
|
|
47
|
+
this.data = [];
|
|
48
|
+
clearTimeout(this.timeout);
|
|
49
|
+
this.timeout = null;
|
|
50
|
+
this.callback(output as PlayerLayerState);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Execute this callback when there is new data from the Denon hardware.
|
|
55
|
+
* @param callback User callback when we have an update.
|
|
56
|
+
* @returns
|
|
57
|
+
*/
|
|
58
|
+
onDataReady(callback: DataQueueCallback) {
|
|
59
|
+
this.callback = callback;
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
}
|