roku-pkg-cli
Version:
A comprehensive CLI tool for managing multiple Roku projects with automated device discovery, build integration, and package generation. Perfect for CI/CD pipelines with full automation support.
269 lines (268 loc) • 10 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RokuDiscovery = void 0;
const dgram = __importStar(require("dgram"));
const axios_1 = __importDefault(require("axios"));
class RokuDiscovery {
static SSDP_MULTICAST_IP = '239.255.255.250';
static SSDP_PORT = 1900;
static ROKU_ECP_PORT = 8060;
static DISCOVERY_TIMEOUT = 5000;
/**
* Discover Roku devices using SSDP (Simple Service Discovery Protocol)
*/
static async discoverDevices() {
const devices = new Map();
// Try both SSDP and network scanning for better discovery
const [ssdpDevices, networkDevices] = await Promise.allSettled([
this.discoverViaSsdp(),
this.discoverViaNetworkScan()
]);
// Combine results from both methods
if (ssdpDevices.status === 'fulfilled') {
ssdpDevices.value.forEach(device => {
devices.set(device.ip, device);
});
}
if (networkDevices.status === 'fulfilled') {
networkDevices.value.forEach(device => {
// Merge with existing device info if found via SSDP
const existing = devices.get(device.ip);
if (existing) {
devices.set(device.ip, { ...existing, ...device });
}
else {
devices.set(device.ip, device);
}
});
}
return Array.from(devices.values()).sort((a, b) => a.name.localeCompare(b.name));
}
/**
* Discover Roku devices using SSDP
*/
static async discoverViaSsdp() {
return new Promise((resolve) => {
const devices = new Map();
const socket = dgram.createSocket('udp4');
const searchMessage = [
'M-SEARCH * HTTP/1.1',
`HOST: ${this.SSDP_MULTICAST_IP}:${this.SSDP_PORT}`,
'MAN: "ssdp:discover"',
'MX: 3',
'ST: roku:ecp',
'',
''
].join('\r\n');
socket.on('message', async (msg, remote) => {
const response = msg.toString();
if (response.includes('roku:ecp') && response.includes('LOCATION:')) {
const locationMatch = response.match(/LOCATION:\s*(.+)/i);
if (locationMatch) {
const location = locationMatch[1].trim();
try {
const device = await this.getDeviceInfo(location);
if (device) {
devices.set(device.ip, device);
}
}
catch (error) {
// Ignore individual device errors
}
}
}
});
socket.on('error', () => {
// Ignore socket errors, just resolve with what we have
});
socket.bind(() => {
socket.setBroadcast(true);
socket.send(searchMessage, this.SSDP_PORT, this.SSDP_MULTICAST_IP, () => {
setTimeout(() => {
socket.close();
resolve(Array.from(devices.values()));
}, this.DISCOVERY_TIMEOUT);
});
});
});
}
/**
* Discover Roku devices by scanning common network ranges
*/
static async discoverViaNetworkScan() {
const devices = [];
const networkRanges = this.getNetworkRanges();
// Check each potential IP in parallel (limited concurrency)
const checkPromises = [];
for (const range of networkRanges) {
for (let i = 1; i <= 254; i++) {
const ip = `${range}.${i}`;
checkPromises.push(this.checkIfRokuDevice(ip));
}
}
// Process in chunks to avoid overwhelming the network
const chunkSize = 50;
for (let i = 0; i < checkPromises.length; i += chunkSize) {
const chunk = checkPromises.slice(i, i + chunkSize);
const results = await Promise.allSettled(chunk);
results.forEach(result => {
if (result.status === 'fulfilled' && result.value) {
devices.push(result.value);
}
});
}
return devices;
}
/**
* Get common network ranges to scan
*/
static getNetworkRanges() {
// Common private network ranges
const ranges = [
'192.168.1',
'192.168.0',
'10.0.0',
'10.0.1',
'172.16.0'
];
// Try to detect current network range
const os = require('os');
const interfaces = os.networkInterfaces();
for (const name of Object.keys(interfaces)) {
const netInterface = interfaces[name];
if (netInterface) {
for (const net of netInterface) {
if (net.family === 'IPv4' && !net.internal) {
const parts = net.address.split('.');
if (parts.length === 4) {
const networkBase = `${parts[0]}.${parts[1]}.${parts[2]}`;
if (!ranges.includes(networkBase)) {
ranges.unshift(networkBase); // Add to front for priority
}
}
}
}
}
}
return ranges;
}
/**
* Check if an IP address has a Roku device
*/
static async checkIfRokuDevice(ip) {
try {
const response = await axios_1.default.get(`http://${ip}:${this.ROKU_ECP_PORT}/query/device-info`, {
timeout: 2000,
validateStatus: (status) => status === 200
});
if (response.data && response.data.includes('<device-info>')) {
return this.parseDeviceInfo(ip, response.data);
}
}
catch (error) {
// Device not found or not a Roku
}
return null;
}
/**
* Get device info from SSDP location URL
*/
static async getDeviceInfo(location) {
try {
// Extract IP from location URL
const urlMatch = location.match(/http:\/\/([^:\/]+)/);
if (!urlMatch)
return null;
const ip = urlMatch[1];
const deviceInfoResponse = await axios_1.default.get(`http://${ip}:${this.ROKU_ECP_PORT}/query/device-info`, {
timeout: 3000
});
if (deviceInfoResponse.data) {
return this.parseDeviceInfo(ip, deviceInfoResponse.data);
}
}
catch (error) {
// Ignore errors
}
return null;
}
/**
* Parse device info XML response
*/
static parseDeviceInfo(ip, xmlData) {
try {
// Simple XML parsing without external dependencies
const getValue = (tag) => {
const match = xmlData.match(new RegExp(`<${tag}>([^<]+)<\/${tag}>`));
return match ? match[1].trim() : '';
};
const friendlyName = getValue('friendly-device-name') || getValue('user-device-name') || `Roku-${ip}`;
const modelName = getValue('model-name') || getValue('model-number') || 'Unknown Roku';
const serialNumber = getValue('serial-number') || 'Unknown';
const softwareVersion = getValue('software-version');
const deviceType = getValue('device-type');
return {
ip,
name: friendlyName,
modelName,
serialNumber,
softwareVersion,
deviceType
};
}
catch (error) {
return null;
}
}
/**
* Test if a discovered device is reachable and responding
*/
static async testDevice(device) {
try {
const response = await axios_1.default.get(`http://${device.ip}:${this.ROKU_ECP_PORT}/query/device-info`, {
timeout: 3000
});
return response.status === 200;
}
catch (error) {
return false;
}
}
}
exports.RokuDiscovery = RokuDiscovery;