UNPKG

philtv-js

Version:

A simple library to pair and interact with Philips TV

33 lines (28 loc) 8 kB
#!/usr/bin/env node 'use strict';var consola=require('consola'),B=require('vm'),crypto=require('crypto'),radash=require('radash'),urllib=require('urllib');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}var B__default=/*#__PURE__*/_interopDefault(B);var $="[a-fA-F\\d:]",l=e=>e&&e.includeBoundaries?`(?:(?<=\\s|^)(?=${$})|(?<=${$})(?=\\s|$))`:"",p="(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}",o="[a-fA-F\\d]{1,4}",g=` (?: (?:${o}:){7}(?:${o}|:)| // 1:2:3:4:5:6:7:: 1:2:3:4:5:6:7:8 (?:${o}:){6}(?:${p}|:${o}|:)| // 1:2:3:4:5:6:: 1:2:3:4:5:6::8 1:2:3:4:5:6::8 1:2:3:4:5:6::1.2.3.4 (?:${o}:){5}(?::${p}|(?::${o}){1,2}|:)| // 1:2:3:4:5:: 1:2:3:4:5::7:8 1:2:3:4:5::8 1:2:3:4:5::7:1.2.3.4 (?:${o}:){4}(?:(?::${o}){0,1}:${p}|(?::${o}){1,3}|:)| // 1:2:3:4:: 1:2:3:4::6:7:8 1:2:3:4::8 1:2:3:4::6:7:1.2.3.4 (?:${o}:){3}(?:(?::${o}){0,2}:${p}|(?::${o}){1,4}|:)| // 1:2:3:: 1:2:3::5:6:7:8 1:2:3::8 1:2:3::5:6:7:1.2.3.4 (?:${o}:){2}(?:(?::${o}){0,3}:${p}|(?::${o}){1,5}|:)| // 1:2:: 1:2::4:5:6:7:8 1:2::8 1:2::4:5:6:7:1.2.3.4 (?:${o}:){1}(?:(?::${o}){0,4}:${p}|(?::${o}){1,6}|:)| // 1:: 1::3:4:5:6:7:8 1::8 1::3:4:5:6:7:1.2.3.4 (?::(?:(?::${o}){0,5}:${p}|(?::${o}){1,7}|:)) // ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::1.2.3.4 )(?:%[0-9a-zA-Z]{1,})? // %eth0 %1 `.replace(/\s*\/\/.*$/gm,"").replace(/\n/g,"").trim(),U=new RegExp(`(?:^${p}$)|(?:^${g}$)`),M=new RegExp(`^${p}$`),F=new RegExp(`^${g}$`),y=e=>e&&e.exact?U:new RegExp(`(?:${l(e)}${p}${l(e)})|(?:${l(e)}${g}${l(e)})`,"g");y.v4=e=>e&&e.exact?M:new RegExp(`${l(e)}${p}${l(e)}`,"g");y.v6=e=>e&&e.exact?F:new RegExp(`${l(e)}${g}${l(e)}`,"g");var T=y;function v(e,{timeout:t}={}){let s=new B__default.default.Script("returnValue = function_()"),i=(...r)=>{let n={function_:()=>e(...r)};return s.runInNewContext(n,{timeout:t}),n.returnValue};return Object.defineProperty(i,"name",{value:`functionTimeout(${e.name||"<anonymous>"})`,configurable:true}),i}function E(e){return (e==null?void 0:e.code)==="ERR_SCRIPT_EXECUTION_TIMEOUT"}var{toString:q}=Object.prototype;function x(e){return q.call(e)==="[object RegExp]"}var I={global:"g",ignoreCase:"i",multiline:"m",dotAll:"s",sticky:"y",unicode:"u"};function b(e,t={}){if(!x(e))throw new TypeError("Expected a RegExp instance");let s=Object.keys(I).map(r=>(typeof t[r]=="boolean"?t[r]:e[r])?I[r]:"").join(""),i=new RegExp(t.source||e.source,s);return i.lastIndex=typeof t.lastIndex=="number"?t.lastIndex:e.lastIndex,i}function R(e,t,{timeout:s}={}){try{return v(()=>b(e).test(t),{timeout:s})()}catch(i){if(E(i))return false;throw i}}var k=45,H={timeout:400};function P(e){return e.length>k?false:R(T({exact:true}),e,H)}var S={version:"1.7.1"};var C="ZmVay1EQVFOaZhwQ4Kv81ypLAZNczV9sG4KkseXWn1NEk6cXmPKO/MCa9sryslvLCFMnNe4Z4CPXzToowvhHvA==";async function K(e,t,s,i={}){let r=`${t}/${e}`;try{let n=await s.request(r,{...i}),c=n.status>=200&&n.status<300;if(!c)return [new Error(`Request failed with status ${n.status} (${n.url})`),void 0,n];let[u,d]=await radash.tryit(JSON.parse)(n.data.toString()),m=u?n.data.toString():d;return [void 0,m,{ok:c,...n,data:m}]}catch(n){return [n,void 0,void 0]}}function _(e){var s;let t=new urllib.HttpClient({connect:{rejectUnauthorized:((s=e==null?void 0:e.options)==null?void 0:s.rejectUnauthorized)??false,timeout:1e3},defaultArgs:{digestAuth:`${e.user}:${e.password}`,contentType:"json",headers:{}}});return {client:t,request:(i,r={})=>K(i,e.baseUrl,t,r)}}function h(){return new urllib.HttpClient({connect:{rejectUnauthorized:false,timeout:1e3},defaultArgs:{contentType:"json"}})}function j(e){return {app_id:"gapp.id",id:e,device_name:"MyName",device_os:"Android",app_name:"PhilTV-js APP",type:"native"}}function O(e,t){let s=crypto.createHmac("sha1",e);return s.write(t),s.end(),s.read("binary").toString("base64")}async function J(e){let t=h();try{let{data:s}=await t.request(`${e}/system`,{dataType:"json",timeout:2e3,signal:AbortSignal.timeout(2e3)}),{api_version:{Major:i},featuring:{systemfeatures:r}}=s,n=r.secured_transport==="true",c=r.pairing_type==="digest_auth_pairing";return [void 0,{apiVersion:i,systemFeatures:r,isSecureTransport:n,isGoodPairingType:c,isReady:n&&c}]}catch(s){return [s,void 0]}}var f=class{tvBase;deviceId;apiUrls;deviceInformation;httpClients;startPairingResponse;credentials;apiVersion;pairingRequestUrl;constructor(t){this.tvBase=t,this.apiUrls={secure:`https://${this.tvBase.tvIp}:${this.tvBase.apiPort||1926}`},this.deviceId=crypto.randomBytes(16).toString("hex"),this.deviceInformation=j(this.deviceId);}async init(){let[t,s]=await J(this.apiUrls.secure);return t||(this.apiVersion=s==null?void 0:s.apiVersion,this.pairingRequestUrl=`${this.apiUrls.secure}/${this.apiVersion}/pair/request`),[t,s]}async startPairing(){let t=h(),{data:s}=await t.request(this.pairingRequestUrl,{method:"POST",data:{device:this.deviceInformation,scope:["read","write","control"]},dataType:"json"}),{timestamp:i,auth_key:r,timeout:n,error_id:c,error_text:u}=s;return c!=="SUCCESS"?[new Error(`Failed to start pairing: ${u}`),void 0]:(this.startPairingResponse={authTimestamp:i,authKey:r,timeout:n},this.credentials={user:this.deviceId,password:r},this.httpClients={digest:_({user:this.credentials.user,password:this.credentials.password,baseUrl:`${this.apiUrls.secure}/${this.apiVersion}`})},[void 0,this.startPairingResponse])}async completePairing(t){var s;if(!t)return [new Error("Failed to complete pairing"),void 0];if(this.startPairingResponse){let i=Buffer.from(C,"base64"),r=((s=this.startPairingResponse)==null?void 0:s.authTimestamp)+t,n=O(i,r),c={device:this.deviceInformation,auth:{auth_AppId:"1",pin:t,auth_timestamp:this.startPairingResponse.authTimestamp,auth_signature:n}};if(!this.httpClients.digest)return [new Error("No digest client"),void 0];let[u,d]=await this.httpClients.digest.request("pair/grant",{method:"POST",data:c,retry:0});return u?[u,void 0]:(d==null?void 0:d.error_id)!=="SUCCESS"?[new Error(`Failed to complete pairing: ${d==null?void 0:d.error_text}`),void 0]:[void 0,{user:this.credentials.user,password:this.credentials.password,apiUrl:this.apiUrls.secure,apiVersion:this.apiVersion,fullApiUrl:`${this.apiUrls.secure}/${this.apiVersion}`}]}return [new Error("Failed to complete pairing"),void 0]}};var X=["SIGINT","SIGTERM","uncaughtException"];for(let e of X)process.on(e,()=>{consola.consola.info("OK. Bye.");});var D=process.argv[2];async function Z(){consola.consola.box(` philtv-js Pairing TV ${S.version} `.trim());let e;D?e=D.trim():e=await(async()=>await consola.consola.prompt("Enter TV ip address:",{type:"text"}))(),P(e)||(consola.consola.error(`'${e}' is an invalid IP address. Bye.`),process.exit(1));let t=new f({tvIp:e});consola.consola.info(`Starting pairing with TV at ${t.apiUrls.secure}`),consola.consola.start("Trying to contact TV...");let[s,i]=await t.init();(s||i&&!i.isReady)&&(consola.consola.error(` Check if the TV is on and the IP is correct. Only Philips TVs with API version 6 or higher are supported. Only secure transport and digest auth pairing are supported (https). ${s?s==null?void 0:s.message:""} Bye. `),process.exit(1)),consola.consola.success("TV is ready with API version",i==null?void 0:i.apiVersion),consola.consola.start(`Trying to pair TV at ${t.pairingRequestUrl}...`);let[r]=await t.startPairing();r&&(consola.consola.error(` Failed to start pairing. ${r==null?void 0:r.message} Bye. `),process.exit(1));let c=await(async()=>await consola.consola.prompt("Enter pin code from TV:",{type:"text"}))(),[u,d]=await t.completePairing(c);u?consola.consola.error(`${u.message} Bye.`):(consola.consola.success("Pairing successful"),consola.consola.box(JSON.stringify(d,null,2)));}Z();