philtv-js
Version:
A simple library to pair and interact with Philips TV
1 lines • 9.57 kB
JavaScript
import{JS_SECRET_KEY as e,ambilightBrightnessChoices as t,ambilightFollowAudioModeEnum as n,ambilightFollowVideoModeEnum as r,ambilightModes as i,inputKeys as a}from"./constants.mjs";import{HttpClient as o}from"urllib";import{z as s}from"zod";import{omit as c}from"radash";import{createHmac as l,randomBytes as u}from"node:crypto";async function d(e,t,n,r={}){try{let i=`${t}/${e}`,a=await n.request(i,{...r});if(!(a.status>=200&&a.status<300))return{success:!1,error:Error(`Request failed with status ${a.status} (${a.url})`)};let o=a.data.toString();try{return{success:!0,data:JSON.parse(o)}}catch{return{success:!0,data:o}}}catch(e){return{success:!1,error:e instanceof Error?e:Error(String(e))}}}function f(e){let t=new o({connect:{rejectUnauthorized:e?.options?.rejectUnauthorized??!1,timeout:1e3},defaultArgs:{digestAuth:`${e.user}:${e.password}`,contentType:`json`,headers:{}}});return{client:t,request:(n,r={})=>d(n,e.baseUrl,t,r)}}function p(){return new o({connect:{rejectUnauthorized:!1,timeout:1e3},defaultArgs:{contentType:`json`}})}const m=s.object({apiUrl:s.url(),user:s.string().min(1),password:s.string().min(1)}),h=s.enum(a),g=s.enum(r),_=s.enum(n),v=s.literal(t),y=s.enum(i);function b(e){async function t(){let t=await e.getBrightnessValue(),n=Math.min(10,t+1);await e.setBrightness(n)}async function n(){let t=await e.getBrightnessValue(),n=Math.max(0,t-1);await e.setBrightness(n)}async function r(){let t=await e.getBrightnessInformation();return Number(t.data?.value??0)}async function i(e){let t=await e.getMenuStructureItem(`ambilight_brightness`);return(await e.getCurrentSetting(t.node_id))[0]?.value}async function a(e,t){let n=v.parse(t),r=await e.getMenuStructureItem(`ambilight_brightness`);return e.setMenuItemSetting(r,Number(n))}return{increase:t,decrease:n,getBrightnessValue:r,getBrightnessInformation:i,setBrightness:a}}async function x(e){let[t,n,r,i,a]=await Promise.all([e.getConfiguration(),e.getBrightnessInformation(),e.getMode(),e.getCachedState(),e.getAmbiHue()]);return{configuration:t??null,mode:r??null,brightness:n??null,currentBrightness:Number(n.data?.value??0),cached:i??null,ambiHue:a??null}}function S(e){async function t(){let t=await e.digestClient.request(`ambilight/mode`);if(!t.success)throw t.error;return t.data}async function n(t){let n=y.parse(t);await e.digestClient.request(`ambilight/mode`,{method:`POST`,data:{current:n}})}return{getMode:t,setMode:n}}var C=class{digestClient;menuApi;constructor(e,t){this.digestClient=e,this.menuApi=t}async setBrightness(e){return b(this).setBrightness(this.menuApi,e)}async getBrightnessInformation(){return b(this).getBrightnessInformation(this.menuApi)}async getBrightnessValue(){return b(this).getBrightnessValue()}async increaseBrightness(){await b(this).increase()}async decreaseBrightness(){await b(this).decrease()}async getMode(){return S(this).getMode()}async setMode(e){await S(this).setMode(e)}async getFullInformation(){return x(this)}async setCurrentConfiguration(e){await this.digestClient.request(`ambilight/currentconfiguration`,{method:`POST`,data:{styleName:e.styleName,isExpert:!1,menuSetting:e.menuSetting}})}async setFollowVideoMode(e){let t=g.parse(e);await this.setCurrentConfiguration({styleName:`FOLLOW_VIDEO`,isExpert:!1,menuSetting:t})}async setFollowAudioMode(e){let t=_.parse(e);await this.setCurrentConfiguration({styleName:`FOLLOW_AUDIO`,isExpert:!1,menuSetting:t})}async turnOff(){await this.setCurrentConfiguration({styleName:`OFF`,isExpert:!1})}async setStaticColor(e,t){let n=e.replace(`#`,``),r=Number.parseInt(n.substring(0,2),16),i=Number.parseInt(n.substring(2,4),16),a=Number.parseInt(n.substring(4,6),16);await this.setMode(`manual`),t?.brightness&&await this.setBrightness(t.brightness),await this.setCachedColor({r,g:i,b:a})}async setCachedColor(e){let t=await this.digestClient.request(`ambilight/cached`,{method:`POST`,data:e});if(!t.success)throw t.error}async getCachedState(){let e=await this.digestClient.request(`ambilight/cached`);if(!e.success)throw e.error;return e.data}async getAmbiHue(){let e=await this.digestClient.request(`HueLamp/power`);if(!e.success)throw e.error;return e.data}async getConfiguration(){let e=await this.digestClient.request(`ambilight/currentconfiguration`);if(!e.success)throw e.error;return e.data}},w=class{digestClient;constructor(e){this.digestClient=e}async sendKey(e){let t=h.parse(e),n=await this.digestClient.request(`input/key`,{method:`POST`,data:{key:t}});if(!n.success)throw n.error}};function T(e){let t=[];function n(e){let{node_id:r,type:i,string_id:a,context:o,data:s}=e;if(t.push({node_id:r,type:i,string_id:a,context:o,data:c(s,[`nodes`])}),s?.nodes)for(let e of s.nodes)n(e)}return n(e),t}function E(e,t){switch(e){case`SLIDER_NODE`:return{value:Number(t)};case`TOGGLE_NODE`:return{boolean_value:t?`true`:`false`};case`LIST_NODE`:return typeof t==`string`?{string_id:t}:{enum_id:Number(t)};default:return t}}var D=class{digestClient;constructor(e){this.digestClient=e}async getMenuStructure(e={flat:!1}){let t=await this.digestClient.request(`menuitems/settings/structure`);if(!t.success)throw t.error;return e.flat?T(t.data.node):t.data}async getMenuStructureItem(e,t){let n=(await this.getMenuStructure({flat:!0})).find(n=>e?n.context===e:n.node_id===t);if(!n)throw Error(`Menu Item not found`);return n}async setMenuItemSetting(e,t){if(!e)throw Error(`Item not found`);let n={values:[{value:{Nodeid:e.node_id,Controllable:`true`,Available:`true`,data:E(e.type,t)}}]},r=await this.digestClient.request(`menuitems/settings/update`,{method:`POST`,data:n});if(!r.success)throw r.error}async getCurrentSetting(e){let t=await this.digestClient.request(`menuitems/settings/current`,{method:`POST`,data:{nodes:[{nodeid:e}]}});if(!t.success)throw Error(`getCurrentSetting`);return t.data.values}},O=class{digestClient;constructor(e){this.digestClient=e}async getSystem(){let e=await this.digestClient.request(`system`);if(!e.success)throw e.error;return{...e.data,fullApiVersion:`${e.data.api_version.Major}.${e.data.api_version.Minor}.${e.data.api_version.Patch}`}}async getCurrentActivity(){let e=await this.digestClient.request(`activities/current`);if(!e.success)throw e.error;return e.data}async getApplications(){let e=await this.digestClient.request(`applications`);if(!e.success)throw e.error;return e.data}},k=class{digestClient;menu;system;input;ambilight;constructor(e){let{user:t,apiUrl:n,password:r}=m.parse(e);this.digestClient=f({user:t,password:r,baseUrl:n}),this.menu=new D(this.digestClient),this.system=new O(this.digestClient),this.input=new w(this.digestClient),this.ambilight=new C(this.digestClient,this.menu)}};function A(e){return{app_id:`gapp.id`,id:e,device_name:`MyName`,device_os:`Android`,app_name:`PhilTV-js APP`,type:`native`}}function j(e,t){let n=l(`sha1`,e);return n.write(t),n.end(),n.read(`binary`).toString(`base64`)}async function M(e){let t=p();try{let{data:n}=await t.request(`${e}/system`,{dataType:`json`,timeout:2e3,signal:AbortSignal.timeout(2e3)}),r=n,i=r.api_version.Major,a=r.featuring.systemfeatures,o=a.secured_transport===`true`,s=a.pairing_type===`digest_auth_pairing`;return{success:!0,data:{apiVersion:i,systemFeatures:a,isSecureTransport:o,isGoodPairingType:s,isReady:o&&s}}}catch(e){return{success:!1,error:e instanceof Error?e:Error(String(e))}}}var N=class{tvBase;deviceId;apiUrls;deviceInformation;httpClients;startPairingResponse;credentials;apiVersion;pairingRequestUrl;constructor(e){this.tvBase=e,this.apiUrls={secure:`https://${this.tvBase.tvIp}:${this.tvBase.apiPort||1926}`},this.deviceId=u(16).toString(`hex`),this.deviceInformation=A(this.deviceId)}async init(){let e=await M(this.apiUrls.secure);return e.success?e?.data?.isReady?(this.apiVersion=e.data.apiVersion,this.pairingRequestUrl=`${this.apiUrls.secure}/${this.apiVersion}/pair/request`,{success:!0,data:e.data}):{success:!1,error:Error(`TV is not ready`)}:{success:!1,error:e.error}}async startPairing(){try{let{data:e}=await p().request(this.pairingRequestUrl,{method:`POST`,data:{device:this.deviceInformation,scope:[`read`,`write`,`control`]},dataType:`json`}),t=e,n=t.error_id,r=t.error_text;if(n!==`SUCCESS`)return{success:!1,error:Error(`Failed to start pairing: ${r}`)};let i=t.timestamp,a=t.auth_key,o=t.timeout;return this.startPairingResponse={authTimestamp:i,authKey:a,timeout:o},this.credentials={user:this.deviceId,password:a},this.httpClients={digest:f({user:this.credentials.user,password:this.credentials.password,baseUrl:`${this.apiUrls.secure}/${this.apiVersion}`})},{success:!0,data:this.startPairingResponse}}catch(e){return{success:!1,error:e instanceof Error?e:Error(String(e))}}}async completePairing(t){if(!t)return{success:!1,error:Error(`Failed to complete pairing`)};if(this.startPairingResponse){let n=j(Buffer.from(e,`base64`),this.startPairingResponse.authTimestamp.toString()+t),r={device:this.deviceInformation,auth:{auth_AppId:`1`,pin:t,auth_timestamp:this.startPairingResponse.authTimestamp,auth_signature:n}};if(!this.httpClients.digest)return{success:!1,error:Error(`No digest client`)};let i=await this.httpClients.digest.request(`pair/grant`,{method:`POST`,data:r,retry:0});if(!i.success)return i;let a=i.data;return a?.error_id===`SUCCESS`?{success:!0,data:{user:this.credentials.user,password:this.credentials.password,apiUrl:this.apiUrls.secure,apiVersion:this.apiVersion,fullApiUrl:`${this.apiUrls.secure}/${this.apiVersion}`}}:{success:!1,error:Error(`Failed to complete pairing: ${a?.error_text}`)}}return{success:!1,error:Error(`Failed to complete pairing`)}}};export{k as PhilTVApi,N as PhilTVPairing};