UNPKG

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
"use strict"; 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;