diff --git a/.gitignore b/.gitignore index a56a7ef..0771922 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ node_modules - +/dist \ No newline at end of file diff --git a/lib/airplay.js b/lib/airplay.js deleted file mode 100644 index b02e796..0000000 --- a/lib/airplay.js +++ /dev/null @@ -1,10 +0,0 @@ -exports.Browser = require('./airplay/browser').Browser; -exports.createBrowser = function() { - return new exports.Browser(); -}; - -exports.Device = require('./airplay/device').Device; -exports.connect = function(host, port, opt_pass) { - // TODO: connect - throw 'not yet implemented'; -}; diff --git a/lib/airplay.ts b/lib/airplay.ts new file mode 100644 index 0000000..12ac8a4 --- /dev/null +++ b/lib/airplay.ts @@ -0,0 +1,13 @@ +export { Browser } from './airplay/browser.js'; +export { Device } from './airplay/device.js'; + +import { Browser } from './airplay/browser.js'; + +export function createBrowser(): Browser { + return new Browser(); +} + +export function connect(host: string, port: number, pass?: unknown): never { + // TODO: connect + throw 'not yet implemented'; +} \ No newline at end of file diff --git a/lib/airplay/browser.js b/lib/airplay/browser.js deleted file mode 100644 index 4fb997a..0000000 --- a/lib/airplay/browser.js +++ /dev/null @@ -1,73 +0,0 @@ -var events = require('events'); -var mdns = require('mdns'); -var util = require('util'); - -var Device = require('./device').Device; - -var Browser = function() { - var self = this; - - this.devices_ = {}; - this.nextDeviceId_ = 0; - - this.browser_ = mdns.createBrowser(mdns.tcp('airplay')); - this.browser_.on('serviceUp', function(info, flags) { - var device = self.findDeviceByInfo_(info); - if (!device) { - device = new Device(self.nextDeviceId_++, info); - self.devices_[device.id] = device; - device.on('ready', function() { - self.emit('deviceOnline', device); - }); - device.on('close', function() { - delete self.devices_[device.id]; - self.emit('deviceOffline', device); - }); - } - }); - this.browser_.on('serviceDown', function(info, flags) { - var device = self.findDeviceByInfo_(info); - if (device) { - device.close(); - } - }); -}; -util.inherits(Browser, events.EventEmitter); -exports.Browser = Browser; - -Browser.prototype.findDeviceByInfo_ = function(info) { - for (var deviceId in this.devices_) { - var device = this.devices_[deviceId]; - if (device.matchesInfo(info)) { - return device; - } - } - return null; -}; - -Browser.prototype.getDevices = function() { - var devices = []; - for (var deviceId in this.devices_) { - var device = this.devices_[deviceId]; - if (device.isReady()) { - devices.push(device); - } - } - return devices; -}; - -Browser.prototype.getDeviceById = function(deviceId) { - var device = this.devices_[deviceId]; - if (device && device.isReady()) { - return device; - } - return null; -}; - -Browser.prototype.start = function() { - this.browser_.start(); -}; - -Browser.prototype.stop = function() { - this.browser_.stop(); -}; diff --git a/lib/airplay/browser.ts b/lib/airplay/browser.ts new file mode 100644 index 0000000..3877d9c --- /dev/null +++ b/lib/airplay/browser.ts @@ -0,0 +1,71 @@ +import { EventEmitter } from "node:events"; +import { createBrowser, tcp } from "mdns"; +import { Device } from "./device.js"; + +import type { Info } from "./device.js"; + +export class Browser extends EventEmitter { + private devices: Record = {}; + private nextDeviceId: number = 0; + private browser = createBrowser(tcp("airplay")); + + constructor() { + super(); + + this.browser.on("serviceUp",(info: Info, flags) => { + let device = this.findDeviceByInfo(info) ?? new Device(this.nextDeviceId++,info); + this.devices[device.id] = device; + + device.on("ready",() => { + this.emit("deviceOnline",device); + }); + + device.on("close",() => { + delete this.devices[device.id]; + this.emit("deviceOffline",device); + }); + }); + + this.browser.on("serviceDown",(info: Info, flags) => { + let device = this.findDeviceByInfo(info); + device?.close(); + }); + } + + private findDeviceByInfo(info: Info): Device | null { + for (const deviceId in this.devices){ + const device = this.devices[deviceId]; + if (device.matchesInfo(info)){ + return device; + } + } + return null; + } + + getDevices(): Device[] { + const devices: Device[] = []; + for (const deviceId in this.devices){ + const device = this.devices[deviceId]; + if (device.isReady()){ + devices.push(device); + } + } + return devices; + } + + getDeviceById(deviceId: number): Device | null { + const device = this.devices[deviceId]; + if (device && device.isReady()){ + return device; + } + return null; + } + + start(): void { + this.browser.start(); + } + + stop(): void { + this.browser.stop(); + } +} \ No newline at end of file diff --git a/lib/airplay/client.js b/lib/airplay/client.js deleted file mode 100644 index 5694448..0000000 --- a/lib/airplay/client.js +++ /dev/null @@ -1,134 +0,0 @@ -var buffer = require('buffer'); -var events = require('events'); -var net = require('net'); -var util = require('util'); - -var Client = function(host, port, user, pass, callback) { - var self = this; - - this.host_ = host; - this.port_ = port; - this.user_ = user; - this.pass_ = pass; - - this.responseWaiters_ = []; - - this.socket_ = net.createConnection(port, host); - this.socket_.on('connect', function() { - self.responseWaiters_.push({ - callback: callback - }); - self.socket_.write( - 'GET /playback-info HTTP/1.1\n' + - 'User-Agent: MediaControl/1.0\n' + - 'Content-Length: 0\n' + - '\n'); - }); - - this.socket_.on('data', function(data) { - var res = self.parseResponse_(data.toString()); - //util.puts(util.inspect(res)); - - var waiter = self.responseWaiters_.shift(); - if (waiter.callback) { - waiter.callback(res); - } - }); -}; -util.inherits(Client, events.EventEmitter); -exports.Client = Client; - -Client.prototype.close = function() { - if (this.socket_) { - this.socket_.destroy(); - } - this.socket_ = null; -}; - -Client.prototype.parseResponse_ = function(res) { - // Look for HTTP response: - // HTTP/1.1 200 OK - // Some-Header: value - // Content-Length: 427 - // \n - // body (427 bytes) - - var header = res; - var body = ''; - var splitPoint = res.indexOf('\r\n\r\n'); - if (splitPoint != -1) { - header = res.substr(0, splitPoint); - body = res.substr(splitPoint + 4); - } - - // Normalize header \r\n -> \n - header = header.replace(/\r\n/g, '\n'); - - // Peel off status - var status = header.substr(0, header.indexOf('\n')); - var statusMatch = status.match(/HTTP\/1.1 ([0-9]+) (.+)/); - header = header.substr(status.length + 1); - - // Parse headers - var allHeaders = {}; - var headerLines = header.split('\n'); - for (var n = 0; n < headerLines.length; n++) { - var headerLine = headerLines[n]; - var key = headerLine.substr(0, headerLine.indexOf(':')); - var value = headerLine.substr(key.length + 2); - allHeaders[key] = value; - } - - // Trim body? - return { - statusCode: parseInt(statusMatch[1]), - statusReason: statusMatch[2], - headers: allHeaders, - body: body - }; -}; - -Client.prototype.issue_ = function(req, body, callback) { - if (!this.socket_) { - util.puts('client not connected'); - return; - } - - req.headers = req.headers || {}; - req.headers['User-Agent'] = 'MediaControl/1.0'; - req.headers['Content-Length'] = body ? buffer.Buffer.byteLength(body) : 0; - req.headers['Connection'] = 'keep-alive'; - - var allHeaders = ''; - for (var key in req.headers) { - allHeaders += key + ': ' + req.headers[key] + '\n'; - } - - var text = - req.method + ' ' + req.path + ' HTTP/1.1\n' + - allHeaders + '\n'; - if (body) { - text += body; - } - - this.responseWaiters_.push({ - callback: callback - }); - this.socket_.write(text); -}; - -Client.prototype.get = function(path, callback) { - var req = { - method: 'GET', - path: path, - }; - this.issue_(req, null, callback); -}; - -Client.prototype.post = function(path, body, callback) { - var req = { - method: 'POST', - path: path, - }; - this.issue_(req, body, callback); -}; diff --git a/lib/airplay/client.ts b/lib/airplay/client.ts new file mode 100644 index 0000000..e926fbd --- /dev/null +++ b/lib/airplay/client.ts @@ -0,0 +1,123 @@ +import { EventEmitter } from "node:events"; +import { createConnection } from "node:net"; + +import type { Socket } from "node:net"; + +export type ResponseWaiter = (response: ParsedResponse | null) => void; + +export interface ParsedResponse { + statusCode: number; + statusReason: string; + headers: Record; + body: string; +} + +export class Client extends EventEmitter { + private responseWaiters: { callback: ResponseWaiter; }[] = []; + private socket: Socket | null; + + constructor(private host: string, private port: number, private user: string, private pass: string, callback: ResponseWaiter) { + super(); + this.socket = createConnection(this.port,this.host); + + this.socket.on("connect",() => { + this.responseWaiters.push({ callback }); + this.socket!.write( + "GET /playback-info HTTP/1.1\n" + + "User-Agent: MediaControl/1.0\n" + + "Content-Length: 0\n" + + "\n"); + }); + + this.socket.on("data",data => { + const res = this.parseResponse(data.toString()); + //util.puts(util.inspect(res)); + const waiter = this.responseWaiters.shift(); + waiter?.callback(res); + }); + } + + close(): void { + this.socket?.destroy(); + this.socket = null; + } + + private parseResponse(res: string): ParsedResponse { + // Look for HTTP response: + // HTTP/1.1 200 OK + // Some-Header: value + // Content-Length: 427 + // \n + // body (427 bytes) + let header = res; + let body = ""; + const splitPoint = res.indexOf("\r\n\r\n"); + + if (splitPoint != -1){ + header = res.substr(0,splitPoint); + body = res.substr(splitPoint + 4); + } + + // Normalize header \r\n -> \n + header = header.replace(/\r\n/g,"\n"); + + // Peel off status + const status = header.substr(0,header.indexOf("\n")); + const statusMatch = status.match(/HTTP\/1.1 ([0-9]+) (.+)/)!; + header = header.substr(status.length + 1); + + // Parse headers + const allHeaders: Record = {}; + const headerLines = header.split("\n"); + + for (let n = 0; n < headerLines.length; n++){ + const headerLine = headerLines[n]; + const key = headerLine.substr(0,headerLine.indexOf(":")); + const value = headerLine.substr(key.length + 2); + allHeaders[key] = value; + } + + // Trim body? + return { + statusCode: parseInt(statusMatch[1]), + statusReason: statusMatch[2], + headers: allHeaders, + body + }; + } + + private issue(req, body: string | null, callback: ResponseWaiter): void { + if (!this.socket){ + console.error("client not connected"); + return; + } + + req.headers = req.headers || {}; + req.headers["User-Agent"] = "MediaControl/1.0"; + req.headers["Content-Length"] = body ? Buffer.byteLength(body) : 0; + req.headers["Connection"] = "keep-alive"; + + let allHeaders = ""; + for (let key in req.headers){ + allHeaders += key + ": " + req.headers[key] + "\n"; + } + + let text = req.method + " " + req.path + " HTTP/1.1\n" + allHeaders + "\n"; + if (body !== null){ + text += body; + } + + this.responseWaiters.push({ callback }); + this.socket.write(text); + } + + get(path: string, callback: ResponseWaiter): void { + const req = { method: "GET", path }; + this.issue(req,null,callback); + } + + post(path: string, body: string | null, callback: ResponseWaiter): void { + const req = { method: "POST", path }; + this.issue(req,body,callback); + } +} \ No newline at end of file diff --git a/lib/airplay/device.js b/lib/airplay/device.js deleted file mode 100644 index 3ba7c0d..0000000 --- a/lib/airplay/device.js +++ /dev/null @@ -1,187 +0,0 @@ -var events = require('events'); -var plist = require('plist'); -var util = require('util'); - -var Client = require('./client').Client; - -var Device = function(id, info, opt_readyCallback) { - var self = this; - - this.id = id; - this.info_ = info; - this.serverInfo_ = null; - this.ready_ = false; - - var host = info.host; - var port = info.port; - var user = 'Airplay'; - var pass = ''; - this.client_ = new Client(host, port, user, pass, function() { - // TODO: support passwords - - self.client_.get('/server-info', function(res) { - plist.parse(res.body, function(err, obj) { - var el = obj[0]; - self.serverInfo_ = { - deviceId: el.deviceid, - features: el.features, - model: el.model, - protocolVersion: el.protovers, - sourceVersion: el.srcvers - }; - }); - - self.makeReady_(opt_readyCallback); - }); - }); -}; -util.inherits(Device, events.EventEmitter); -exports.Device = Device; - -Device.prototype.isReady = function() { - return this.ready_; -}; - -Device.prototype.makeReady_ = function(opt_readyCallback) { - this.ready_ = true; - if (opt_readyCallback) { - opt_readyCallback(this); - } - this.emit('ready'); -}; - -Device.prototype.close = function() { - if (this.client_) { - this.client_.close(); - } - this.client_ = null; - this.ready_ = false; - - this.emit('close'); -}; - -Device.prototype.getInfo = function() { - var info = this.info_; - var serverInfo = this.serverInfo_; - return { - id: this.id, - name: info.serviceName, - deviceId: info.host, - features: serverInfo.features, - model: serverInfo.model, - slideshowFeatures: [], - supportedContentTypes: [] - }; -}; - -Device.prototype.getName = function() { - return this.info_.serviceName; -}; - -Device.prototype.matchesInfo = function(info) { - for (var key in info) { - if (this.info_[key] != info[key]) { - return false; - } - } - return true; -}; - -Device.prototype.default = function(callback) { - if (callback) { - callback(this.getInfo()); - } -}; - -Device.prototype.status = function(callback) { - this.client_.get('/playback-info', function(res) { - if (res) { - plist.parse(res.body, function(err, obj) { - var el = obj[0]; - var result = { - duration: el.duration, - position: el.position, - rate: el.rate, - playbackBufferEmpty: el.playbackBufferEmpty, - playbackBufferFull: el.playbackBufferFull, - playbackLikelyToKeepUp: el.playbackLikelyToKeepUp, - readyToPlay: el.readyToPlay, - loadedTimeRanges: el.loadedTimeRanges, - seekableTimeRanges: el.seekableTimeRanges - }; - if (callback) { - callback(result); - } - }); - } else { - if (callback) { - callback(null); - } - } - }); -}; - -Device.prototype.authorize = function(req, callback) { - // TODO: implement authorize - if (callback) { - callback(null); - } -}; - -Device.prototype.play = function(content, start, callback) { - var body = - 'Content-Location: ' + content + '\n' + - 'Start-Position: ' + start + '\n'; - this.client_.post('/play', body, function(res) { - if (callback) { - callback(res ? {} : null); - } - }); -}; - -Device.prototype.stop = function(callback) { - this.client_.post('/stop', null, function(res) { - if (callback) { - callback(res ? {} : null); - } - }); -}; - -Device.prototype.scrub = function(position, callback) { - this.client_.post('/scrub?position=' + position, null, function(res) { - if (callback) { - callback(res ? {} : null); - } - }); -}; - -Device.prototype.reverse = function(callback) { - this.client_.post('/reverse', null, function(res) { - if (callback) { - callback(res ? {} : null); - } - }) -}; - -Device.prototype.rate = function(value, callback) { - this.client_.post('/rate?value=' + value, null, function(res) { - if (callback) { - callback(res ? {} : null); - } - }) -}; - -Device.prototype.volume = function(value, callback) { - this.client_.post('/volume?value=' + value, null, function(res) { - if (callback) { - callback(res ? {} : null); - } - }) -}; - -Device.prototype.photo = function(req, callback) { - // TODO: implement photo - if (callback) { - callback(null); - } -}; diff --git a/lib/airplay/device.ts b/lib/airplay/device.ts new file mode 100644 index 0000000..307ee27 --- /dev/null +++ b/lib/airplay/device.ts @@ -0,0 +1,178 @@ +import { EventEmitter } from "node:events"; +import { parse } from "plist"; +import { Client } from "./client.js"; + +import type { ResponseWaiter, ParsedResponse } from "./client.js"; + +export type ReadyCallback = (device: Device) => void; + +export interface DeviceInfo { + id: number; + name: string; + deviceId: string; + features: unknown[]; + model: string; + slideshowFeatures: unknown[]; + supportedContentTypes: unknown[]; +} + +export interface Info { + host: string; + port: number; + serviceName: string; +} + +export interface ServerInfo { + features: unknown[]; + model: string; +} + +export class Device extends EventEmitter { + private serverInfo: ServerInfo | null = null; + private ready: boolean = false; + private client: Client | null; + + constructor(public id: number, private info: Info, readyCallback?: ReadyCallback) { + super(); + + const { host, port } = info; + const user = "Airplay"; + const pass = ""; + + this.client = new Client(host,port,user,pass,() => { + // TODO: support passwords + this.client!.get("/server-info",res => { + parse(res.body,(err, obj) => { + var el = obj[0]; + this.serverInfo = { + deviceId: el.deviceid, + features: el.features, + model: el.model, + protocolVersion: el.protovers, + sourceVersion: el.srcvers + }; + }); + + this.makeReady(readyCallback); + }); + }); + } + + isReady(): boolean { + return this.ready; + } + + private makeReady(readyCallback?: ReadyCallback): void { + this.ready = true; + readyCallback?.(this); + this.emit("ready"); + } + + close(): void { + this.client?.close(); + this.client = null; + this.ready = false; + + this.emit("close"); + } + + getInfo(): DeviceInfo { + const { id, info, serverInfo } = this; + const { serviceName: name, host: deviceId } = info; + const { features, model } = serverInfo!; + return { id, name, deviceId, features, model, slideshowFeatures: [], supportedContentTypes: [] }; + } + + getName(): string { + return this.info.serviceName; + } + + matchesInfo(info: Info): boolean { + for (var key in info) { + if (this.info[key] != info[key]) { + return false; + } + } + return true; + } + + default(callback): void { + if (callback) { + callback(this.getInfo()); + } + } + + status(callback?: ResponseWaiter): void { + this.client!.get("/playback-info", (res) => { + if (res) { + parse(res.body, (err, obj) => { + var el = obj[0]; + var result = { + duration: el.duration, + position: el.position, + rate: el.rate, + playbackBufferEmpty: el.playbackBufferEmpty, + playbackBufferFull: el.playbackBufferFull, + playbackLikelyToKeepUp: el.playbackLikelyToKeepUp, + readyToPlay: el.readyToPlay, + loadedTimeRanges: el.loadedTimeRanges, + seekableTimeRanges: el.seekableTimeRanges + }; + if (callback) { + callback(result); + } + }); + } else { + callback?.(null); + } + }); + } + + authorize(req, callback?: ResponseWaiter): void { + // TODO: implement authorize + callback?.(null); + } + + play(content, start, callback?: ResponseWaiter): void { + const body = "Content-Location: " + content + "\n" + + "Start-Position: " + start + "\n"; + this.client!.post("/play",body,res => { + callback?.(res ? {} as ParsedResponse : null); + }); + } + + stop(callback?: ResponseWaiter): void { + this.client!.post("/stop",null,res => { + callback?.(res ? {} as ParsedResponse : null); + }); + } + + scrub(position, callback?: ResponseWaiter): void { + this.client!.post("/scrub?position=" + position,null,res => { + callback?.(res ? {} as ParsedResponse : null); + }); + } + + reverse(callback?: ResponseWaiter): void { + this.client!.post("/reverse",null,res => { + callback?.(res ? {} as ParsedResponse : null); + }); + } + + rate(value, callback?: ResponseWaiter): void { + this.client!.post("/rate?value=" + value,null,res => { + callback?.(res ? {} as ParsedResponse : null); + }); + } + + volume(value, callback?: ResponseWaiter): void { + this.client!.post("/volume?value=" + value,null,res => { + callback?.(res ? {} as ParsedResponse : null); + }); + } + + photo(req, callback?: ResponseWaiter): void { + // TODO: implement photo + callback?.(null); + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..071780f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,114 @@ +{ + "name": "airplay", + "version": "0.0.3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "airplay", + "version": "0.0.3", + "dependencies": { + "mdns": "^2.7.2", + "plist": "^3.1.0" + }, + "devDependencies": { + "@types/mdns": "^0.0.34", + "@types/node": "^20.4.2", + "@types/plist": "^3.0.2" + } + }, + "node_modules/@types/mdns": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@types/mdns/-/mdns-0.0.34.tgz", + "integrity": "sha512-4Rrt/0wRAudtOnmhfDdoFhy5r20yHe0KiDK+/+I9RBBMW67F4S6y8tJH06AzrUDZzS/SH/U2pw1W0lrgQ+OlPg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.4.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz", + "integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==", + "dev": true + }, + "node_modules/@types/plist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.2.tgz", + "integrity": "sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.9.tgz", + "integrity": "sha512-4VSbbcMoxc4KLjb1gs96SRmi7w4h1SF+fCoiK0XaQX62buCc1G5d0DC5bJ9xJBNPDSVCmIrcl8BiYxzjrqaaJA==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bindings": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", + "integrity": "sha512-u4cBQNepWxYA55FunZSM7wMi55yQaN0otnhhilNoWHq0MfOfJeQx0v0mRRpolGOExPjZcl6FtB0BB8Xkb88F0g==" + }, + "node_modules/mdns": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/mdns/-/mdns-2.7.2.tgz", + "integrity": "sha512-NBOQT22DKvuNWVY7nKNbs6w9eGRyPwnc4ZjKOsCG2G/4wNt1+IyiHvc+5yhcAUZLG46cOY321YW7Ufz3lMtrhw==", + "hasInstallScript": true, + "dependencies": { + "bindings": "~1.2.1", + "nan": "^2.14.0" + } + }, + "node_modules/nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==" + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "engines": { + "node": ">=8.0" + } + } + } +} diff --git a/package.json b/package.json index 835047b..3c48b97 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "description": "Apple AirPlay client library", "version": "0.0.3", "author": "Ben Vanik ", - "contributors": [], "repository": { "type": "git", "url": "git://github.com/benvanik/node-airplay.git" @@ -14,18 +13,15 @@ "media", "airplay" ], - "directories": { - "lib": "./lib/airplay" - }, - "main": "./lib/airplay", + "main": "./lib/airplay.js", + "type": "module", "dependencies": { - "plist": ">=0.2.1", - "mdns": ">=0.0.7" - }, - "scripts": { - }, - "engines": { - "node": ">= 0.5.x" + "mdns": "^2.7.2", + "plist": "^3.1.0" }, - "devDependencies": {} -} + "devDependencies": { + "@types/mdns": "^0.0.34", + "@types/node": "^20.4.2", + "@types/plist": "^3.0.2" + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5a92ad5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "rootDir": "./lib", + "outDir": "./dist", + "module": "ESNext", + "moduleResolution": "NodeNext", + "target": "ESNext", + "declaration": true, + "strict": true, + "noImplicitOverride": true + } +} \ No newline at end of file