stagelinq 1.0.0 → 1.0.4
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/Databases/Databases.ts +23 -6
- package/Databases/DbConnection.ts +7 -2
- package/StageLinq/index.ts +2 -1
- package/cli/index.ts +23 -6
- package/devices/Player.ts +40 -5
- package/network/StageLinqDevices.ts +96 -45
- package/network-path-prototype.patch +181 -0
- package/package.json +2 -1
- package/services/FileTransfer.ts +6 -1
- package/types/player.ts +2 -0
- package/utils/getTempFilePath.ts +1 -1
package/Databases/Databases.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { ConnectionInfo, Source } from '../types';
|
|
1
2
|
import { EventEmitter } from 'stream';
|
|
2
3
|
import { FileTransfer } from '../services';
|
|
3
4
|
import { getTempFilePath } from '../albumArt';
|
|
5
|
+
import { Logger } from '../LogEmitter';
|
|
4
6
|
import { NetworkDevice } from '../network';
|
|
5
7
|
import * as fs from 'fs';
|
|
6
|
-
import { Logger } from '../LogEmitter';
|
|
7
|
-
import { ConnectionInfo, Source } from '../types';
|
|
8
8
|
|
|
9
9
|
export declare interface Databases {
|
|
10
10
|
on(event: 'dbDownloaded', listener: (sourceName: string, dbPath: string) => void): this;
|
|
@@ -25,7 +25,10 @@ export class Databases extends EventEmitter {
|
|
|
25
25
|
const sources = await service.getSources();
|
|
26
26
|
const output: string[] = [];
|
|
27
27
|
for (const source of sources) {
|
|
28
|
-
const
|
|
28
|
+
const deviceId = /(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/i
|
|
29
|
+
.exec(Buffer.from(connectionInfo.token).toString('hex')).splice(1).join('-');
|
|
30
|
+
const dbConnectionName = `net://${deviceId}/${source.name}`;
|
|
31
|
+
Logger.debug(`DB network path: ${dbConnectionName}`);
|
|
29
32
|
if (this.sources.has(dbConnectionName)) {
|
|
30
33
|
Logger.debug(`Already seen ${source} on ${connectionInfo.address}:${connectionInfo.port}`);
|
|
31
34
|
} else {
|
|
@@ -60,9 +63,23 @@ export class Databases extends EventEmitter {
|
|
|
60
63
|
this.emit('dbDownloaded', sourceId, dbPath);
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
getDbPath(dbSourceName
|
|
64
|
-
if (!this.sources.has(dbSourceName))
|
|
65
|
-
|
|
66
|
+
getDbPath(dbSourceName?: string) {
|
|
67
|
+
if (!dbSourceName || !this.sources.has(dbSourceName)) {
|
|
68
|
+
|
|
69
|
+
// Hack: Denon will save metadata on streaming files but only on an
|
|
70
|
+
// internal database. So if the source is "(Unknown)streaming://"
|
|
71
|
+
// return the first internal database we find.
|
|
72
|
+
for (const entry of Array.from(this.sources.entries())) {
|
|
73
|
+
if (/\(Internal\)/.test(entry[0])) {
|
|
74
|
+
Logger.debug(`Returning copy of internal database`);
|
|
75
|
+
return this.sources.get(entry[0]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Else, throw an exception.
|
|
80
|
+
throw new Error(`Data source "${dbSourceName}" doesn't exist.`);
|
|
81
|
+
}
|
|
82
|
+
|
|
66
83
|
return this.sources.get(dbSourceName);
|
|
67
84
|
}
|
|
68
85
|
|
|
@@ -20,7 +20,7 @@ export class DbConnection {
|
|
|
20
20
|
* @returns
|
|
21
21
|
*/
|
|
22
22
|
querySource<T>(query: string, ...params: any[]): T[] {
|
|
23
|
-
console.debug(`Querying ${this.dbPath}: `);
|
|
23
|
+
console.debug(`Querying ${this.dbPath}: ${query} (${params.join(', ')})`);
|
|
24
24
|
const result = this.db.prepare(query);
|
|
25
25
|
return result.all(params);
|
|
26
26
|
}
|
|
@@ -32,11 +32,16 @@ export class DbConnection {
|
|
|
32
32
|
* @returns
|
|
33
33
|
*/
|
|
34
34
|
getTrackInfo(trackPath: string) {
|
|
35
|
-
|
|
35
|
+
if (/streaming:\/\//.test(trackPath)) {
|
|
36
|
+
return this.querySource('SELECT * FROM Track WHERE uri = (?)', trackPath);
|
|
37
|
+
} else {
|
|
38
|
+
return this.querySource('SELECT * FROM Track WHERE path = (?)', trackPath);
|
|
39
|
+
}
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
close() {
|
|
39
43
|
console.debug(`Closing ${this.dbPath}`);
|
|
40
44
|
this.db.close();
|
|
41
45
|
}
|
|
46
|
+
|
|
42
47
|
}
|
package/StageLinq/index.ts
CHANGED
|
@@ -19,7 +19,7 @@ export class StageLinq extends EventEmitter {
|
|
|
19
19
|
logger: Logger = Logger.instance;
|
|
20
20
|
options: StageLinqOptions;
|
|
21
21
|
|
|
22
|
-
private listener: StageLinqListener
|
|
22
|
+
private listener: StageLinqListener;
|
|
23
23
|
|
|
24
24
|
constructor(options?: StageLinqOptions) {
|
|
25
25
|
super();
|
|
@@ -31,6 +31,7 @@ export class StageLinq extends EventEmitter {
|
|
|
31
31
|
* Connect to the StageLinq network.
|
|
32
32
|
*/
|
|
33
33
|
async connect() {
|
|
34
|
+
this.listener = new StageLinqListener();
|
|
34
35
|
const msg = createDiscoveryMessage(Action.Login, this.options.actingAs);
|
|
35
36
|
await announce(msg);
|
|
36
37
|
this.listener.listenForDevices(async (connectionInfo) => {
|
package/cli/index.ts
CHANGED
|
@@ -2,11 +2,14 @@ import { ActingAsDevice } from '../types';
|
|
|
2
2
|
import { DbConnection } from "../Databases";
|
|
3
3
|
import { sleep } from '../utils/sleep';
|
|
4
4
|
import { StageLinq } from '../StageLinq';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as os from 'os';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
|
|
5
9
|
require('console-stamp')(console, {
|
|
6
10
|
format: ':date(HH:MM:ss) :label',
|
|
7
11
|
});
|
|
8
12
|
|
|
9
|
-
|
|
10
13
|
(async () => {
|
|
11
14
|
|
|
12
15
|
console.log('Starting CLI');
|
|
@@ -69,9 +72,9 @@ require('console-stamp')(console, {
|
|
|
69
72
|
});
|
|
70
73
|
});
|
|
71
74
|
|
|
72
|
-
// Fires when StageLinq
|
|
73
|
-
stageLinq.devices.on('ready', (
|
|
74
|
-
console.log(`
|
|
75
|
+
// Fires when StageLinq and all devices are ready to use.
|
|
76
|
+
stageLinq.devices.on('ready', () => {
|
|
77
|
+
console.log(`StageLinq is ready!`);
|
|
75
78
|
});
|
|
76
79
|
|
|
77
80
|
// Fires when a new track is loaded on to a player.
|
|
@@ -79,9 +82,10 @@ require('console-stamp')(console, {
|
|
|
79
82
|
|
|
80
83
|
// Example of how to connect to the database using this library's
|
|
81
84
|
// implementation of BetterSqlite3 to get additional information.
|
|
82
|
-
if (stageLinq.options.downloadDbSources
|
|
85
|
+
if (stageLinq.options.downloadDbSources) {
|
|
83
86
|
try {
|
|
84
|
-
const
|
|
87
|
+
const dbPath = stageLinq.databases.getDbPath(status.dbSourceName)
|
|
88
|
+
const connection = new DbConnection(dbPath);
|
|
85
89
|
const result = connection.getTrackInfo(status.trackPath);
|
|
86
90
|
connection.close();
|
|
87
91
|
console.log('Database entry:', result);
|
|
@@ -89,6 +93,19 @@ require('console-stamp')(console, {
|
|
|
89
93
|
console.error(e);
|
|
90
94
|
}
|
|
91
95
|
}
|
|
96
|
+
|
|
97
|
+
// Example of how to download the actual track from the media.
|
|
98
|
+
try {
|
|
99
|
+
const tempfile = path.resolve(os.tmpdir(), 'media');
|
|
100
|
+
const data = await stageLinq.devices.downloadFile(status.deviceId, status.trackPathAbsolute);
|
|
101
|
+
if (data) {
|
|
102
|
+
fs.writeFileSync(tempfile, Buffer.from(data));
|
|
103
|
+
console.log(`Downloaded ${status.trackPathAbsolute} to ${tempfile}`);
|
|
104
|
+
}
|
|
105
|
+
} catch(e) {
|
|
106
|
+
console.error(`Could not download ${status.trackPathAbsolute}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
92
109
|
console.log('New track loaded:', status);
|
|
93
110
|
});
|
|
94
111
|
|
package/devices/Player.ts
CHANGED
|
@@ -15,11 +15,13 @@ interface PlayerOptions {
|
|
|
15
15
|
stateMap: StateMap;
|
|
16
16
|
address: string,
|
|
17
17
|
port: number;
|
|
18
|
+
deviceId: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
interface SourceAndTrackPath {
|
|
21
22
|
source: string;
|
|
22
23
|
trackPath: string;
|
|
24
|
+
trackPathAbsolute: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -47,6 +49,7 @@ export class Player extends EventEmitter {
|
|
|
47
49
|
private masterStatus: boolean; // If this device has the matser tempo
|
|
48
50
|
private decks: Map<string, PlayerLayerState> = new Map();
|
|
49
51
|
private queue: {[layer: string]: PlayerMessageQueue} = {};
|
|
52
|
+
private deviceId: string;
|
|
50
53
|
|
|
51
54
|
/**
|
|
52
55
|
* Initialize a player device.
|
|
@@ -59,6 +62,7 @@ export class Player extends EventEmitter {
|
|
|
59
62
|
options.stateMap.on('message', this.messageHandler.bind(this));
|
|
60
63
|
this.address = options.address;
|
|
61
64
|
this.port = options.port;
|
|
65
|
+
this.deviceId = options.deviceId;
|
|
62
66
|
this.queue = {
|
|
63
67
|
A: new PlayerMessageQueue('A').onDataReady(this.handleUpdate.bind(this)),
|
|
64
68
|
B: new PlayerMessageQueue('B').onDataReady(this.handleUpdate.bind(this)),
|
|
@@ -104,7 +108,8 @@ export class Player extends EventEmitter {
|
|
|
104
108
|
: (/Track\/TrackNetworkPath$/.test(name)) ? {
|
|
105
109
|
trackNetworkPath: json.string,
|
|
106
110
|
source: this.getSourceAndTrackPath(json.string).source,
|
|
107
|
-
trackPath: this.getSourceAndTrackPath(json.string).trackPath
|
|
111
|
+
trackPath: this.getSourceAndTrackPath(json.string).trackPath,
|
|
112
|
+
trackPathAbsolute: this.getSourceAndTrackPath(json.string).trackPathAbsolute
|
|
108
113
|
}
|
|
109
114
|
: (/Track\/SongLoaded$/.test(name)) ? { songLoaded: json.state }
|
|
110
115
|
: (/Track\/SongName$/.test(name)) ? { title: json.string }
|
|
@@ -148,14 +153,32 @@ export class Player extends EventEmitter {
|
|
|
148
153
|
port: this.port,
|
|
149
154
|
masterTempo: this.masterTempo,
|
|
150
155
|
masterStatus: this.masterStatus,
|
|
156
|
+
deviceId: `net://${this.deviceId}`,
|
|
151
157
|
...result
|
|
152
158
|
};
|
|
153
159
|
|
|
154
|
-
// We're casting
|
|
160
|
+
// We're casting here because we originally built it up piecemeal.
|
|
155
161
|
const currentState = output as PlayerStatus;
|
|
156
|
-
|
|
157
|
-
if (
|
|
162
|
+
|
|
163
|
+
if (currentState.trackNetworkPath && currentState.trackNetworkPath.startsWith('net:')) {
|
|
164
|
+
const pathParts = currentState.trackNetworkPath.split('net://')[1].split('/', 2);
|
|
165
|
+
currentState.dbSourceName = `net://${pathParts[0]}/${pathParts[1]}`;
|
|
166
|
+
currentState.deviceId = `net://${pathParts[0]}`;
|
|
167
|
+
} else if (!currentState.source || /Unknown/.test(currentState.source)) {
|
|
168
|
+
// Tracks from streaming sources won't be in the database.
|
|
169
|
+
currentState.dbSourceName = '';
|
|
170
|
+
} else {
|
|
171
|
+
currentState.dbSourceName = `net://${this.deviceId}/${currentState.source}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// If a song is loaded and we have a location emit the trackLoaded event.
|
|
175
|
+
if (currentState.trackNetworkPath)
|
|
176
|
+
this.emit('trackLoaded', currentState);
|
|
177
|
+
|
|
178
|
+
// If the song is actually playing emit the nowPlaying event.
|
|
158
179
|
if (result.playState) this.emit('nowPlaying', currentState);
|
|
180
|
+
|
|
181
|
+
// Emit that the state has changed.
|
|
159
182
|
this.emit('stateChanged', currentState);
|
|
160
183
|
}
|
|
161
184
|
|
|
@@ -165,17 +188,29 @@ export class Player extends EventEmitter {
|
|
|
165
188
|
}
|
|
166
189
|
|
|
167
190
|
private getSourceAndTrackPath(p_path: string): SourceAndTrackPath {
|
|
168
|
-
if (!p_path || p_path.length === 0) return { source: '', trackPath: '' };
|
|
191
|
+
if (!p_path || p_path.length === 0) return { source: '', trackPath: '', trackPathAbsolute: '' };
|
|
169
192
|
const parts = p_path.split('/');
|
|
170
193
|
const source = parts[3];
|
|
171
194
|
let trackPath = parts.slice(5).join('/');
|
|
195
|
+
|
|
196
|
+
// Handle streaming tracks.
|
|
197
|
+
if (/\(Unknown\)/.test(source)) {
|
|
198
|
+
return {
|
|
199
|
+
source: source.replace(':', ''),
|
|
200
|
+
trackPath: `streaming://${trackPath}`,
|
|
201
|
+
trackPathAbsolute: `streaming://${trackPath}`
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
172
205
|
if (parts[4] !== 'Engine Library') {
|
|
173
206
|
// This probably occurs with RekordBox conversions; tracks are outside Engine Library folder
|
|
174
207
|
trackPath = `../${parts[4]}/${trackPath}`;
|
|
175
208
|
}
|
|
209
|
+
|
|
176
210
|
return {
|
|
177
211
|
source: source,
|
|
178
212
|
trackPath: trackPath,
|
|
213
|
+
trackPathAbsolute: `/${source}/Engine Library/${trackPath}`
|
|
179
214
|
};
|
|
180
215
|
}
|
|
181
216
|
|
|
@@ -14,13 +14,17 @@ interface StageLinqDevice {
|
|
|
14
14
|
fileTransferService: FileTransfer;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
// This time needs to be just long enough for discovery messages from all to
|
|
18
|
+
// come through.
|
|
19
|
+
const WAIT_FOR_DEVICES_TIMEOUT_MS = 3000;
|
|
20
|
+
|
|
17
21
|
export declare interface StageLinqDevices {
|
|
18
22
|
on(event: 'trackLoaded', listener: (status: PlayerStatus) => void): this;
|
|
19
23
|
on(event: 'stateChanged', listener: (status: PlayerStatus) => void): this;
|
|
20
24
|
on(event: 'nowPlaying', listener: (status: PlayerStatus) => void): this;
|
|
21
25
|
on(event: 'connected', listener: (connectionInfo: ConnectionInfo) => void): this;
|
|
22
26
|
on(event: 'message', listener: (connectionInfo: ConnectionInfo, message: ServiceMessage<StateData>) => void): this;
|
|
23
|
-
on(event: 'ready', listener: (
|
|
27
|
+
on(event: 'ready', listener: () => void): this;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
//////////////////////////////////////////////////////////////////////////////
|
|
@@ -38,44 +42,97 @@ export class StageLinqDevices extends EventEmitter {
|
|
|
38
42
|
private discoveryStatus: Map<string, ConnectionStatus> = new Map();
|
|
39
43
|
private options: StageLinqOptions;
|
|
40
44
|
|
|
45
|
+
private deviceWatchTimeout: NodeJS.Timeout | null = null;
|
|
46
|
+
private stateMapCallback: { connectionInfo: ConnectionInfo, networkDevice: NetworkDevice }[] = [];
|
|
47
|
+
|
|
41
48
|
constructor(options: StageLinqOptions) {
|
|
42
49
|
super();
|
|
43
50
|
this.options = options;
|
|
44
51
|
this._databases = new Databases();
|
|
52
|
+
this.waitForAllDevices = this.waitForAllDevices.bind(this);
|
|
53
|
+
this.waitForAllDevices();
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
/**
|
|
48
|
-
*
|
|
57
|
+
* Handle incoming discovery messages from the StageLinq network
|
|
49
58
|
*
|
|
50
|
-
* @param connectionInfo
|
|
51
|
-
* @returns Retries to connect 3 times. If successful return void if not throw exception.
|
|
59
|
+
* @param connectionInfo Connection info.
|
|
52
60
|
*/
|
|
53
61
|
async handleDevice(connectionInfo: ConnectionInfo) {
|
|
54
62
|
Logger.silly(this.showDiscoveryStatus(connectionInfo));
|
|
55
63
|
|
|
56
|
-
// Ignore this discovery message if connected
|
|
57
|
-
// if it's blacklisted.
|
|
64
|
+
// Ignore this discovery message if we've already connected to it,
|
|
65
|
+
// are still connecting, if it has failed, or if it's blacklisted.
|
|
58
66
|
if (this.isConnected(connectionInfo)
|
|
59
67
|
|| this.isConnecting(connectionInfo)
|
|
60
68
|
|| this.isFailed(connectionInfo)
|
|
61
69
|
|| this.isIgnored(connectionInfo)) return;
|
|
62
70
|
|
|
63
|
-
this.
|
|
71
|
+
this.connectToDevice(connectionInfo);
|
|
72
|
+
}
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Disconnect from all connected devices
|
|
76
|
+
*/
|
|
77
|
+
disconnectAll() {
|
|
78
|
+
for (const device of this.devices.values()) {
|
|
79
|
+
device.networkDevice.disconnect();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
68
82
|
|
|
69
|
-
|
|
83
|
+
get databases() {
|
|
84
|
+
return this._databases;
|
|
85
|
+
}
|
|
70
86
|
|
|
87
|
+
async downloadFile(deviceId: string, path: string) {
|
|
88
|
+
const device = this.devices.get(deviceId);
|
|
89
|
+
const file = await device.fileTransferService.getFile(path);
|
|
90
|
+
return file;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
////////////////////////////////////////////////////////////////////////////
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Waits for all devices to be connected with databases downloaded
|
|
97
|
+
* then connects to the StateMap.
|
|
98
|
+
*/
|
|
99
|
+
private waitForAllDevices() {
|
|
100
|
+
Logger.log('Start watching for devices ...');
|
|
101
|
+
this.deviceWatchTimeout = setInterval(async () => {
|
|
102
|
+
// Check if any devices are still connecting.
|
|
103
|
+
const values = Array.from(this.discoveryStatus.values());
|
|
104
|
+
const foundDevices = values.length > 1;
|
|
105
|
+
const allConnected = !values.includes(ConnectionStatus.CONNECTING);
|
|
106
|
+
|
|
107
|
+
if (foundDevices && allConnected) {
|
|
108
|
+
Logger.log('All devices found!');
|
|
109
|
+
clearInterval(this.deviceWatchTimeout);
|
|
110
|
+
for (const cb of this.stateMapCallback) {
|
|
111
|
+
this.setupStateMap(cb.connectionInfo, cb.networkDevice);
|
|
112
|
+
}
|
|
113
|
+
this.emit('ready');
|
|
114
|
+
} else {
|
|
115
|
+
Logger.log(`Waiting for devices ...`);
|
|
116
|
+
}
|
|
117
|
+
}, WAIT_FOR_DEVICES_TIMEOUT_MS);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Attempt to connect to a device. Retry if necessary.
|
|
122
|
+
*
|
|
123
|
+
* @param connectionInfo Connection info
|
|
124
|
+
* @returns
|
|
125
|
+
*/
|
|
126
|
+
private async connectToDevice(connectionInfo: ConnectionInfo) {
|
|
127
|
+
this.discoveryStatus.set(this.deviceId(connectionInfo), ConnectionStatus.CONNECTING);
|
|
128
|
+
let attempt = 1;
|
|
71
129
|
while (attempt < this.options.maxRetries) {
|
|
72
130
|
try {
|
|
73
131
|
Logger.info(`Connecting to ${this.deviceId(connectionInfo)}. ` +
|
|
74
132
|
`Attempt ${attempt}/${this.options.maxRetries}`);
|
|
75
|
-
|
|
76
|
-
await this.connectToPlayer(connectionInfo);
|
|
133
|
+
await this.downloadDatabase(connectionInfo);
|
|
77
134
|
this.discoveryStatus.set(this.deviceId(connectionInfo), ConnectionStatus.CONNECTED);
|
|
78
|
-
this.emit('
|
|
135
|
+
this.emit('connected', connectionInfo);
|
|
79
136
|
return; // Don't forget to return!
|
|
80
137
|
} catch(e) {
|
|
81
138
|
|
|
@@ -84,49 +141,28 @@ export class StageLinqDevices extends EventEmitter {
|
|
|
84
141
|
`(${attempt}/${this.options.maxRetries}): ${e}`);
|
|
85
142
|
attempt += 1;
|
|
86
143
|
sleep(500);
|
|
87
|
-
|
|
88
144
|
}
|
|
89
145
|
}
|
|
90
|
-
|
|
91
146
|
// We failed 3 times. Throw exception.
|
|
92
147
|
this.discoveryStatus.set(this.deviceId(connectionInfo), ConnectionStatus.FAILED);
|
|
93
148
|
throw new Error(`Could not connect to ${this.deviceId(connectionInfo)}`);
|
|
94
149
|
}
|
|
95
150
|
|
|
96
151
|
/**
|
|
97
|
-
*
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
for (const device of this.devices.values()) {
|
|
101
|
-
device.networkDevice.disconnect();
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
get databases() {
|
|
106
|
-
return this._databases;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async downloadFile(ipAddress: string, path: string) {
|
|
110
|
-
const device = this.devices.get(ipAddress);
|
|
111
|
-
const file = await device.fileTransferService.getFile(path);
|
|
112
|
-
return file;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
////////////////////////////////////////////////////////////////////////////
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Connect to the player.
|
|
119
|
-
* @param connectionInfo Device to connect to.
|
|
152
|
+
* Download databases from the device.
|
|
153
|
+
*
|
|
154
|
+
* @param connectionInfo Connection info
|
|
120
155
|
* @returns
|
|
121
156
|
*/
|
|
122
|
-
private async
|
|
157
|
+
private async downloadDatabase(connectionInfo: ConnectionInfo) {
|
|
123
158
|
const networkDevice = new NetworkDevice(connectionInfo);
|
|
124
159
|
await networkDevice.connect();
|
|
125
160
|
|
|
161
|
+
const sourceId = this.sourceId(connectionInfo);
|
|
126
162
|
Logger.info(`Successfully connected to ${this.deviceId(connectionInfo)}`);
|
|
127
163
|
const fileTransfer = await networkDevice.connectToService(FileTransfer);
|
|
128
164
|
|
|
129
|
-
this.devices.set(
|
|
165
|
+
this.devices.set(`net://${sourceId}`, {
|
|
130
166
|
networkDevice: networkDevice,
|
|
131
167
|
fileTransferService: fileTransfer
|
|
132
168
|
});
|
|
@@ -136,7 +172,24 @@ export class StageLinqDevices extends EventEmitter {
|
|
|
136
172
|
Logger.debug(`Database sources: ${sources.join(', ')}`);
|
|
137
173
|
}
|
|
138
174
|
|
|
175
|
+
// Append to the list of states we need to setup later.
|
|
176
|
+
this.stateMapCallback.push({ connectionInfo, networkDevice });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private sourceId(connectionInfo: ConnectionInfo) {
|
|
180
|
+
return /(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/i
|
|
181
|
+
.exec(Buffer.from(connectionInfo.token).toString('hex')).splice(1).join('-');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Setup stateMap.
|
|
186
|
+
* @param connectionInfo Connection info
|
|
187
|
+
* @param networkDevice Network device
|
|
188
|
+
*/
|
|
189
|
+
private async setupStateMap(connectionInfo: ConnectionInfo, networkDevice: NetworkDevice) {
|
|
139
190
|
// Setup StateMap
|
|
191
|
+
Logger.debug(`Setting up stateMap for ${connectionInfo.address}`);
|
|
192
|
+
|
|
140
193
|
const stateMap = await networkDevice.connectToService(StateMap);
|
|
141
194
|
stateMap.on('message', (data) => {
|
|
142
195
|
this.emit('message', connectionInfo, data)
|
|
@@ -146,7 +199,8 @@ export class StageLinqDevices extends EventEmitter {
|
|
|
146
199
|
const player = new Player({
|
|
147
200
|
stateMap: stateMap,
|
|
148
201
|
address: connectionInfo.address,
|
|
149
|
-
port: connectionInfo.port
|
|
202
|
+
port: connectionInfo.port,
|
|
203
|
+
deviceId: this.sourceId(connectionInfo)
|
|
150
204
|
});
|
|
151
205
|
|
|
152
206
|
player.on('trackLoaded', (status) => {
|
|
@@ -160,9 +214,6 @@ export class StageLinqDevices extends EventEmitter {
|
|
|
160
214
|
player.on('nowPlaying', (status) => {
|
|
161
215
|
this.emit('nowPlaying', status);
|
|
162
216
|
});
|
|
163
|
-
|
|
164
|
-
this.emit('connected', connectionInfo);
|
|
165
|
-
|
|
166
217
|
}
|
|
167
218
|
|
|
168
219
|
private deviceId(device: ConnectionInfo) {
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
diff --git a/Databases/Databases.ts b/Databases/Databases.ts
|
|
2
|
+
index 10e0cba..4248e1e 100644
|
|
3
|
+
--- a/Databases/Databases.ts
|
|
4
|
+
+++ b/Databases/Databases.ts
|
|
5
|
+
@@ -25,7 +25,11 @@ export class Databases extends EventEmitter {
|
|
6
|
+
const sources = await service.getSources();
|
|
7
|
+
const output: string[] = [];
|
|
8
|
+
for (const source of sources) {
|
|
9
|
+
- const dbConnectionName = `${connectionInfo.address}_${connectionInfo.port}_${source.name}`;
|
|
10
|
+
+ const deviceId = /(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/i
|
|
11
|
+
+ .exec(Buffer.from(connectionInfo.token).toString('hex')).splice(1).join('-');
|
|
12
|
+
+
|
|
13
|
+
+ const dbConnectionName = `net://${deviceId}/${source.name}`;
|
|
14
|
+
+ Logger.debug(`DB network path: net://${deviceId}/${source.name}`)
|
|
15
|
+
if (this.sources.has(dbConnectionName)) {
|
|
16
|
+
Logger.debug(`Already seen ${source} on ${connectionInfo.address}:${connectionInfo.port}`);
|
|
17
|
+
} else {
|
|
18
|
+
diff --git a/Databases/DbConnection.ts b/Databases/DbConnection.ts
|
|
19
|
+
index 24ebd3a..6294024 100644
|
|
20
|
+
--- a/Databases/DbConnection.ts
|
|
21
|
+
+++ b/Databases/DbConnection.ts
|
|
22
|
+
@@ -20,7 +20,7 @@ export class DbConnection {
|
|
23
|
+
* @returns
|
|
24
|
+
*/
|
|
25
|
+
querySource<T>(query: string, ...params: any[]): T[] {
|
|
26
|
+
- console.debug(`Querying ${this.dbPath}: `);
|
|
27
|
+
+ console.debug(`Querying ${this.dbPath}: ${query}`);
|
|
28
|
+
const result = this.db.prepare(query);
|
|
29
|
+
return result.all(params);
|
|
30
|
+
}
|
|
31
|
+
@@ -32,7 +32,7 @@ export class DbConnection {
|
|
32
|
+
* @returns
|
|
33
|
+
*/
|
|
34
|
+
getTrackInfo(trackPath: string) {
|
|
35
|
+
- return this.querySource(`SELECT * FROM Track WHERE path = '${trackPath}'`);
|
|
36
|
+
+ return this.querySource(`SELECT * FROM Track WHERE path = ?`, trackPath);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
close() {
|
|
40
|
+
diff --git a/cli/index.ts b/cli/index.ts
|
|
41
|
+
index 8f532ca..5649037 100644
|
|
42
|
+
--- a/cli/index.ts
|
|
43
|
+
+++ b/cli/index.ts
|
|
44
|
+
@@ -96,7 +96,7 @@ require('console-stamp')(console, {
|
|
45
|
+
// Example of how to download the actual track from the media.
|
|
46
|
+
try {
|
|
47
|
+
const tempfile = path.resolve(os.tmpdir(), 'media');
|
|
48
|
+
- const data = await stageLinq.devices.downloadFile(status.address, status.trackPathAbsolute);
|
|
49
|
+
+ const data = await stageLinq.devices.downloadFile(status.deviceId, status.trackPathAbsolute);
|
|
50
|
+
if (data) {
|
|
51
|
+
fs.writeFileSync(tempfile, Buffer.from(data));
|
|
52
|
+
console.log(`Downloaded ${status.trackPathAbsolute} to ${tempfile}`);
|
|
53
|
+
diff --git a/devices/Player.ts b/devices/Player.ts
|
|
54
|
+
index 64f0db0..c6193ca 100644
|
|
55
|
+
--- a/devices/Player.ts
|
|
56
|
+
+++ b/devices/Player.ts
|
|
57
|
+
@@ -13,8 +13,9 @@ export declare interface Player {
|
|
58
|
+
|
|
59
|
+
interface PlayerOptions {
|
|
60
|
+
stateMap: StateMap;
|
|
61
|
+
- address: string,
|
|
62
|
+
+ address: string;
|
|
63
|
+
port: number;
|
|
64
|
+
+ deviceId: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface SourceAndTrackPath {
|
|
68
|
+
@@ -48,6 +49,7 @@ export class Player extends EventEmitter {
|
|
69
|
+
private masterStatus: boolean; // If this device has the matser tempo
|
|
70
|
+
private decks: Map<string, PlayerLayerState> = new Map();
|
|
71
|
+
private queue: {[layer: string]: PlayerMessageQueue} = {};
|
|
72
|
+
+ private deviceId: string;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Initialize a player device.
|
|
76
|
+
@@ -60,6 +62,8 @@ export class Player extends EventEmitter {
|
|
77
|
+
options.stateMap.on('message', this.messageHandler.bind(this));
|
|
78
|
+
this.address = options.address;
|
|
79
|
+
this.port = options.port;
|
|
80
|
+
+ this.deviceId = options.deviceId;
|
|
81
|
+
+
|
|
82
|
+
this.queue = {
|
|
83
|
+
A: new PlayerMessageQueue('A').onDataReady(this.handleUpdate.bind(this)),
|
|
84
|
+
B: new PlayerMessageQueue('B').onDataReady(this.handleUpdate.bind(this)),
|
|
85
|
+
@@ -150,22 +154,27 @@ export class Player extends EventEmitter {
|
|
86
|
+
port: this.port,
|
|
87
|
+
masterTempo: this.masterTempo,
|
|
88
|
+
masterStatus: this.masterStatus,
|
|
89
|
+
+ deviceId: `net://${this.deviceId}`,
|
|
90
|
+
...result
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// We're casting here because we originally built it up piecemeal.
|
|
94
|
+
const currentState = output as PlayerStatus;
|
|
95
|
+
|
|
96
|
+
- if (/Unknown/.test(currentState.source)) {
|
|
97
|
+
+ if (currentState.trackNetworkPath && currentState.trackNetworkPath.startsWith('net:')) {
|
|
98
|
+
+ const pathParts = currentState.trackNetworkPath.split('net://')[1].split('/', 2)
|
|
99
|
+
+ currentState.dbSourceName = `net://${pathParts[0]}/${pathParts[1]}`;
|
|
100
|
+
+ currentState.deviceId = `net://${pathParts[0]}`;
|
|
101
|
+
+ } else if (!currentState.source || /Unknown/.test(currentState.source)) {
|
|
102
|
+
// Tracks from streaming sources won't be in the database.
|
|
103
|
+
currentState.dbSourceName = '';
|
|
104
|
+
} else {
|
|
105
|
+
- currentState.dbSourceName = currentState.source
|
|
106
|
+
- ? `${this.address}_${this.port}_${currentState.source}` : '';
|
|
107
|
+
+ currentState.dbSourceName = `net://${this.deviceId}/${currentState.source}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If a song is loaded and we have a location emit the trackLoaded event.
|
|
111
|
+
- if (songLoadedSignalPresent && currentState.trackNetworkPath)
|
|
112
|
+
+ //
|
|
113
|
+
+ if (/* songLoadedSignalPresent && */ currentState.trackNetworkPath)
|
|
114
|
+
this.emit('trackLoaded', currentState);
|
|
115
|
+
|
|
116
|
+
// If the song is actually playing emit the nowPlaying event.
|
|
117
|
+
diff --git a/network/StageLinqDevices.ts b/network/StageLinqDevices.ts
|
|
118
|
+
index 949619a..6bdce8b 100644
|
|
119
|
+
--- a/network/StageLinqDevices.ts
|
|
120
|
+
+++ b/network/StageLinqDevices.ts
|
|
121
|
+
@@ -106,8 +106,8 @@ export class StageLinqDevices extends EventEmitter {
|
|
122
|
+
return this._databases;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
- async downloadFile(ipAddress: string, path: string) {
|
|
126
|
+
- const device = this.devices.get(ipAddress);
|
|
127
|
+
+ async downloadFile(deviceId: string, path: string) {
|
|
128
|
+
+ const device = this.devices.get(deviceId);
|
|
129
|
+
const file = await device.fileTransferService.getFile(path);
|
|
130
|
+
return file;
|
|
131
|
+
}
|
|
132
|
+
@@ -123,10 +123,12 @@ export class StageLinqDevices extends EventEmitter {
|
|
133
|
+
const networkDevice = new NetworkDevice(connectionInfo);
|
|
134
|
+
await networkDevice.connect();
|
|
135
|
+
|
|
136
|
+
+ const deviceId = /(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/i
|
|
137
|
+
+ .exec(Buffer.from(connectionInfo.token).toString('hex')).splice(1).join('-');
|
|
138
|
+
Logger.info(`Successfully connected to ${this.deviceId(connectionInfo)}`);
|
|
139
|
+
const fileTransfer = await networkDevice.connectToService(FileTransfer);
|
|
140
|
+
|
|
141
|
+
- this.devices.set(connectionInfo.address, {
|
|
142
|
+
+ this.devices.set(`net://${deviceId}`, {
|
|
143
|
+
networkDevice: networkDevice,
|
|
144
|
+
fileTransferService: fileTransfer
|
|
145
|
+
});
|
|
146
|
+
@@ -146,7 +148,8 @@ export class StageLinqDevices extends EventEmitter {
|
|
147
|
+
const player = new Player({
|
|
148
|
+
stateMap: stateMap,
|
|
149
|
+
address: connectionInfo.address,
|
|
150
|
+
- port: connectionInfo.port
|
|
151
|
+
+ port: connectionInfo.port,
|
|
152
|
+
+ deviceId
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
player.on('trackLoaded', (status) => {
|
|
156
|
+
diff --git a/types/player.ts b/types/player.ts
|
|
157
|
+
index f672c6d..188d36e 100644
|
|
158
|
+
--- a/types/player.ts
|
|
159
|
+
+++ b/types/player.ts
|
|
160
|
+
@@ -1,6 +1,7 @@
|
|
161
|
+
|
|
162
|
+
export interface PlayerStatus {
|
|
163
|
+
address: string;
|
|
164
|
+
+ deviceId: string;
|
|
165
|
+
artist: string;
|
|
166
|
+
currentBpm: number
|
|
167
|
+
deck: string;
|
|
168
|
+
diff --git a/utils/getTempFilePath.ts b/utils/getTempFilePath.ts
|
|
169
|
+
index c0d8bca..d737fe5 100644
|
|
170
|
+
--- a/utils/getTempFilePath.ts
|
|
171
|
+
+++ b/utils/getTempFilePath.ts
|
|
172
|
+
@@ -9,7 +9,8 @@ import * as path from 'path';
|
|
173
|
+
* @returns Absolute path
|
|
174
|
+
*/
|
|
175
|
+
export function getTempFilePath(p_path: string) {
|
|
176
|
+
- const tmpPath = `/${os.tmpdir()}/localdb/${p_path}`;
|
|
177
|
+
+ const tmpPath = `/${os.tmpdir()}/localdb/${p_path}`
|
|
178
|
+
+ .replace('net://', '');
|
|
179
|
+
let paths = tmpPath.split(/[/\\]/).filter((e) => e.length > 0);
|
|
180
|
+
const isFolder = p_path.endsWith('/') || p_path.endsWith('\\');
|
|
181
|
+
let filename = '';
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stagelinq",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Typescript library to connect to Denon StageLinq devices",
|
|
5
|
+
"homepage": "https://github.com/chrisle/StageLinq",
|
|
5
6
|
"scripts": {
|
|
6
7
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
7
8
|
"start": "node dist/cli/index.js",
|
package/services/FileTransfer.ts
CHANGED
|
@@ -169,6 +169,12 @@ export class FileTransfer extends Service<FileTransferData> {
|
|
|
169
169
|
this.receivedFile = new WriteContext({ size: txinfo.size });
|
|
170
170
|
|
|
171
171
|
const totalChunks = Math.ceil(txinfo.size / CHUNK_SIZE);
|
|
172
|
+
const total = parseInt(txinfo.size);
|
|
173
|
+
|
|
174
|
+
if (total === 0) {
|
|
175
|
+
Logger.warn(`${p_location} doesn't exist or is a streaming file`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
172
178
|
|
|
173
179
|
await this.requestChunkRange(txinfo.txid, 0, totalChunks - 1);
|
|
174
180
|
|
|
@@ -179,7 +185,6 @@ export class FileTransfer extends Service<FileTransferData> {
|
|
|
179
185
|
}, DOWNLOAD_TIMEOUT);
|
|
180
186
|
|
|
181
187
|
while (this.receivedFile.isEOF() === false) {
|
|
182
|
-
const total = parseInt(txinfo.size);
|
|
183
188
|
const bytesDownloaded = total - this.receivedFile.sizeLeft();
|
|
184
189
|
const percentComplete = (bytesDownloaded / total) * 100;
|
|
185
190
|
this.emit('fileTransferProgress', {
|
package/types/player.ts
CHANGED
|
@@ -4,6 +4,7 @@ export interface PlayerStatus {
|
|
|
4
4
|
artist: string;
|
|
5
5
|
currentBpm: number
|
|
6
6
|
deck: string;
|
|
7
|
+
deviceId: string;
|
|
7
8
|
externalMixerVolume: number;
|
|
8
9
|
fileLocation: string;
|
|
9
10
|
hasTrackData: boolean;
|
|
@@ -22,6 +23,7 @@ export interface PlayerStatus {
|
|
|
22
23
|
source: string;
|
|
23
24
|
dbSourceName: string;
|
|
24
25
|
trackPath: string;
|
|
26
|
+
trackPathAbsolute: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export interface PlayerLayerState {
|
package/utils/getTempFilePath.ts
CHANGED
|
@@ -9,7 +9,7 @@ import * as path from 'path';
|
|
|
9
9
|
* @returns Absolute path
|
|
10
10
|
*/
|
|
11
11
|
export function getTempFilePath(p_path: string) {
|
|
12
|
-
const tmpPath = `/${os.tmpdir()}/localdb/${p_path}
|
|
12
|
+
const tmpPath = `/${os.tmpdir()}/localdb/${p_path}`.replace('net://', '');
|
|
13
13
|
let paths = tmpPath.split(/[/\\]/).filter((e) => e.length > 0);
|
|
14
14
|
const isFolder = p_path.endsWith('/') || p_path.endsWith('\\');
|
|
15
15
|
let filename = '';
|