diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index 77c6c1d..7ef181f 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -18941,17 +18941,231 @@ class Plex { } } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const escapeHtml = (unsafe) => { + if (unsafe) { + return unsafe + .toString() + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + return ''; +}; +const fetchEntityRegistry = (conn) => conn.sendMessagePromise({ + type: 'config/entity_registry/list' +}); +const getHeight = (el) => { + const height = Math.max(el.scrollHeight, el.offsetHeight, el.clientHeight, el.scrollHeight, el.offsetHeight); + return height; +}; +const getOffset = (el) => { + let x = 0; + let y = 0; + while (el && + el.offsetParent && + !lodash.isNaN(el.offsetLeft) && + !lodash.isNaN(el.offsetTop)) { + x += el.offsetLeft - el.scrollLeft; + y += el.offsetTop - el.scrollTop; + const tmp = el.offsetParent; + if (tmp) { + // eslint-disable-next-line no-param-reassign + el = tmp; + } + } + return { top: y, left: x }; +}; +const getDetailsBottom = (seasonContainers, episodeContainers, activeElem) => { + const lastSeasonContainer = seasonContainers[seasonContainers.length - 1]; + const lastEpisodeContainer = episodeContainers[episodeContainers.length - 1]; + let detailBottom = false; + if (seasonContainers.length > 0 && parseInt(activeElem.style.top, 10) > 0) { + detailBottom = getHeight(lastSeasonContainer) + parseInt(getOffset(lastSeasonContainer).top, 10) + 10; + } + else if (episodeContainers.length > 0) { + detailBottom = getHeight(lastEpisodeContainer) + parseInt(getOffset(lastEpisodeContainer).top, 10) + 10; + } + return detailBottom; +}; +const hasEpisodes = (media) => { + let result = false; + // eslint-disable-next-line consistent-return + lodash.forEach(media, data => { + if (lodash.isEqual(data.type, 'episode')) { + result = true; + return false; + } + }); + return result; +}; +const isVideoFullScreen = (_this) => { + const videoPlayer = _this.getElementsByClassName('videoPlayer')[0]; + const video = videoPlayer.children[0]; + if (!video) + return false; + const body = document.getElementsByTagName('body')[0]; + return ((video.offsetWidth === body.offsetWidth && video.offsetHeight === body.offsetHeight) || + (_this.videoElem && _this.videoElem.classList.contains('simulatedFullScreen'))); +}; +const getOldPlexServerErrorMessage = (libraryName) => { + return `PlexMeetsHomeAssistant: 404 Error requesting library feed for ${libraryName}. Plex API might have changed or using outdated server. Library ${libraryName} will not work.`; +}; +const findTrailerURL = (movieData) => { + let foundURL = ''; + if (movieData.Extras && movieData.Extras.Metadata && movieData.Extras.Metadata.length > 0) { + // eslint-disable-next-line consistent-return + lodash.forEach(movieData.Extras.Metadata, extra => { + if (extra.subtype === 'trailer') { + foundURL = extra.Media[0].Part[0].key; + return false; + } + }); + } + return foundURL; +}; +const clickHandler = (elem, clickFunction, holdFunction) => { + let longpress = false; + let presstimer = null; + const cancel = (e) => { + e.stopPropagation(); + if (presstimer !== null) { + clearTimeout(presstimer); + presstimer = null; + } + }; + const click = (e) => { + e.stopPropagation(); + if (presstimer !== null) { + clearTimeout(presstimer); + presstimer = null; + } + if (longpress) { + return false; + } + clickFunction(e); + return true; + }; + const start = (e) => { + e.stopPropagation(); + if (e.type === 'click' && e.button !== 0) { + return; + } + longpress = false; + presstimer = setTimeout(() => { + holdFunction(e); + longpress = true; + }, 1000); + }; + elem.addEventListener('mousedown', start); + elem.addEventListener('touchstart', start); + elem.addEventListener('click', click); + elem.addEventListener('mouseout', cancel); + elem.addEventListener('touchend', cancel); + elem.addEventListener('touchleave', cancel); + elem.addEventListener('touchcancel', cancel); +}; +const sleep = async (ms) => { + return new Promise(resolve => setTimeout(resolve, ms)); +}; +const getState = async (hass, entityID) => { + return hass.callApi('GET', `states/${entityID}`); +}; +const waitUntilState = async (hass, entityID, state) => { + let entityState = await getState(hass, entityID); + while (entityState.state !== state) { + // eslint-disable-next-line no-await-in-loop + entityState = await getState(hass, entityID); + // eslint-disable-next-line no-await-in-loop + await sleep(1000); + } +}; +const createEpisodesView = (playController, plex, data) => { + const episodeContainer = document.createElement('div'); + episodeContainer.className = 'episodeContainer'; + episodeContainer.style.width = `${CSS_STYLE.episodeWidth}px`; + const episodeThumbURL = plex.authorizeURL(`${plex.getBasicURL()}/photo/:/transcode?width=${CSS_STYLE.episodeWidth}&height=${CSS_STYLE.episodeHeight}&minSize=1&upscale=1&url=${data.thumb}`); + const episodeElem = document.createElement('div'); + episodeElem.className = 'episodeElem'; + episodeElem.style.width = `${CSS_STYLE.episodeWidth}px`; + episodeElem.style.height = `${CSS_STYLE.episodeHeight}px`; + episodeElem.style.backgroundImage = `url('${episodeThumbURL}')`; + episodeElem.dataset.clicked = 'false'; + if (typeof data.lastViewedAt === 'undefined') { + const toViewElem = document.createElement('div'); + toViewElem.className = 'toViewEpisode'; + episodeElem.appendChild(toViewElem); + } + if (playController.isPlaySupported(data)) { + const episodeInteractiveArea = document.createElement('div'); + episodeInteractiveArea.className = 'interactiveArea'; + const episodePlayButton = document.createElement('button'); + episodePlayButton.name = 'playButton'; + episodePlayButton.addEventListener('click', episodeEvent => { + episodeEvent.stopPropagation(); + playController.play(data, true); + }); + episodeInteractiveArea.append(episodePlayButton); + episodeElem.append(episodeInteractiveArea); + } + episodeContainer.append(episodeElem); + const episodeTitleElem = document.createElement('div'); + episodeTitleElem.className = 'episodeTitleElem'; + episodeTitleElem.innerHTML = escapeHtml(data.title); + episodeContainer.append(episodeTitleElem); + const episodeNumber = document.createElement('div'); + episodeNumber.className = 'episodeNumber'; + if (data.type === 'episode') { + episodeNumber.innerHTML = escapeHtml(`Episode ${escapeHtml(data.index)}`); + } + else if (data.type === 'clip') { + let text = ''; + switch (data.subtype) { + case 'behindTheScenes': + text = 'Behind the Scenes'; + break; + case 'trailer': + text = 'Trailer'; + break; + case 'scene': + text = 'Scene'; + break; + case 'sceneOrSample': + text = 'Scene'; + break; + default: + text = data.subtype; + break; + } + episodeNumber.innerHTML = escapeHtml(text); + } + episodeContainer.append(episodeNumber); + episodeContainer.addEventListener('click', episodeEvent => { + episodeEvent.stopPropagation(); + }); + return episodeContainer; +}; +const isScrolledIntoView = (elem) => { + const rect = elem.getBoundingClientRect(); + const elemTop = rect.top; + const elemBottom = rect.bottom; + // Only completely visible elements return true: + const isVisible = elemTop >= 0 && elemBottom <= window.innerHeight; + // Partially visible elements return true: + // isVisible = elemTop < window.innerHeight && elemBottom >= 0; + return isVisible; +}; + class PlayController { constructor(hass, plex, entity, runBefore, runAfter, libraryName) { this.plexPlayerEntity = ''; this.runBefore = false; this.runAfter = false; this.supported = supported; - this.getState = async (entityID) => { - return this.hass.callApi('GET', `states/${entityID}`); - }; this.getKodiSearchResults = async () => { - return JSON.parse((await this.getState('sensor.kodi_media_sensor_search')).attributes.data); + return JSON.parse((await getState(this.hass, 'sensor.kodi_media_sensor_search')).attributes.data); }; this.getKodiSearch = async (search) => { await this.hass.callService('kodi_media_sensors', 'call_method', { @@ -19006,7 +19220,12 @@ class PlayController { }; this.play = async (data, instantPlay = false) => { if (lodash.isArray(this.runBefore)) { + const entityID = `${this.runBefore[0]}.${this.runBefore[1]}`; await this.hass.callService(this.runBefore[0], this.runBefore[1], {}); + const entityState = await getState(this.hass, entityID); + if (lodash.isEqual(entityState.state, 'on')) { + await waitUntilState(this.hass, entityID, 'off'); + } } const entity = this.getPlayService(data); switch (entity.key) { @@ -19358,208 +19577,6 @@ class PlayController { } } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const escapeHtml = (unsafe) => { - if (unsafe) { - return unsafe - .toString() - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } - return ''; -}; -const fetchEntityRegistry = (conn) => conn.sendMessagePromise({ - type: 'config/entity_registry/list' -}); -const getHeight = (el) => { - const height = Math.max(el.scrollHeight, el.offsetHeight, el.clientHeight, el.scrollHeight, el.offsetHeight); - return height; -}; -const getOffset = (el) => { - let x = 0; - let y = 0; - while (el && - el.offsetParent && - !lodash.isNaN(el.offsetLeft) && - !lodash.isNaN(el.offsetTop)) { - x += el.offsetLeft - el.scrollLeft; - y += el.offsetTop - el.scrollTop; - const tmp = el.offsetParent; - if (tmp) { - // eslint-disable-next-line no-param-reassign - el = tmp; - } - } - return { top: y, left: x }; -}; -const getDetailsBottom = (seasonContainers, episodeContainers, activeElem) => { - const lastSeasonContainer = seasonContainers[seasonContainers.length - 1]; - const lastEpisodeContainer = episodeContainers[episodeContainers.length - 1]; - let detailBottom = false; - if (seasonContainers.length > 0 && parseInt(activeElem.style.top, 10) > 0) { - detailBottom = getHeight(lastSeasonContainer) + parseInt(getOffset(lastSeasonContainer).top, 10) + 10; - } - else if (episodeContainers.length > 0) { - detailBottom = getHeight(lastEpisodeContainer) + parseInt(getOffset(lastEpisodeContainer).top, 10) + 10; - } - return detailBottom; -}; -const hasEpisodes = (media) => { - let result = false; - // eslint-disable-next-line consistent-return - lodash.forEach(media, data => { - if (lodash.isEqual(data.type, 'episode')) { - result = true; - return false; - } - }); - return result; -}; -const isVideoFullScreen = (_this) => { - const videoPlayer = _this.getElementsByClassName('videoPlayer')[0]; - const video = videoPlayer.children[0]; - if (!video) - return false; - const body = document.getElementsByTagName('body')[0]; - return ((video.offsetWidth === body.offsetWidth && video.offsetHeight === body.offsetHeight) || - (_this.videoElem && _this.videoElem.classList.contains('simulatedFullScreen'))); -}; -const getOldPlexServerErrorMessage = (libraryName) => { - return `PlexMeetsHomeAssistant: 404 Error requesting library feed for ${libraryName}. Plex API might have changed or using outdated server. Library ${libraryName} will not work.`; -}; -const findTrailerURL = (movieData) => { - let foundURL = ''; - if (movieData.Extras && movieData.Extras.Metadata && movieData.Extras.Metadata.length > 0) { - // eslint-disable-next-line consistent-return - lodash.forEach(movieData.Extras.Metadata, extra => { - if (extra.subtype === 'trailer') { - foundURL = extra.Media[0].Part[0].key; - return false; - } - }); - } - return foundURL; -}; -const clickHandler = (elem, clickFunction, holdFunction) => { - let longpress = false; - let presstimer = null; - const cancel = (e) => { - e.stopPropagation(); - if (presstimer !== null) { - clearTimeout(presstimer); - presstimer = null; - } - }; - const click = (e) => { - e.stopPropagation(); - if (presstimer !== null) { - clearTimeout(presstimer); - presstimer = null; - } - if (longpress) { - return false; - } - clickFunction(e); - return true; - }; - const start = (e) => { - e.stopPropagation(); - if (e.type === 'click' && e.button !== 0) { - return; - } - longpress = false; - presstimer = setTimeout(() => { - holdFunction(e); - longpress = true; - }, 1000); - }; - elem.addEventListener('mousedown', start); - elem.addEventListener('touchstart', start); - elem.addEventListener('click', click); - elem.addEventListener('mouseout', cancel); - elem.addEventListener('touchend', cancel); - elem.addEventListener('touchleave', cancel); - elem.addEventListener('touchcancel', cancel); -}; -const createEpisodesView = (playController, plex, data) => { - const episodeContainer = document.createElement('div'); - episodeContainer.className = 'episodeContainer'; - episodeContainer.style.width = `${CSS_STYLE.episodeWidth}px`; - const episodeThumbURL = plex.authorizeURL(`${plex.getBasicURL()}/photo/:/transcode?width=${CSS_STYLE.episodeWidth}&height=${CSS_STYLE.episodeHeight}&minSize=1&upscale=1&url=${data.thumb}`); - const episodeElem = document.createElement('div'); - episodeElem.className = 'episodeElem'; - episodeElem.style.width = `${CSS_STYLE.episodeWidth}px`; - episodeElem.style.height = `${CSS_STYLE.episodeHeight}px`; - episodeElem.style.backgroundImage = `url('${episodeThumbURL}')`; - episodeElem.dataset.clicked = 'false'; - if (typeof data.lastViewedAt === 'undefined') { - const toViewElem = document.createElement('div'); - toViewElem.className = 'toViewEpisode'; - episodeElem.appendChild(toViewElem); - } - if (playController.isPlaySupported(data)) { - const episodeInteractiveArea = document.createElement('div'); - episodeInteractiveArea.className = 'interactiveArea'; - const episodePlayButton = document.createElement('button'); - episodePlayButton.name = 'playButton'; - episodePlayButton.addEventListener('click', episodeEvent => { - episodeEvent.stopPropagation(); - playController.play(data, true); - }); - episodeInteractiveArea.append(episodePlayButton); - episodeElem.append(episodeInteractiveArea); - } - episodeContainer.append(episodeElem); - const episodeTitleElem = document.createElement('div'); - episodeTitleElem.className = 'episodeTitleElem'; - episodeTitleElem.innerHTML = escapeHtml(data.title); - episodeContainer.append(episodeTitleElem); - const episodeNumber = document.createElement('div'); - episodeNumber.className = 'episodeNumber'; - if (data.type === 'episode') { - episodeNumber.innerHTML = escapeHtml(`Episode ${escapeHtml(data.index)}`); - } - else if (data.type === 'clip') { - let text = ''; - switch (data.subtype) { - case 'behindTheScenes': - text = 'Behind the Scenes'; - break; - case 'trailer': - text = 'Trailer'; - break; - case 'scene': - text = 'Scene'; - break; - case 'sceneOrSample': - text = 'Scene'; - break; - default: - text = data.subtype; - break; - } - episodeNumber.innerHTML = escapeHtml(text); - } - episodeContainer.append(episodeNumber); - episodeContainer.addEventListener('click', episodeEvent => { - episodeEvent.stopPropagation(); - }); - return episodeContainer; -}; -const isScrolledIntoView = (elem) => { - const rect = elem.getBoundingClientRect(); - const elemTop = rect.top; - const elemBottom = rect.bottom; - // Only completely visible elements return true: - const isVisible = elemTop >= 0 && elemBottom <= window.innerHeight; - // Partially visible elements return true: - // isVisible = elemTop < window.innerHeight && elemBottom >= 0; - return isVisible; -}; - /* eslint-disable @typescript-eslint/no-explicit-any */ class PlexMeetsHomeAssistantEditor extends HTMLElement { constructor() { diff --git a/src/modules/PlayController.ts b/src/modules/PlayController.ts index 0e62aa4..f63dc46 100644 --- a/src/modules/PlayController.ts +++ b/src/modules/PlayController.ts @@ -6,6 +6,7 @@ import _ from 'lodash'; import axios from 'axios'; import Plex from './Plex'; import { supported } from '../const'; +import { waitUntilState, getState } from './utils'; class PlayController { entity: Record; @@ -44,12 +45,8 @@ class PlayController { } } - private getState = async (entityID: string): Promise> => { - return this.hass.callApi('GET', `states/${entityID}`); - }; - private getKodiSearchResults = async (): Promise> => { - return JSON.parse((await this.getState('sensor.kodi_media_sensor_search')).attributes.data); + return JSON.parse((await getState(this.hass, 'sensor.kodi_media_sensor_search')).attributes.data); }; private getKodiSearch = async (search: string): Promise> => { @@ -107,7 +104,13 @@ class PlayController { play = async (data: Record, instantPlay = false): Promise => { if (_.isArray(this.runBefore)) { + const entityID = `${this.runBefore[0]}.${this.runBefore[1]}`; await this.hass.callService(this.runBefore[0], this.runBefore[1], {}); + + const entityState = await getState(this.hass, entityID); + if (_.isEqual(entityState.state, 'on')) { + await waitUntilState(this.hass, entityID, 'off'); + } } const entity = this.getPlayService(data); switch (entity.key) { diff --git a/src/modules/utils.ts b/src/modules/utils.ts index 4667865..605ba02 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-env browser */ +import { HomeAssistant } from 'custom-card-helpers'; import { Connection } from 'home-assistant-js-websocket'; import _ from 'lodash'; import { CSS_STYLE } from '../const'; -import PlayController from './PlayController'; import Plex from './Plex'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const escapeHtml = (unsafe: any): string => { @@ -159,7 +159,26 @@ const clickHandler = (elem: HTMLButtonElement, clickFunction: Function, holdFunc elem.addEventListener('touchcancel', cancel); }; -const createEpisodesView = (playController: PlayController, plex: Plex, data: Record): HTMLElement => { +const sleep = async (ms: number): Promise => { + return new Promise(resolve => setTimeout(resolve, ms)); +}; + +const getState = async (hass: HomeAssistant, entityID: string): Promise> => { + return hass.callApi('GET', `states/${entityID}`); +}; + +const waitUntilState = async (hass: HomeAssistant, entityID: string, state: string): Promise => { + let entityState = await getState(hass, entityID); + + while (entityState.state !== state) { + // eslint-disable-next-line no-await-in-loop + entityState = await getState(hass, entityID); + // eslint-disable-next-line no-await-in-loop + await sleep(1000); + } +}; + +const createEpisodesView = (playController: any, plex: Plex, data: Record): HTMLElement => { const episodeContainer = document.createElement('div'); episodeContainer.className = 'episodeContainer'; episodeContainer.style.width = `${CSS_STYLE.episodeWidth}px`; @@ -262,5 +281,7 @@ export { getWidth, getDetailsBottom, clickHandler, - fetchEntityRegistry + fetchEntityRegistry, + waitUntilState, + getState };