Merge pull request #16 from JurajNyiri/support_shared_servers

Support shared servers
pull/30/head 1.3
Juraj Nyíri 3 years ago committed by GitHub
commit b0c208b12b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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 attributes
- Provided entity ID needs to have attribute adb_response - Provided entity ID needs to have attribute adb_response
**Supported play types**: **Supported**:
✅ Shared Plex servers
✅ Movies ✅ 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 both entities cannot be 'unavailable'
- State of kodi cannot be 'off' - State of kodi cannot be 'off'
**Supported play types**: **Supported**:
✅ Shared Plex servers _\*if content available in kodi_
✅ Movies ✅ Movies
@ -204,7 +208,9 @@ Play button is only visible if all the conditions inside Availability section of
- Media player entity cannot be `unavailable` - Media player entity cannot be `unavailable`
**Supported play types**: **Supported**:
✅ Shared Plex servers
✅ Movies ✅ Movies
@ -263,7 +269,9 @@ entity:
- Plex needs to run on the defined device - Plex needs to run on the defined device
**Supported play types**: **Supported**:
✅ Shared Plex servers _\*requires additional configuration, see below_
✅ Movies ✅ Movies
@ -273,6 +281,63 @@ entity:
✅ Episodes ✅ 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 ## 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. 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.

@ -18672,7 +18672,7 @@ class Plex {
constructor(ip, port = false, token, protocol = 'http', sort = 'titleSort:asc') { constructor(ip, port = false, token, protocol = 'http', sort = 'titleSort:asc') {
this.serverInfo = {}; this.serverInfo = {};
this.clients = []; this.clients = [];
this.requestTimeout = 5000; this.requestTimeout = 10000;
this.sections = []; this.sections = [];
this.init = async () => { this.init = async () => {
await this.getClients(); await this.getClients();
@ -18917,13 +18917,13 @@ class PlayController {
await this.hass.callService(this.runAfter[0], this.runAfter[1], {}); await this.hass.callService(this.runAfter[0], this.runAfter[1], {});
} }
}; };
this.plexPlayerCreateQueue = async (movieID) => { this.plexPlayerCreateQueue = async (movieID, plex) => {
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}`; 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({ const plexResponse = await axios({
method: 'post', method: 'post',
url, url,
headers: { headers: {
'X-Plex-Token': this.plex.token, 'X-Plex-Token': plex.token,
'X-Plex-Client-Identifier': 'PlexMeetsHomeAssistant' 'X-Plex-Client-Identifier': 'PlexMeetsHomeAssistant'
} }
}); });
@ -18935,10 +18935,27 @@ class PlayController {
playQueueSelectedMetadataItemID: plexResponse.data.MediaContainer.playQueueSelectedMetadataItemID playQueueSelectedMetadataItemID: plexResponse.data.MediaContainer.playQueueSelectedMetadataItemID
}; };
}; };
this.playViaPlexPlayer = async (entityName, movieID) => { this.playViaPlexPlayer = async (entity, movieID) => {
const machineID = this.getPlexPlayerMachineIdentifier(entityName); const machineID = this.getPlexPlayerMachineIdentifier(entity);
const { playQueueID, playQueueSelectedMetadataItemID } = await this.plexPlayerCreateQueue(movieID); let { plex } = this;
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}`; 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 { try {
const plexResponse = await axios({ const plexResponse = await axios({
method: 'post', method: 'post',
@ -19061,9 +19078,58 @@ class PlayController {
}); });
return service; 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 = ''; 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) || if (lodash.isEqual(plexClient.machineIdentifier, entityName) ||
lodash.isEqual(plexClient.product, entityName) || lodash.isEqual(plexClient.product, entityName) ||
lodash.isEqual(plexClient.name, entityName) || lodash.isEqual(plexClient.name, entityName) ||
@ -19078,9 +19144,9 @@ class PlayController {
this.isPlaySupported = (data) => { this.isPlaySupported = (data) => {
return !lodash.isEmpty(this.getPlayService(data)); return !lodash.isEmpty(this.getPlayService(data));
}; };
this.isPlexPlayerSupported = (entityName) => { this.isPlexPlayerSupported = (entity) => {
let found = false; let found = false;
if (this.getPlexPlayerMachineIdentifier(entityName)) { if (this.getPlexPlayerMachineIdentifier(entity)) {
found = true; found = true;
} }
return found || !lodash.isEqual(this.runBefore, false); return found || !lodash.isEqual(this.runBefore, false);
@ -19205,6 +19271,47 @@ const findTrailerURL = (movieData) => {
} }
return foundURL; 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 createEpisodesView = (playController, plex, data) => {
const episodeContainer = document.createElement('div'); const episodeContainer = document.createElement('div');
episodeContainer.className = 'episodeContainer'; episodeContainer.className = 'episodeContainer';
@ -20100,6 +20207,13 @@ class PlexMeetsHomeAssistant extends HTMLElement {
this.renderPage(); this.renderPage();
try { try {
if (this.plex) { 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(); await this.plex.init();
try { try {
const onDeck = await this.plex.getOnDeck(); const onDeck = await this.plex.getOnDeck();
@ -21140,11 +21254,14 @@ class PlexMeetsHomeAssistant extends HTMLElement {
interactiveArea.append(playButton); interactiveArea.append(playButton);
} }
movieElem.append(interactiveArea); movieElem.append(interactiveArea);
playButton.addEventListener('click', event => { clickHandler(playButton, (event) => {
event.stopPropagation(); event.stopPropagation();
if (this.hassObj && this.playController) { if (this.hassObj && this.playController) {
this.playController.play(data, true); this.playController.play(data, true);
} }
}, (event) => {
console.log('Play version... will be here!');
event.stopPropagation();
}); });
const titleElem = document.createElement('div'); const titleElem = document.createElement('div');
if (lodash.isEqual(data.type, 'episode')) { if (lodash.isEqual(data.type, 'episode')) {
@ -21240,9 +21357,6 @@ class PlexMeetsHomeAssistant extends HTMLElement {
} }
set hass(hass) { set hass(hass) {
this.hassObj = 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) { if (!this.content) {
this.error = ''; this.error = '';
if (!this.loading) { if (!this.loading) {

@ -121,19 +121,18 @@ class PlayController {
} }
}; };
private plexPlayerCreateQueue = async (movieID: number): Promise<Record<string, number>> => { private plexPlayerCreateQueue = async (movieID: number, plex: Plex): Promise<Record<string, number>> => {
const url = `${this.plex.protocol}://${this.plex.ip}:${ 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}`;
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}`;
const plexResponse = await axios({ const plexResponse = await axios({
method: 'post', method: 'post',
url, url,
headers: { headers: {
'X-Plex-Token': this.plex.token, 'X-Plex-Token': plex.token,
'X-Plex-Client-Identifier': 'PlexMeetsHomeAssistant' 'X-Plex-Client-Identifier': 'PlexMeetsHomeAssistant'
} }
}); });
if (plexResponse.status !== 200) { if (plexResponse.status !== 200) {
throw Error('Error reaching Plex to generate queue'); throw Error('Error reaching Plex to generate queue');
} }
@ -143,15 +142,30 @@ class PlayController {
}; };
}; };
private playViaPlexPlayer = async (entityName: string, movieID: number): Promise<void> => { private playViaPlexPlayer = async (entity: string | Record<string, any>, movieID: number): Promise<void> => {
const machineID = this.getPlexPlayerMachineIdentifier(entityName); const machineID = this.getPlexPlayerMachineIdentifier(entity);
const { playQueueID, playQueueSelectedMetadataItemID } = await this.plexPlayerCreateQueue(movieID); 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 { try {
const plexResponse = await axios({ const plexResponse = await axios({
method: 'post', method: 'post',
@ -284,9 +298,71 @@ class PlayController {
return service; return service;
}; };
private getPlexPlayerMachineIdentifier = (entityName: string): string => { init = async (): Promise<void> => {
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, any>): string => {
let machineIdentifier = ''; 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 ( if (
_.isEqual(plexClient.machineIdentifier, entityName) || _.isEqual(plexClient.machineIdentifier, entityName) ||
_.isEqual(plexClient.product, entityName) || _.isEqual(plexClient.product, entityName) ||
@ -305,11 +381,12 @@ class PlayController {
return !_.isEmpty(this.getPlayService(data)); return !_.isEmpty(this.getPlayService(data));
}; };
private isPlexPlayerSupported = (entityName: string): boolean => { private isPlexPlayerSupported = (entity: string | Record<string, any>): boolean => {
let found = false; let found = false;
if (this.getPlexPlayerMachineIdentifier(entityName)) { if (this.getPlexPlayerMachineIdentifier(entity)) {
found = true; found = true;
} }
return found || !_.isEqual(this.runBefore, false); return found || !_.isEqual(this.runBefore, false);
}; };

@ -15,7 +15,7 @@ class Plex {
clients: Array<Record<string, any>> = []; clients: Array<Record<string, any>> = [];
requestTimeout = 5000; requestTimeout = 10000;
sort: string; sort: string;

@ -104,6 +104,54 @@ const findTrailerURL = (movieData: Record<string, any>): string => {
} }
return foundURL; 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<string, any>): HTMLElement => { const createEpisodesView = (playController: PlayController, plex: Plex, data: Record<string, any>): HTMLElement => {
const episodeContainer = document.createElement('div'); const episodeContainer = document.createElement('div');
@ -206,5 +254,6 @@ export {
hasEpisodes, hasEpisodes,
getOldPlexServerErrorMessage, getOldPlexServerErrorMessage,
getWidth, getWidth,
getDetailsBottom getDetailsBottom,
clickHandler
}; };

@ -15,7 +15,8 @@ import {
isVideoFullScreen, isVideoFullScreen,
hasEpisodes, hasEpisodes,
getOldPlexServerErrorMessage, getOldPlexServerErrorMessage,
getDetailsBottom getDetailsBottom,
clickHandler
} from './modules/utils'; } from './modules/utils';
import style from './modules/style'; import style from './modules/style';
@ -116,15 +117,6 @@ class PlexMeetsHomeAssistant extends HTMLElement {
set hass(hass: HomeAssistant) { set hass(hass: HomeAssistant) {
this.hassObj = 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) { if (!this.content) {
this.error = ''; this.error = '';
@ -210,6 +202,7 @@ class PlexMeetsHomeAssistant extends HTMLElement {
} }
this.renderNewElementsIfNeeded(); this.renderNewElementsIfNeeded();
}); });
if (this.card) { if (this.card) {
this.previousPageWidth = this.card.offsetWidth; this.previousPageWidth = this.card.offsetWidth;
} }
@ -218,6 +211,19 @@ class PlexMeetsHomeAssistant extends HTMLElement {
this.renderPage(); this.renderPage();
try { try {
if (this.plex) { if (this.plex) {
if (this.hassObj) {
const entityConfig: Record<string, any> = 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(); await this.plex.init();
try { try {
@ -1362,13 +1368,20 @@ class PlexMeetsHomeAssistant extends HTMLElement {
movieElem.append(interactiveArea); movieElem.append(interactiveArea);
playButton.addEventListener('click', event => { clickHandler(
playButton,
(event: any): void => {
event.stopPropagation(); event.stopPropagation();
if (this.hassObj && this.playController) { if (this.hassObj && this.playController) {
this.playController.play(data, true); this.playController.play(data, true);
} }
}); },
(event: any): void => {
console.log('Play version... will be here!');
event.stopPropagation();
}
);
const titleElem = document.createElement('div'); const titleElem = document.createElement('div');
if (_.isEqual(data.type, 'episode')) { if (_.isEqual(data.type, 'episode')) {

Loading…
Cancel
Save