Add: Initial support for Kodi, refactor and readme update.

pull/16/head
Juraj Nyíri 4 years ago
parent 1214039249
commit c8a03ce9c3

@ -12,17 +12,39 @@ Custom integration which integrates plex into Home Assistant and makes it possib
- Reload browser, clear cache as usual
- Create a new Home Assistant tab, turn on panel mode
- Add a new card to it:
- Add a new card, see configuration below.
## Configuration
**type**: 'custom:plex-meets-homeassistant'
**token**: Enter your [Plex Token](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/)
**ip**: Enter ip address of plex server. You can also enter hostname without protocol or port.
**port**: Enter port of your plex sever.
**protocol**: _Optional_ Protocol to use for Plex. Defaults to "http".
**maxCount**: _Optional_ Maximum number of items to display in card.
**entity**: You need to configure at least one supported media_player entity.
- **adb**: Entity id of your media_player configured via [Android TV](https://www.home-assistant.io/integrations/androidtv/)
- **kodi**: Entity id of your media_player configured via [Kodi](https://www.home-assistant.io/integrations/kodi/). You also need to install and configure integration [Kodi Recently Added Media](https://github.com/jtbgroup/kodi-media-sensors) and its sensor **kodi_media_sensor_search**.
_You can combine multiple supported entities_, in that case, entity for supported content will be chosen in order how you entered them.
As an example, if content can be played / shown both by kodi and adb, and you entered kodi first, it will be shown by kodi. If it cannot be played by kodi but can be played by adb, adb will be used.
This will also work with play button being shown, it will only show when you can actually play content on your device.
Example of card configuration:
```
type: 'custom:plex-meets-homeassistant'
token: Plex token
ip: Plex IP address
port: Plex port
entity_id: Android TV media_player entity
libraryName: Plex library name that you wish to display
protocol: Optional - protocol to use for plex, defaults to http
maxCount: Optional - maximum number of items to display
token: QWdsqEXAMPLETOKENqwerty
ip: 192.168.13.37
port: 32400
libraryName: Movies
protocol: http
maxCount: 10
entity:
kodi: media_player.kodi_123456qwe789rty
adb: media_player.living_room_nvidia_shield
```
If you are using Home Assistant via HTTPS, you need to specify port `https` for Plex and have Plex available on https connection.

@ -18698,7 +18698,72 @@ class Plex {
class PlayController {
constructor(hass, plex, entity) {
this.play = async (mediaID, instantPlay = false) => {
this.supported = {
// kodi: ['movie', 'episode'],
kodi: ['movie'],
adb: ['movie', 'show', 'season', 'episode']
};
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);
};
this.getKodiSearch = async (search) => {
await this.hass.callService('kodi_media_sensors', 'call_method', {
// eslint-disable-next-line @typescript-eslint/camelcase
entity_id: 'sensor.kodi_media_sensor_search',
method: 'search',
item: {
// eslint-disable-next-line @typescript-eslint/camelcase
media_type: 'all',
value: search
}
});
const results = await this.getKodiSearchResults();
let foundResult = {};
lodash.forEach(results, result => {
if (lodash.isEqual(result.title, search)) {
foundResult = result;
return false;
}
});
return foundResult;
};
this.play = async (data, instantPlay = false) => {
const playService = this.getPlayService(data);
switch (playService) {
case 'kodi':
await this.playViaKodi(data.title, data.type);
break;
case 'adb':
await this.playViaADB(data.key.split('/')[3], instantPlay);
break;
default:
throw Error(`No service available to play ${data.title}!`);
}
};
this.playViaKodi = async (title, type) => {
const kodiData = await this.getKodiSearch(title);
if (type === 'movie') {
await this.hass.callService('kodi', 'call_method', {
// eslint-disable-next-line @typescript-eslint/camelcase
entity_id: this.entity.kodi,
method: 'Player.Open',
item: {
// eslint-disable-next-line @typescript-eslint/camelcase
movieid: kodiData.movieid
}
});
}
else if (type === 'episode') {
console.log('TODO');
}
else {
throw Error(`Plex type ${type} is not supported in Kodi.`);
}
};
this.playViaADB = async (mediaID, instantPlay = false) => {
const serverID = await this.plex.getServerID();
let command = `am start`;
if (instantPlay) {
@ -18707,19 +18772,45 @@ class PlayController {
command += ` -a android.intent.action.VIEW 'plex://server://${serverID}/com.plexapp.plugins.library/library/metadata/${mediaID}'`;
this.hass.callService('androidtv', 'adb_command', {
// eslint-disable-next-line @typescript-eslint/camelcase
entity_id: this.entity,
entity_id: this.entity.adb,
command: 'HOME'
});
this.hass.callService('androidtv', 'adb_command', {
// eslint-disable-next-line @typescript-eslint/camelcase
entity_id: this.entity,
entity_id: this.entity.adb,
command
});
};
this.isPlaySupported = () => {
return (this.hass.states[this.entity] &&
this.hass.states[this.entity].attributes &&
this.hass.states[this.entity].attributes.adb_response !== undefined);
this.getPlayService = (data) => {
let service = '';
lodash.forEach(this.entity, (value, key) => {
if (lodash.includes(this.supported[key], data.type)) {
if ((key === 'kodi' && this.isKodiSupported()) || (key === 'adb' && this.isADBSupported())) {
service = key;
return false;
}
}
});
return service;
};
this.isPlaySupported = (data) => {
return !lodash.isEmpty(this.getPlayService(data));
};
this.isKodiSupported = () => {
if (this.entity.kodi) {
return (this.hass.states[this.entity.kodi] &&
this.hass.states['sensor.kodi_media_sensor_search'] &&
this.hass.states['sensor.kodi_media_sensor_search'].state !== 'unavailable' &&
this.hass.states[this.entity.kodi].state !== 'off' &&
this.hass.states[this.entity.kodi].state !== 'unavailable' &&
lodash.includes(this.entity.kodi, 'kodi_'));
}
return false;
};
this.isADBSupported = () => {
return (this.hass.states[this.entity.adb] &&
this.hass.states[this.entity.adb].attributes &&
this.hass.states[this.entity.adb].attributes.adb_response !== undefined);
};
this.hass = hass;
this.plex = plex;
@ -19309,7 +19400,6 @@ class PlexMeetsHomeAssistant extends HTMLElement {
this.requestTimeout = 3000;
this.loading = false;
this.maxCount = false;
this.playSupported = false;
this.error = '';
this.previousPositions = [];
this.contentBGHeight = 0;
@ -19402,7 +19492,6 @@ class PlexMeetsHomeAssistant extends HTMLElement {
this.detailElem.className = 'detail';
this.detailElem.innerHTML =
"<h1></h1><h2></h2><span class='metaInfo'></span><span class='detailDesc'></span><div class='clear'></div>";
if (this.playSupported) ;
this.content.appendChild(this.detailElem);
this.seasonsElem = document.createElement('div');
this.seasonsElem.className = 'seasons';
@ -19628,17 +19717,21 @@ class PlexMeetsHomeAssistant extends HTMLElement {
seasonElem.style.height = `${CSS_STYLE.height - 3}px`;
seasonElem.style.backgroundImage = `url('${thumbURL}')`;
seasonElem.dataset.clicked = 'false';
if (this.playController && !this.playController.isPlaySupported(seasonData)) {
seasonElem.style.cursor = 'pointer';
}
const interactiveArea = document.createElement('div');
interactiveArea.className = 'interactiveArea';
const playButton = document.createElement('button');
playButton.name = 'playButton';
playButton.addEventListener('click', event => {
event.stopPropagation();
if (this.plex && this.playController) {
this.playController.play(seasonData.key.split('/')[3]);
}
});
interactiveArea.append(playButton);
if (this.playController && this.playController.isPlaySupported(seasonData)) {
const playButton = this.getPlayButton();
playButton.addEventListener('click', event => {
event.stopPropagation();
if (this.plex && this.playController) {
this.playController.play(seasonData);
}
});
interactiveArea.append(playButton);
}
seasonElem.append(interactiveArea);
seasonContainer.append(seasonElem);
const seasonTitleElem = document.createElement('div');
@ -19688,18 +19781,20 @@ class PlexMeetsHomeAssistant extends HTMLElement {
episodeElem.style.height = `${CSS_STYLE.episodeHeight}px`;
episodeElem.style.backgroundImage = `url('${episodeThumbURL}')`;
episodeElem.dataset.clicked = 'false';
const episodeInteractiveArea = document.createElement('div');
episodeInteractiveArea.className = 'interactiveArea';
const episodePlayButton = document.createElement('button');
episodePlayButton.name = 'playButton';
episodePlayButton.addEventListener('click', episodeEvent => {
episodeEvent.stopPropagation();
if (this.plex && this.playController) {
this.playController.play(episodeData.key.split('/')[3], true);
}
});
episodeInteractiveArea.append(episodePlayButton);
episodeElem.append(episodeInteractiveArea);
if (this.playController && this.playController.isPlaySupported(episodeData)) {
const episodeInteractiveArea = document.createElement('div');
episodeInteractiveArea.className = 'interactiveArea';
const episodePlayButton = document.createElement('button');
episodePlayButton.name = 'playButton';
episodePlayButton.addEventListener('click', episodeEvent => {
episodeEvent.stopPropagation();
if (this.plex && this.playController) {
this.playController.play(episodeData, true);
}
});
episodeInteractiveArea.append(episodePlayButton);
episodeElem.append(episodeInteractiveArea);
}
episodeContainer.append(episodeElem);
const episodeTitleElem = document.createElement('div');
episodeTitleElem.className = 'episodeTitleElem';
@ -19853,7 +19948,7 @@ class PlexMeetsHomeAssistant extends HTMLElement {
movieElem.style.width = `${CSS_STYLE.width}px`;
movieElem.style.height = `${CSS_STYLE.height}px`;
movieElem.style.backgroundImage = `url('${thumbURL}')`;
if (!this.playSupported) {
if (this.playController && !this.playController.isPlaySupported(data)) {
movieElem.style.cursor = 'pointer';
}
movieElem.addEventListener('click', () => {
@ -19863,16 +19958,14 @@ class PlexMeetsHomeAssistant extends HTMLElement {
const playButton = this.getPlayButton();
const interactiveArea = document.createElement('div');
interactiveArea.className = 'interactiveArea';
if (this.playSupported) {
if (this.playController && this.playController.isPlaySupported(data)) {
interactiveArea.append(playButton);
}
movieElem.append(interactiveArea);
playButton.addEventListener('click', event => {
event.stopPropagation();
const keyParts = data.key.split('/');
const movieID = keyParts[3];
if (this.hassObj && this.playController) {
this.playController.play(movieID, data.type === 'movie');
this.playController.play(data, data.type === 'movie');
}
});
const titleElem = document.createElement('div');
@ -19898,8 +19991,8 @@ class PlexMeetsHomeAssistant extends HTMLElement {
};
this.setConfig = (config) => {
this.plexProtocol = 'http';
if (!config.entity_id) {
throw new Error('You need to define an entity_id');
if (config.entity.length === 0) {
throw new Error('You need to define at least one entity');
}
if (!config.token) {
throw new Error('You need to define a token');
@ -19929,12 +20022,9 @@ class PlexMeetsHomeAssistant extends HTMLElement {
set hass(hass) {
this.hassObj = hass;
if (this.plex) {
this.playController = new PlayController(this.hassObj, this.plex, this.config.entity_id);
this.playController = new PlayController(this.hassObj, this.plex, this.config.entity);
}
if (!this.content) {
if (this.playController) {
this.playSupported = this.playController.isPlaySupported();
}
this.error = '';
if (!this.loading) {
this.loadInitialData();

@ -1,20 +1,92 @@
/* eslint-disable consistent-return */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { HomeAssistant } from 'custom-card-helpers';
import _ from 'lodash';
import Plex from './Plex';
class PlayController {
entity: string;
entity: Record<string, any>;
hass: HomeAssistant;
plex: Plex;
constructor(hass: HomeAssistant, plex: Plex, entity: string) {
supported: any = {
// kodi: ['movie', 'episode'],
kodi: ['movie'],
adb: ['movie', 'show', 'season', 'episode']
};
constructor(hass: HomeAssistant, plex: Plex, entity: Record<string, any>) {
this.hass = hass;
this.plex = plex;
this.entity = entity;
}
play = async (mediaID: number, instantPlay = false): Promise<void> => {
private getState = async (entityID: string): Promise<Record<string, any>> => {
return this.hass.callApi('GET', `states/${entityID}`);
};
private getKodiSearchResults = async (): Promise<Record<string, any>> => {
return JSON.parse((await this.getState('sensor.kodi_media_sensor_search')).attributes.data);
};
private getKodiSearch = async (search: string): Promise<Record<string, any>> => {
await this.hass.callService('kodi_media_sensors', 'call_method', {
// eslint-disable-next-line @typescript-eslint/camelcase
entity_id: 'sensor.kodi_media_sensor_search',
method: 'search',
item: {
// eslint-disable-next-line @typescript-eslint/camelcase
media_type: 'all',
value: search
}
});
const results = await this.getKodiSearchResults();
let foundResult = {};
_.forEach(results, result => {
if (_.isEqual(result.title, search)) {
foundResult = result;
return false;
}
});
return foundResult;
};
play = async (data: Record<string, any>, instantPlay = false): Promise<void> => {
const playService = this.getPlayService(data);
switch (playService) {
case 'kodi':
await this.playViaKodi(data.title, data.type);
break;
case 'adb':
await this.playViaADB(data.key.split('/')[3], instantPlay);
break;
default:
throw Error(`No service available to play ${data.title}!`);
}
};
private playViaKodi = async (title: string, type: string): Promise<void> => {
const kodiData = await this.getKodiSearch(title);
if (type === 'movie') {
await this.hass.callService('kodi', 'call_method', {
// eslint-disable-next-line @typescript-eslint/camelcase
entity_id: this.entity.kodi,
method: 'Player.Open',
item: {
// eslint-disable-next-line @typescript-eslint/camelcase
movieid: kodiData.movieid
}
});
} else if (type === 'episode') {
console.log('TODO');
} else {
throw Error(`Plex type ${type} is not supported in Kodi.`);
}
};
private playViaADB = async (mediaID: number, instantPlay = false): Promise<void> => {
const serverID = await this.plex.getServerID();
let command = `am start`;
@ -26,21 +98,52 @@ class PlayController {
this.hass.callService('androidtv', 'adb_command', {
// eslint-disable-next-line @typescript-eslint/camelcase
entity_id: this.entity,
entity_id: this.entity.adb,
command: 'HOME'
});
this.hass.callService('androidtv', 'adb_command', {
// eslint-disable-next-line @typescript-eslint/camelcase
entity_id: this.entity,
entity_id: this.entity.adb,
command
});
};
isPlaySupported = (): boolean => {
private getPlayService = (data: Record<string, any>): string => {
let service = '';
_.forEach(this.entity, (value, key) => {
if (_.includes(this.supported[key], data.type)) {
if ((key === 'kodi' && this.isKodiSupported()) || (key === 'adb' && this.isADBSupported())) {
service = key;
return false;
}
}
});
return service;
};
isPlaySupported = (data: Record<string, any>): boolean => {
return !_.isEmpty(this.getPlayService(data));
};
private isKodiSupported = (): boolean => {
if (this.entity.kodi) {
return (
this.hass.states[this.entity.kodi] &&
this.hass.states['sensor.kodi_media_sensor_search'] &&
this.hass.states['sensor.kodi_media_sensor_search'].state !== 'unavailable' &&
this.hass.states[this.entity.kodi].state !== 'off' &&
this.hass.states[this.entity.kodi].state !== 'unavailable' &&
_.includes(this.entity.kodi, 'kodi_')
);
}
return false;
};
private isADBSupported = (): boolean => {
return (
this.hass.states[this.entity] &&
this.hass.states[this.entity].attributes &&
this.hass.states[this.entity].attributes.adb_response !== undefined
this.hass.states[this.entity.adb] &&
this.hass.states[this.entity.adb].attributes &&
this.hass.states[this.entity.adb].attributes.adb_response !== undefined
);
};
}

@ -45,8 +45,6 @@ class PlexMeetsHomeAssistant extends HTMLElement {
maxCount: false | number = false;
playSupported = false;
error = '';
content: any;
@ -62,14 +60,10 @@ 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_id);
this.playController = new PlayController(this.hassObj, this.plex, this.config.entity);
}
if (!this.content) {
if (this.playController) {
this.playSupported = this.playController.isPlaySupported();
}
this.error = '';
if (!this.loading) {
this.loadInitialData();
@ -175,11 +169,6 @@ class PlexMeetsHomeAssistant extends HTMLElement {
this.detailElem.innerHTML =
"<h1></h1><h2></h2><span class='metaInfo'></span><span class='detailDesc'></span><div class='clear'></div>";
if (this.playSupported) {
// todo: temp disabled
// this.detailElem.innerHTML += "<span class='detailPlayAction'></span>";
}
this.content.appendChild(this.detailElem);
this.seasonsElem = document.createElement('div');
@ -426,19 +415,24 @@ class PlexMeetsHomeAssistant extends HTMLElement {
seasonElem.style.backgroundImage = `url('${thumbURL}')`;
seasonElem.dataset.clicked = 'false';
if (this.playController && !this.playController.isPlaySupported(seasonData)) {
seasonElem.style.cursor = 'pointer';
}
const interactiveArea = document.createElement('div');
interactiveArea.className = 'interactiveArea';
const playButton = document.createElement('button');
playButton.name = 'playButton';
playButton.addEventListener('click', event => {
event.stopPropagation();
if (this.plex && this.playController) {
this.playController.play(seasonData.key.split('/')[3]);
}
});
if (this.playController && this.playController.isPlaySupported(seasonData)) {
const playButton = this.getPlayButton();
playButton.addEventListener('click', event => {
event.stopPropagation();
if (this.plex && this.playController) {
this.playController.play(seasonData);
}
});
interactiveArea.append(playButton);
interactiveArea.append(playButton);
}
seasonElem.append(interactiveArea);
seasonContainer.append(seasonElem);
@ -498,21 +492,22 @@ class PlexMeetsHomeAssistant extends HTMLElement {
episodeElem.style.backgroundImage = `url('${episodeThumbURL}')`;
episodeElem.dataset.clicked = 'false';
const episodeInteractiveArea = document.createElement('div');
episodeInteractiveArea.className = 'interactiveArea';
const episodePlayButton = document.createElement('button');
episodePlayButton.name = 'playButton';
episodePlayButton.addEventListener('click', episodeEvent => {
episodeEvent.stopPropagation();
if (this.plex && this.playController) {
this.playController.play(episodeData.key.split('/')[3], true);
}
});
episodeInteractiveArea.append(episodePlayButton);
episodeElem.append(episodeInteractiveArea);
if (this.playController && this.playController.isPlaySupported(episodeData)) {
const episodeInteractiveArea = document.createElement('div');
episodeInteractiveArea.className = 'interactiveArea';
const episodePlayButton = document.createElement('button');
episodePlayButton.name = 'playButton';
episodePlayButton.addEventListener('click', episodeEvent => {
episodeEvent.stopPropagation();
if (this.plex && this.playController) {
this.playController.play(episodeData, true);
}
});
episodeInteractiveArea.append(episodePlayButton);
episodeElem.append(episodeInteractiveArea);
}
episodeContainer.append(episodeElem);
const episodeTitleElem = document.createElement('div');
@ -686,7 +681,7 @@ class PlexMeetsHomeAssistant extends HTMLElement {
movieElem.style.width = `${CSS_STYLE.width}px`;
movieElem.style.height = `${CSS_STYLE.height}px`;
movieElem.style.backgroundImage = `url('${thumbURL}')`;
if (!this.playSupported) {
if (this.playController && !this.playController.isPlaySupported(data)) {
movieElem.style.cursor = 'pointer';
}
@ -698,7 +693,7 @@ class PlexMeetsHomeAssistant extends HTMLElement {
const playButton = this.getPlayButton();
const interactiveArea = document.createElement('div');
interactiveArea.className = 'interactiveArea';
if (this.playSupported) {
if (this.playController && this.playController.isPlaySupported(data)) {
interactiveArea.append(playButton);
}
@ -706,11 +701,9 @@ class PlexMeetsHomeAssistant extends HTMLElement {
playButton.addEventListener('click', event => {
event.stopPropagation();
const keyParts = data.key.split('/');
const movieID = keyParts[3];
if (this.hassObj && this.playController) {
this.playController.play(movieID, data.type === 'movie');
this.playController.play(data, data.type === 'movie');
}
});
@ -743,8 +736,8 @@ class PlexMeetsHomeAssistant extends HTMLElement {
setConfig = (config: any): void => {
this.plexProtocol = 'http';
if (!config.entity_id) {
throw new Error('You need to define an entity_id');
if (config.entity.length === 0) {
throw new Error('You need to define at least one entity');
}
if (!config.token) {
throw new Error('You need to define a token');

Loading…
Cancel
Save