diff --git a/README.md b/README.md index 29ad131..30a85c4 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,9 @@ Play button is only visible if all the conditions inside Availability section of - Provided entity ID needs to have attributes - Provided entity ID needs to have attribute adb_response -**Supported play types**: +**Supported**: + +✅ Shared Plex servers ✅ Movies @@ -180,7 +182,9 @@ Play button is only visible if all the conditions inside Availability section of - State of both entities cannot be 'unavailable' - State of kodi cannot be 'off' -**Supported play types**: +**Supported**: + +✅ Shared Plex servers _\*if content available in kodi_ ✅ Movies @@ -204,7 +208,9 @@ Play button is only visible if all the conditions inside Availability section of - Media player entity cannot be `unavailable` -**Supported play types**: +**Supported**: + +✅ Shared Plex servers ✅ Movies @@ -263,7 +269,9 @@ entity: - Plex needs to run on the defined device -**Supported play types**: +**Supported**: + +✅ Shared Plex servers _\*requires additional configuration, see below_ ✅ Movies @@ -273,6 +281,63 @@ entity: ✅ Episodes +**Shared Plex servers configuration** + +plexPlayer can be configured in multiple ways, achieving the same thing: + +``` +entity: + plexPlayer: TV 2020 +``` + +``` +entity: + plexPlayer: + - TV 2020 +``` + +``` +entity: + plexPlayer: + identifier: TV 2020 +``` + +``` +entity: + plexPlayer: + - identifier: TV 2020 +``` + +As can be seen from the last two examples, it is possible to configure it as an object having key "identifier". + +That is useful, if you want to stream media from shared or remote Plex server. Add information about your local Plex server which sees your device on which you wish to play content. This is done by including a new key, "server" having additional keys: + +Example 1: + +``` +entity: + plexPlayer: + - identifier: TV 2020 + server: + ip: 192.168.13.37 # Mandatory + token: QWdsqEXAMPLETOKENqwerty # Mandatory + port: 32400 + protocol: http +``` + +Example 2: + +``` +entity: + plexPlayer: + identifier: TV 2020 + server: + ip: 192.168.13.37 # Mandatory + token: QWdsqEXAMPLETOKENqwerty # Mandatory + port: 32400 + protocol: http +``` + ## Sorting You can use _:desc_ or _:asc_ after every value to change the order from ascending to descending. For example, titlesort would become titleSort:asc, or titleSort:desc. diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index eebd9ca..3843c15 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -18672,7 +18672,7 @@ class Plex { constructor(ip, port = false, token, protocol = 'http', sort = 'titleSort:asc') { this.serverInfo = {}; this.clients = []; - this.requestTimeout = 5000; + this.requestTimeout = 10000; this.sections = []; this.init = async () => { await this.getClients(); @@ -18917,13 +18917,13 @@ class PlayController { await this.hass.callService(this.runAfter[0], this.runAfter[1], {}); } }; - this.plexPlayerCreateQueue = async (movieID) => { - const url = `${this.plex.protocol}://${this.plex.ip}:${this.plex.port}/playQueues?type=video&shuffle=0&repeat=0&continuous=1&own=1&uri=server://${await this.plex.getServerID()}/com.plexapp.plugins.library/library/metadata/${movieID}`; + this.plexPlayerCreateQueue = async (movieID, plex) => { + const url = `${plex.getBasicURL()}/playQueues?type=video&shuffle=0&repeat=0&continuous=1&own=1&uri=server://${await plex.getServerID()}/com.plexapp.plugins.library/library/metadata/${movieID}`; const plexResponse = await axios({ method: 'post', url, headers: { - 'X-Plex-Token': this.plex.token, + 'X-Plex-Token': plex.token, 'X-Plex-Client-Identifier': 'PlexMeetsHomeAssistant' } }); @@ -18935,10 +18935,27 @@ class PlayController { playQueueSelectedMetadataItemID: plexResponse.data.MediaContainer.playQueueSelectedMetadataItemID }; }; - this.playViaPlexPlayer = async (entityName, movieID) => { - const machineID = this.getPlexPlayerMachineIdentifier(entityName); - const { playQueueID, playQueueSelectedMetadataItemID } = await this.plexPlayerCreateQueue(movieID); - const url = `${this.plex.protocol}://${this.plex.ip}:${this.plex.port}/player/playback/playMedia?address=${this.plex.ip}&commandID=1&containerKey=/playQueues/${playQueueID}?window=100%26own=1&key=/library/metadata/${playQueueSelectedMetadataItemID}&machineIdentifier=${await this.plex.getServerID()}&offset=0&port=${this.plex.port}&token=${this.plex.token}&type=video&protocol=${this.plex.protocol}`; + this.playViaPlexPlayer = async (entity, movieID) => { + const machineID = this.getPlexPlayerMachineIdentifier(entity); + let { plex } = this; + if (lodash.isObject(entity) && !lodash.isNil(entity.plex)) { + plex = entity.plex; + } + const { playQueueID, playQueueSelectedMetadataItemID } = await this.plexPlayerCreateQueue(movieID, this.plex); + let url = plex.getBasicURL(); + url += `/player/playback/playMedia`; + url += `?type=video`; + url += `&commandID=1`; + url += `&providerIdentifier=com.plexapp.plugins.library`; + url += `&containerKey=/playQueues/${playQueueID}`; + url += `&key=/library/metadata/${playQueueSelectedMetadataItemID}`; + url += `&offset=0`; + url += `&machineIdentifier=${await this.plex.getServerID()}`; + url += `&protocol=${this.plex.protocol}`; + url += `&address=${this.plex.ip}`; + url += `&port=${this.plex.port}`; + url += `&token=${this.plex.token}`; + url = plex.authorizeURL(url); try { const plexResponse = await axios({ method: 'post', @@ -19061,9 +19078,58 @@ class PlayController { }); return service; }; - this.getPlexPlayerMachineIdentifier = (entityName) => { + this.init = async () => { + if (!lodash.isNil(this.entity.plexPlayer)) { + if (lodash.isArray(this.entity.plexPlayer)) { + for (let i = 0; i < this.entity.plexPlayer.length; i += 1) { + if (lodash.isObjectLike(this.entity.plexPlayer[i]) && !lodash.isNil(this.entity.plexPlayer[i].server)) { + let port = false; + if (!lodash.isNil(this.entity.plexPlayer[i].server.port)) { + port = this.entity.plexPlayer[i].server.port; + } + let protocol = 'http'; + if (!lodash.isNil(this.entity.plexPlayer[i].server.protocol)) { + protocol = this.entity.plexPlayer[i].server.protocol; + } + // eslint-disable-next-line no-param-reassign + this.entity.plexPlayer[i].plex = new Plex(this.entity.plexPlayer[i].server.ip, port, this.entity.plexPlayer[i].server.token, protocol); + // eslint-disable-next-line no-await-in-loop + await this.entity.plexPlayer[i].plex.getClients(); + } + } + } + else if (!lodash.isNil(this.entity.plexPlayer.server) && + !lodash.isNil(this.entity.plexPlayer.server.ip) && + !lodash.isNil(this.entity.plexPlayer.server.token)) { + let port = false; + if (!lodash.isNil(this.entity.plexPlayer.server.port)) { + port = this.entity.plexPlayer.server.port; + } + let protocol = 'http'; + if (!lodash.isNil(this.entity.plexPlayer.server.protocol)) { + protocol = this.entity.plexPlayer.server.protocol; + } + // eslint-disable-next-line no-param-reassign + this.entity.plexPlayer.plex = new Plex(this.entity.plexPlayer.server.ip, port, this.entity.plexPlayer.server.token, protocol); + // eslint-disable-next-line no-await-in-loop + await this.entity.plexPlayer.plex.getClients(); + } + } + }; + this.getPlexPlayerMachineIdentifier = (entity) => { let machineIdentifier = ''; - lodash.forEach(this.plex.clients, plexClient => { + let { plex } = this; + let entityName = ''; + if (lodash.isString(entity)) { + entityName = entity; + } + else if (lodash.isObjectLike(entity) && !lodash.isNil(entity.identifier)) { + entityName = entity.identifier; + if (!lodash.isNil(entity.plex) && entity.plex) { + plex = entity.plex; + } + } + lodash.forEach(plex.clients, plexClient => { if (lodash.isEqual(plexClient.machineIdentifier, entityName) || lodash.isEqual(plexClient.product, entityName) || lodash.isEqual(plexClient.name, entityName) || @@ -19078,9 +19144,9 @@ class PlayController { this.isPlaySupported = (data) => { return !lodash.isEmpty(this.getPlayService(data)); }; - this.isPlexPlayerSupported = (entityName) => { + this.isPlexPlayerSupported = (entity) => { let found = false; - if (this.getPlexPlayerMachineIdentifier(entityName)) { + if (this.getPlexPlayerMachineIdentifier(entity)) { found = true; } return found || !lodash.isEqual(this.runBefore, false); @@ -19205,6 +19271,47 @@ const findTrailerURL = (movieData) => { } 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'; @@ -20100,6 +20207,13 @@ class PlexMeetsHomeAssistant extends HTMLElement { this.renderPage(); try { if (this.plex) { + if (this.hassObj) { + const entityConfig = JSON.parse(JSON.stringify(this.config.entity)); // todo: find a nicer solution + this.playController = new PlayController(this.hassObj, this.plex, entityConfig, this.runBefore, this.runAfter); + if (this.playController) { + await this.playController.init(); + } + } await this.plex.init(); try { const onDeck = await this.plex.getOnDeck(); @@ -21140,11 +21254,14 @@ class PlexMeetsHomeAssistant extends HTMLElement { interactiveArea.append(playButton); } movieElem.append(interactiveArea); - playButton.addEventListener('click', event => { + clickHandler(playButton, (event) => { event.stopPropagation(); if (this.hassObj && this.playController) { this.playController.play(data, true); } + }, (event) => { + console.log('Play version... will be here!'); + event.stopPropagation(); }); const titleElem = document.createElement('div'); if (lodash.isEqual(data.type, 'episode')) { @@ -21240,9 +21357,6 @@ class PlexMeetsHomeAssistant extends HTMLElement { } set hass(hass) { this.hassObj = hass; - if (this.plex) { - this.playController = new PlayController(this.hassObj, this.plex, this.config.entity, this.runBefore, this.runAfter); - } if (!this.content) { this.error = ''; if (!this.loading) { diff --git a/src/modules/PlayController.ts b/src/modules/PlayController.ts index 2cc78e2..d1b2f97 100644 --- a/src/modules/PlayController.ts +++ b/src/modules/PlayController.ts @@ -121,19 +121,18 @@ class PlayController { } }; - private plexPlayerCreateQueue = async (movieID: number): Promise> => { - const url = `${this.plex.protocol}://${this.plex.ip}:${ - this.plex.port - }/playQueues?type=video&shuffle=0&repeat=0&continuous=1&own=1&uri=server://${await this.plex.getServerID()}/com.plexapp.plugins.library/library/metadata/${movieID}`; + private plexPlayerCreateQueue = async (movieID: number, plex: Plex): Promise> => { + const url = `${plex.getBasicURL()}/playQueues?type=video&shuffle=0&repeat=0&continuous=1&own=1&uri=server://${await plex.getServerID()}/com.plexapp.plugins.library/library/metadata/${movieID}`; const plexResponse = await axios({ method: 'post', url, headers: { - 'X-Plex-Token': this.plex.token, + 'X-Plex-Token': plex.token, 'X-Plex-Client-Identifier': 'PlexMeetsHomeAssistant' } }); + if (plexResponse.status !== 200) { throw Error('Error reaching Plex to generate queue'); } @@ -143,15 +142,30 @@ class PlayController { }; }; - private playViaPlexPlayer = async (entityName: string, movieID: number): Promise => { - const machineID = this.getPlexPlayerMachineIdentifier(entityName); - const { playQueueID, playQueueSelectedMetadataItemID } = await this.plexPlayerCreateQueue(movieID); + private playViaPlexPlayer = async (entity: string | Record, movieID: number): Promise => { + const machineID = this.getPlexPlayerMachineIdentifier(entity); + let { plex } = this; + if (_.isObject(entity) && !_.isNil(entity.plex)) { + plex = entity.plex; + } + const { playQueueID, playQueueSelectedMetadataItemID } = await this.plexPlayerCreateQueue(movieID, this.plex); + + let url = plex.getBasicURL(); + url += `/player/playback/playMedia`; + url += `?type=video`; + url += `&commandID=1`; + url += `&providerIdentifier=com.plexapp.plugins.library`; + url += `&containerKey=/playQueues/${playQueueID}`; + url += `&key=/library/metadata/${playQueueSelectedMetadataItemID}`; + url += `&offset=0`; + url += `&machineIdentifier=${await this.plex.getServerID()}`; + url += `&protocol=${this.plex.protocol}`; + url += `&address=${this.plex.ip}`; + url += `&port=${this.plex.port}`; + url += `&token=${this.plex.token}`; + + url = plex.authorizeURL(url); - const url = `${this.plex.protocol}://${this.plex.ip}:${this.plex.port}/player/playback/playMedia?address=${ - this.plex.ip - }&commandID=1&containerKey=/playQueues/${playQueueID}?window=100%26own=1&key=/library/metadata/${playQueueSelectedMetadataItemID}&machineIdentifier=${await this.plex.getServerID()}&offset=0&port=${ - this.plex.port - }&token=${this.plex.token}&type=video&protocol=${this.plex.protocol}`; try { const plexResponse = await axios({ method: 'post', @@ -284,9 +298,71 @@ class PlayController { return service; }; - private getPlexPlayerMachineIdentifier = (entityName: string): string => { + init = async (): Promise => { + if (!_.isNil(this.entity.plexPlayer)) { + if (_.isArray(this.entity.plexPlayer)) { + for (let i = 0; i < this.entity.plexPlayer.length; i += 1) { + if (_.isObjectLike(this.entity.plexPlayer[i]) && !_.isNil(this.entity.plexPlayer[i].server)) { + let port: number | false = false; + if (!_.isNil(this.entity.plexPlayer[i].server.port)) { + port = this.entity.plexPlayer[i].server.port; + } + let protocol: 'http' | 'https' = 'http'; + if (!_.isNil(this.entity.plexPlayer[i].server.protocol)) { + protocol = this.entity.plexPlayer[i].server.protocol; + } + // eslint-disable-next-line no-param-reassign + this.entity.plexPlayer[i].plex = new Plex( + this.entity.plexPlayer[i].server.ip, + port, + this.entity.plexPlayer[i].server.token, + protocol + ); + // eslint-disable-next-line no-await-in-loop + await this.entity.plexPlayer[i].plex.getClients(); + } + } + } else if ( + !_.isNil(this.entity.plexPlayer.server) && + !_.isNil(this.entity.plexPlayer.server.ip) && + !_.isNil(this.entity.plexPlayer.server.token) + ) { + let port: number | false = false; + if (!_.isNil(this.entity.plexPlayer.server.port)) { + port = this.entity.plexPlayer.server.port; + } + let protocol: 'http' | 'https' = 'http'; + if (!_.isNil(this.entity.plexPlayer.server.protocol)) { + protocol = this.entity.plexPlayer.server.protocol; + } + // eslint-disable-next-line no-param-reassign + this.entity.plexPlayer.plex = new Plex( + this.entity.plexPlayer.server.ip, + port, + this.entity.plexPlayer.server.token, + protocol + ); + // eslint-disable-next-line no-await-in-loop + await this.entity.plexPlayer.plex.getClients(); + } + } + }; + + private getPlexPlayerMachineIdentifier = (entity: string | Record): string => { let machineIdentifier = ''; - _.forEach(this.plex.clients, plexClient => { + + let { plex } = this; + let entityName = ''; + if (_.isString(entity)) { + entityName = entity; + } else if (_.isObjectLike(entity) && !_.isNil(entity.identifier)) { + entityName = entity.identifier; + if (!_.isNil(entity.plex) && entity.plex) { + plex = entity.plex; + } + } + + _.forEach(plex.clients, plexClient => { if ( _.isEqual(plexClient.machineIdentifier, entityName) || _.isEqual(plexClient.product, entityName) || @@ -305,11 +381,12 @@ class PlayController { return !_.isEmpty(this.getPlayService(data)); }; - private isPlexPlayerSupported = (entityName: string): boolean => { + private isPlexPlayerSupported = (entity: string | Record): boolean => { let found = false; - if (this.getPlexPlayerMachineIdentifier(entityName)) { + if (this.getPlexPlayerMachineIdentifier(entity)) { found = true; } + return found || !_.isEqual(this.runBefore, false); }; diff --git a/src/modules/Plex.ts b/src/modules/Plex.ts index 9116f9e..2bdea2f 100644 --- a/src/modules/Plex.ts +++ b/src/modules/Plex.ts @@ -15,7 +15,7 @@ class Plex { clients: Array> = []; - requestTimeout = 5000; + requestTimeout = 10000; sort: string; diff --git a/src/modules/utils.ts b/src/modules/utils.ts index d467e48..a767206 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -104,6 +104,54 @@ const findTrailerURL = (movieData: Record): string => { } return foundURL; }; +const clickHandler = (elem: HTMLButtonElement, clickFunction: Function, holdFunction: Function): void => { + let longpress = false; + let presstimer: any = null; + + const cancel = (e: any): void => { + e.stopPropagation(); + if (presstimer !== null) { + clearTimeout(presstimer); + presstimer = null; + } + }; + + const click = (e: any): boolean => { + e.stopPropagation(); + if (presstimer !== null) { + clearTimeout(presstimer); + presstimer = null; + } + + if (longpress) { + return false; + } + + clickFunction(e); + return true; + }; + + const start = (e: any): void => { + 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: PlayController, plex: Plex, data: Record): HTMLElement => { const episodeContainer = document.createElement('div'); @@ -206,5 +254,6 @@ export { hasEpisodes, getOldPlexServerErrorMessage, getWidth, - getDetailsBottom + getDetailsBottom, + clickHandler }; diff --git a/src/plex-meets-homeassistant.ts b/src/plex-meets-homeassistant.ts index b9a0b0b..bc89eba 100644 --- a/src/plex-meets-homeassistant.ts +++ b/src/plex-meets-homeassistant.ts @@ -15,7 +15,8 @@ import { isVideoFullScreen, hasEpisodes, getOldPlexServerErrorMessage, - getDetailsBottom + getDetailsBottom, + clickHandler } from './modules/utils'; import style from './modules/style'; @@ -116,15 +117,6 @@ class PlexMeetsHomeAssistant extends HTMLElement { set hass(hass: HomeAssistant) { this.hassObj = hass; - if (this.plex) { - this.playController = new PlayController( - this.hassObj, - this.plex, - this.config.entity, - this.runBefore, - this.runAfter - ); - } if (!this.content) { this.error = ''; @@ -210,6 +202,7 @@ class PlexMeetsHomeAssistant extends HTMLElement { } this.renderNewElementsIfNeeded(); }); + if (this.card) { this.previousPageWidth = this.card.offsetWidth; } @@ -218,6 +211,19 @@ class PlexMeetsHomeAssistant extends HTMLElement { this.renderPage(); try { if (this.plex) { + if (this.hassObj) { + const entityConfig: Record = JSON.parse(JSON.stringify(this.config.entity)); // todo: find a nicer solution + this.playController = new PlayController( + this.hassObj, + this.plex, + entityConfig, + this.runBefore, + this.runAfter + ); + if (this.playController) { + await this.playController.init(); + } + } await this.plex.init(); try { @@ -1362,13 +1368,20 @@ class PlexMeetsHomeAssistant extends HTMLElement { movieElem.append(interactiveArea); - playButton.addEventListener('click', event => { - event.stopPropagation(); + clickHandler( + playButton, + (event: any): void => { + event.stopPropagation(); - if (this.hassObj && this.playController) { - this.playController.play(data, true); + if (this.hassObj && this.playController) { + this.playController.play(data, true); + } + }, + (event: any): void => { + console.log('Play version... will be here!'); + event.stopPropagation(); } - }); + ); const titleElem = document.createElement('div'); if (_.isEqual(data.type, 'episode')) {