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.
@@ -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 dbConnectionName = `${connectionInfo.address}_${connectionInfo.port}_${source.name}`;
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: string) {
64
- if (!this.sources.has(dbSourceName))
65
- throw new Error(`Database name ${dbSourceName} does not exist`);
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
- return this.querySource(`SELECT * FROM Track WHERE path = '${trackPath}'`);
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
  }
@@ -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 = new 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 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
+ // 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 && status.dbSourceName) {
85
+ if (stageLinq.options.downloadDbSources) {
83
86
  try {
84
- const connection = new DbConnection(stageLinq.databases.getDbPath(status.dbSourceName));
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 it because we originally built it up piecemeal.
160
+ // We're casting here because we originally built it up piecemeal.
155
161
  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);
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: (connectionInfo: ConnectionInfo) => void): this;
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
- * Attempt to connect to the player.
57
+ * Handle incoming discovery messages from the StageLinq network
49
58
  *
50
- * @param connectionInfo Device discovered
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, connecting, failed, or
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.discoveryStatus.set(this.deviceId(connectionInfo), ConnectionStatus.CONNECTING);
71
+ this.connectToDevice(connectionInfo);
72
+ }
64
73
 
65
- // Retrying appears to be necessary because it seems the Denon hardware
66
- // sometimes doesn't connect. Retrying after a little wait seems to
67
- // solve the issue.
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
- let attempt = 1;
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
- // If this fails, catch it, and maybe retry if necessary.
76
- await this.connectToPlayer(connectionInfo);
133
+ await this.downloadDatabase(connectionInfo);
77
134
  this.discoveryStatus.set(this.deviceId(connectionInfo), ConnectionStatus.CONNECTED);
78
- this.emit('ready', connectionInfo);
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
- * Disconnect from all connected devices
98
- */
99
- disconnectAll() {
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 connectToPlayer(connectionInfo: ConnectionInfo) {
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(connectionInfo.address, {
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.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",
@@ -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 {
@@ -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 = '';