import EventEmitter from 'events';
import { createReadStream } from 'fs';
import type { Metadata } from './interfaces';
export * from './interfaces';
export default class ShairportSyncReaderSimple {
private events = new EventEmitter();
private xmlCache = '';
/**
*
* @param pipe the path to the named pipe that Shairport Sync writes to.
*/
constructor(pipe: string) {
createReadStream(pipe, { encoding: 'utf8' })
.on('data', (data: string) => {
this.xmlCache += data.replace(/\r?\n|\r/g, '');
let firstItem = this.xmlCache.indexOf('- ');
let lastItem = this.xmlCache.lastIndexOf('
');
const itemLength = ''.length;
if (firstItem > 0) {
console.warn('WARNING: An item did not contain the proper start sequence "- ".');
console.warn('WARNING: This incomplete item will be ignored.');
this.xmlCache = this.xmlCache.substring(firstItem); // Remove everything before the first tag
firstItem = this.xmlCache.indexOf('
- '); // Recalculate firstItem after trimming
lastItem = this.xmlCache.lastIndexOf('
'); // Recalculate lastItem after trimming
}
if (firstItem === -1 || lastItem === -1) {
return; // Not enough data to process
}
const xml = this.xmlCache.substring(firstItem, lastItem + itemLength);
this.xmlCache = this.xmlCache.substring(lastItem + itemLength); // Remove processed XML
this.handleData(xml);
})
.on('error', (err: Error) => this.events.emit('error', err));
}
private handleData(xml: string) {
const itemRegex = /- (.*?)<\/type>
(.*?)<\/code>(\d+)<\/length>(?:(.*?)<\/data>)?<\/item>/g;
let match;
while ((match = itemRegex.exec(xml)) !== null) {
const [fullMatch, typeHex, codeHex, length, base64Data] = match;
const type = Buffer.from(typeHex, 'hex').toString('utf8');
const code = Buffer.from(codeHex, 'hex').toString('utf8');
const data = base64Data ? Buffer.from(base64Data, 'base64').toString('utf8') : null;
this.events.emit(type, code, data);
if (data) {
if (type === 'ssnc') {
switch (code) {
case 'pvol':
const [airplayVolume, volume, lowestVolume, highestVolume] = data.split(',');
this.events.emit('pvol', {
airplayVolume: parseFloat(airplayVolume),
volume: parseFloat(volume),
lowestVolume: parseFloat(lowestVolume),
highestVolume: parseFloat(highestVolume)
});
break;
case 'prgr':
const [start, current, end] = data.split(',').map(Number);
this.events.emit('prgr', { start, current, end });
break;
case 'mdst':
case 'mden':
case 'pcst':
case 'pcen':
const timestamp = parseInt(data, 10);
this.events.emit(code, isNaN(timestamp) ? null : timestamp);
break;
default:
this.events.emit(code, data);
break;
}
}
if (type === 'core') {
switch (code) {
case 'astm':
this.events.emit('astm', parseInt(data, 10));
break;
case 'asdk':
this.events.emit('asdk', parseInt(data, 10));
break;
default:
this.events.emit(code, data);
break;
}
}
} else {
this.events.emit(code);
}
// Remove the processed item from the XML
xml = xml.replace(fullMatch, ''); //data.substring(data.indexOf(fullMatch) + fullMatch.length);
itemRegex.lastIndex = 0; // Reset regex index to allow for multiple matches in the same data chunk
}
}
on(event: 'core', listener: (code: string, data: string) => void): this;
on(event: 'ssnc', listener: (code: string, data: string) => void): this;
on(event: T, listener: (data: Metadata[T]) => void): this;
on(event: T | string, listener: (codeOrData: Metadata[T] | string, data: string) => void): this {
this.events.on(event, listener);
return this;
}
}