From 365baf91b11d570cbaacc00bbfc109e879611a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Thu, 2 Sep 2021 18:21:10 +0200 Subject: [PATCH 01/16] WIP cast --- dist/plex-meets-homeassistant.js | 55 ++++++++++++++++++++++++++++++-- src/const.ts | 2 +- src/modules/PlayController.ts | 46 +++++++++++++++++++++++++- src/modules/Plex.ts | 12 +++++++ 4 files changed, 111 insertions(+), 4 deletions(-) diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index 1450540..34afd6e 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -17207,7 +17207,7 @@ const supported = { kodi: ['movie', 'episode'], androidtv: ['movie', 'show', 'season', 'episode', 'clip'], plexPlayer: ['movie', 'show', 'season', 'episode', 'clip'], - cast: ['movie', 'episode'] + cast: ['movie', 'episode', 'epg'] }; var bind = function bind(fn, thisArg) { @@ -18942,6 +18942,13 @@ class Plex { timeout: this.requestTimeout })).data.MediaContainer; }; + this.tune = async (channelID, session) => { + // Todo: what is 12? do we need to get this from somewhere and change? + const url = this.authorizeURL(`${this.getBasicURL()}/livetv/dvrs/12/channels/${channelID}/tune?X-Plex-Session-Identifier=${session}`); + return (await axios.post(url, { + timeout: this.requestTimeout + })).data.MediaContainer; + }; this.getContinueWatching = async () => { const hubs = await this.getHubs(); let continueWatchingData = {}; @@ -19309,7 +19316,43 @@ class PlayController { await this.playViaPlexPlayer(entity.value, processData.key.split('/')[3]); break; case 'cast': - if (this.hass.services.plex) { + if (!lodash.isNil(data.epg)) { + const session = `${Math.floor(Date.now() / 1000)}`; + const streamData = await this.plex.tune(data.channelIdentifier, session); + console.log(streamData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key); + let startURL = `/video/:/transcode/universal/start`; + startURL += `?hasMDE=1`; + startURL += `&path=${streamData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key}`; + startURL += `&mediaIndex=0`; + startURL += `&partIndex=0`; + startURL += `&protocol=http`; + startURL += `&fastSeek=1`; + startURL += `&directPlay=0`; + startURL += `&directStream=1`; + startURL += `&subtitleSize=100`; + startURL += `&audioBoost=100`; + startURL += `&location=lan`; + startURL += `&directStreamAudio=1`; + startURL += `&mediaBufferSize=30720`; + startURL += `&session=${session}`; + startURL += `&offset=0`; + startURL += `&subtitles=burn`; + startURL += `©ts=0`; + startURL += `&X-Plex-Session-Identifier=${session}`; + startURL += `&X-Plex-Client-Profile-Extra=add-transcode-target-audio-codec%28type%3DvideoProfile%26context%3Dstreaming%26protocol%3Dhttp%26audioCodec%3Dac3%29%2Badd-limitation%28scope%3DvideoAudioCodec%26scopeName%3Dac3%26type%3DupperBound%26name%3Daudio.channel%26value%3D6%29%2Badd-transcode-target-audio-codec%28type%3DvideoProfile%26context%3Dstreaming%26protocol%3Dhttp%26audioCodec%3Deac3%29%2Badd-limitation%28scope%3DvideoAudioCodec%26scopeName%3Deac3%26type%3DupperBound%26name%3Daudio.channel%26value%3D6%29%2Badd-limitation%28scope%3DvideoAudioCodec%26scopeName%3Daac%26type%3DupperBound%26name%3Daudio.channel%26value%3D2%29%2Badd-limitation%28scope%3DvideoTranscodeTarget%26scopeName%3Dhevc%26scopeType%3DvideoCodec%26context%3Dstreaming%26protocol%3Dhttp%26type%3Dmatch%26name%3Dvideo.colorTrc%26list%3Dbt709%7Cbt470m%7Cbt470bg%7Csmpte170m%7Csmpte240m%7Cbt2020-10%7Csmpte2084%26isRequired%3Dfalse%29`; + startURL += `&X-Plex-Chunked=1`; + startURL += `&X-Plex-Product=Plex%20Cast`; + startURL += `&X-Plex-Version=4.54.1`; + startURL += `&X-Plex-Client-Identifier=3ievywhylzj29yvxxjmoyq7h`; + startURL += `&X-Plex-Platform-Version=86.0`; + startURL += `&X-Plex-Device=Android`; + startURL += `&X-Plex-Device-Name=Chromecast`; + startURL += `&X-Plex-Device-Screen-Resolution=1280x720%2C960x540`; + startURL += `&X-Plex-Token=transient-ba74f27d-1aae-4518-9047-b7fcfdd88dbb`; + console.log(startURL); + this.playViaCast(entity.value, startURL); + } + else if (this.hass.services.plex) { const libraryName = lodash.isNil(processData.librarySectionTitle) ? this.libraryName : processData.librarySectionTitle; @@ -19470,6 +19513,14 @@ class PlayController { } }; this.playViaCast = (entityName, mediaLink) => { + console.log({ + // eslint-disable-next-line @typescript-eslint/camelcase + entity_id: entityName, + // eslint-disable-next-line @typescript-eslint/camelcase + media_content_type: 'video', + // eslint-disable-next-line @typescript-eslint/camelcase + media_content_id: this.plex.authorizeURL(`${this.plex.getBasicURL()}${mediaLink}`) + }); this.hass.callService('media_player', 'play_media', { // eslint-disable-next-line @typescript-eslint/camelcase entity_id: entityName, diff --git a/src/const.ts b/src/const.ts index 12de975..e4371a4 100644 --- a/src/const.ts +++ b/src/const.ts @@ -12,7 +12,7 @@ const supported: any = { kodi: ['movie', 'episode'], androidtv: ['movie', 'show', 'season', 'episode', 'clip'], plexPlayer: ['movie', 'show', 'season', 'episode', 'clip'], - cast: ['movie', 'episode'] + cast: ['movie', 'episode', 'epg'] }; const LOREM_IPSUM = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec semper risus vitae aliquet interdum. Nulla facilisi. Pellentesque viverra sagittis lorem eget aliquet. Cras vehicula, purus vel consectetur mattis, ipsum arcu ullamcorper mi, id viverra purus ex eu dolor. Integer vehicula lacinia sem convallis iaculis. Nulla fermentum erat interdum, efficitur felis in, mollis neque. Vivamus luctus metus eget nisl pellentesque, placerat elementum magna eleifend. diff --git a/src/modules/PlayController.ts b/src/modules/PlayController.ts index 91e04ae..4bc3438 100644 --- a/src/modules/PlayController.ts +++ b/src/modules/PlayController.ts @@ -131,7 +131,43 @@ class PlayController { await this.playViaPlexPlayer(entity.value, processData.key.split('/')[3]); break; case 'cast': - if (this.hass.services.plex) { + if (!_.isNil(data.epg)) { + const session = `${Math.floor(Date.now() / 1000)}`; + const streamData = await this.plex.tune(data.channelIdentifier, session); + console.log(streamData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key); + let startURL = `/video/:/transcode/universal/start`; + startURL += `?hasMDE=1`; + startURL += `&path=${streamData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key}`; + startURL += `&mediaIndex=0`; + startURL += `&partIndex=0`; + startURL += `&protocol=http`; + startURL += `&fastSeek=1`; + startURL += `&directPlay=0`; + startURL += `&directStream=1`; + startURL += `&subtitleSize=100`; + startURL += `&audioBoost=100`; + startURL += `&location=lan`; + startURL += `&directStreamAudio=1`; + startURL += `&mediaBufferSize=30720`; + startURL += `&session=${session}`; + startURL += `&offset=0`; + startURL += `&subtitles=burn`; + startURL += `©ts=0`; + startURL += `&X-Plex-Session-Identifier=${session}`; + startURL += `&X-Plex-Client-Profile-Extra=add-transcode-target-audio-codec%28type%3DvideoProfile%26context%3Dstreaming%26protocol%3Dhttp%26audioCodec%3Dac3%29%2Badd-limitation%28scope%3DvideoAudioCodec%26scopeName%3Dac3%26type%3DupperBound%26name%3Daudio.channel%26value%3D6%29%2Badd-transcode-target-audio-codec%28type%3DvideoProfile%26context%3Dstreaming%26protocol%3Dhttp%26audioCodec%3Deac3%29%2Badd-limitation%28scope%3DvideoAudioCodec%26scopeName%3Deac3%26type%3DupperBound%26name%3Daudio.channel%26value%3D6%29%2Badd-limitation%28scope%3DvideoAudioCodec%26scopeName%3Daac%26type%3DupperBound%26name%3Daudio.channel%26value%3D2%29%2Badd-limitation%28scope%3DvideoTranscodeTarget%26scopeName%3Dhevc%26scopeType%3DvideoCodec%26context%3Dstreaming%26protocol%3Dhttp%26type%3Dmatch%26name%3Dvideo.colorTrc%26list%3Dbt709%7Cbt470m%7Cbt470bg%7Csmpte170m%7Csmpte240m%7Cbt2020-10%7Csmpte2084%26isRequired%3Dfalse%29`; + startURL += `&X-Plex-Chunked=1`; + startURL += `&X-Plex-Product=Plex%20Cast`; + startURL += `&X-Plex-Version=4.54.1`; + startURL += `&X-Plex-Client-Identifier=3ievywhylzj29yvxxjmoyq7h`; + startURL += `&X-Plex-Platform-Version=86.0`; + startURL += `&X-Plex-Device=Android`; + startURL += `&X-Plex-Device-Name=Chromecast`; + startURL += `&X-Plex-Device-Screen-Resolution=1280x720%2C960x540`; + startURL += `&X-Plex-Token=transient-ba74f27d-1aae-4518-9047-b7fcfdd88dbb`; + console.log(startURL); + + this.playViaCast(entity.value, startURL); + } else if (this.hass.services.plex) { const libraryName = _.isNil(processData.librarySectionTitle) ? this.libraryName : processData.librarySectionTitle; @@ -305,6 +341,14 @@ class PlayController { }; private playViaCast = (entityName: string, mediaLink: string): void => { + console.log({ + // eslint-disable-next-line @typescript-eslint/camelcase + entity_id: entityName, + // eslint-disable-next-line @typescript-eslint/camelcase + media_content_type: 'video', + // eslint-disable-next-line @typescript-eslint/camelcase + media_content_id: this.plex.authorizeURL(`${this.plex.getBasicURL()}${mediaLink}`) + }); this.hass.callService('media_player', 'play_media', { // eslint-disable-next-line @typescript-eslint/camelcase entity_id: entityName, diff --git a/src/modules/Plex.ts b/src/modules/Plex.ts index 2bea9bf..39e7e06 100644 --- a/src/modules/Plex.ts +++ b/src/modules/Plex.ts @@ -365,6 +365,18 @@ class Plex { ).data.MediaContainer; }; + tune = async (channelID: string, session: string): Promise => { + // Todo: what is 12? do we need to get this from somewhere and change? + const url = this.authorizeURL( + `${this.getBasicURL()}/livetv/dvrs/12/channels/${channelID}/tune?X-Plex-Session-Identifier=${session}` + ); + return ( + await axios.post(url, { + timeout: this.requestTimeout + }) + ).data.MediaContainer; + }; + getContinueWatching = async (): Promise => { const hubs = await this.getHubs(); let continueWatchingData: Record = {}; From 3b45a05fe06e2b7c620c31326114c0bfbb417ce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Thu, 2 Sep 2021 18:21:43 +0200 Subject: [PATCH 02/16] WIP cast --- dist/plex-meets-homeassistant.js | 4 ++-- src/modules/PlayController.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index 34afd6e..bafac95 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -19343,12 +19343,12 @@ class PlayController { startURL += `&X-Plex-Chunked=1`; startURL += `&X-Plex-Product=Plex%20Cast`; startURL += `&X-Plex-Version=4.54.1`; - startURL += `&X-Plex-Client-Identifier=3ievywhylzj29yvxxjmoyq7h`; + startURL += `&X-Plex-Client-Identifier=CHANGE_ME`; startURL += `&X-Plex-Platform-Version=86.0`; startURL += `&X-Plex-Device=Android`; startURL += `&X-Plex-Device-Name=Chromecast`; startURL += `&X-Plex-Device-Screen-Resolution=1280x720%2C960x540`; - startURL += `&X-Plex-Token=transient-ba74f27d-1aae-4518-9047-b7fcfdd88dbb`; + startURL += `&X-Plex-Token=CHANGE_ME`; console.log(startURL); this.playViaCast(entity.value, startURL); } diff --git a/src/modules/PlayController.ts b/src/modules/PlayController.ts index 4bc3438..1872754 100644 --- a/src/modules/PlayController.ts +++ b/src/modules/PlayController.ts @@ -158,12 +158,12 @@ class PlayController { startURL += `&X-Plex-Chunked=1`; startURL += `&X-Plex-Product=Plex%20Cast`; startURL += `&X-Plex-Version=4.54.1`; - startURL += `&X-Plex-Client-Identifier=3ievywhylzj29yvxxjmoyq7h`; + startURL += `&X-Plex-Client-Identifier=CHANGE_ME`; startURL += `&X-Plex-Platform-Version=86.0`; startURL += `&X-Plex-Device=Android`; startURL += `&X-Plex-Device-Name=Chromecast`; startURL += `&X-Plex-Device-Screen-Resolution=1280x720%2C960x540`; - startURL += `&X-Plex-Token=transient-ba74f27d-1aae-4518-9047-b7fcfdd88dbb`; + startURL += `&X-Plex-Token=CHANGE_ME`; console.log(startURL); this.playViaCast(entity.value, startURL); From cbff0edb3ef7496c077118af723913f265bd4cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Thu, 2 Sep 2021 20:29:49 +0200 Subject: [PATCH 03/16] WIP tune process --- dist/plex-meets-homeassistant.js | 141 ++++++++++++++++++++++--------- src/const.ts | 2 +- src/modules/PlayController.ts | 58 +++++-------- src/modules/Plex.ts | 97 ++++++++++++++++++++- 4 files changed, 219 insertions(+), 79 deletions(-) diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index bafac95..a3f2379 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -17205,7 +17205,7 @@ const CSS_STYLE = { }; const supported = { kodi: ['movie', 'episode'], - androidtv: ['movie', 'show', 'season', 'episode', 'clip'], + androidtv: ['movie', 'show', 'season', 'episode', 'clip', 'epg'], plexPlayer: ['movie', 'show', 'season', 'episode', 'clip'], cast: ['movie', 'episode', 'epg'] }; @@ -18943,11 +18943,90 @@ class Plex { })).data.MediaContainer; }; this.tune = async (channelID, session) => { + session = 'PlexMeetsHomeAssistant'; + console.log(channelID); // Todo: what is 12? do we need to get this from somewhere and change? - const url = this.authorizeURL(`${this.getBasicURL()}/livetv/dvrs/12/channels/${channelID}/tune?X-Plex-Session-Identifier=${session}`); - return (await axios.post(url, { + let url = this.authorizeURL(`${this.getBasicURL()}/livetv/dvrs/12/channels/${channelID}/tune?X-Plex-Language=en-us`); + console.log('Starting tune process...'); + url = `${this.getBasicURL()}/livetv/dvrs/12/channels/`; + url += `${channelID}`; + url += `/tune`; + url += `?X-Plex-Client-Identifier=${session}`; + url += `&X-Plex-Session-Identifier=${session}`; + const tuneData = (await axios.post(this.authorizeURL(url), { timeout: this.requestTimeout })).data.MediaContainer; + console.log(url); + console.log(tuneData.MediaSubscription[0].MediaGrabOperation[0].Metadata.title); + console.log('___'); + let startURL = `${this.getBasicURL()}/video/:/transcode/universal/start.mpd`; + startURL += `?hasMDE=1`; + startURL += `&path=${tuneData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key}`; + startURL += `&mediaIndex=0`; + startURL += `&partIndex=0`; + startURL += `&protocol=dash`; + startURL += `&fastSeek=1`; + startURL += `&directPlay=0`; + startURL += `&directStream=1`; + startURL += `&subtitleSize=100`; + startURL += `&audioBoost=100`; + startURL += `&location=lan`; + startURL += `&addDebugOverlay=0`; + startURL += `&autoAdjustQuality=0`; + startURL += `&directStreamAudio=1`; + startURL += `&mediaBufferSize=102400`; + startURL += `&session=${session}`; + startURL += `&subtitles=burn`; + startURL += `©ts=0`; + startURL += `&Accept-Language=en-GB`; + startURL += `&X-Plex-Session-Identifier=${session}`; + startURL += `&X-Plex-Client-Profile-Extra=append-transcode-target-codec%28type%3DvideoProfile%26context%3Dstreaming%26audioCodec%3Daac%26protocol%3Ddash%29`; + startURL += `&X-Plex-Incomplete-Segments=1`; + startURL += `&X-Plex-Product=Plex%20Web`; + startURL += `&X-Plex-Version=4.59.2`; + startURL += `&X-Plex-Client-Identifier=${session}`; + startURL += `&X-Plex-Platform=Chrome`; + startURL += `&X-Plex-Platform-Version=92.0`; + startURL += `&X-Plex-Sync-Version=2`; + startURL += `&X-Plex-Features=external-media%2Cindirect-media`; + startURL += `&X-Plex-Model=bundled`; + startURL += `&X-Plex-Device=OSX`; + startURL += `&X-Plex-Device-Name=Chrome`; + startURL += `&X-Plex-Device-Screen-Resolution=1792x444%2C1792x1120`; + startURL += `&X-Plex-Language=en-GB`; + console.log('Deciding...'); + let decisionURL = `${this.getBasicURL()}/video/:/transcode/universal/decision`; + decisionURL += `?advancedSubtitles=text`; + decisionURL += `&audioBoost=100`; + decisionURL += `&autoAdjustQuality=0`; + decisionURL += `&directPlay=1`; + decisionURL += `&directStream=1`; + decisionURL += `&directStreamAudio=1`; + decisionURL += `&fastSeek=1`; + decisionURL += `&hasMDE=1`; + decisionURL += `&location=lan`; + decisionURL += `&mediaIndex=0`; + decisionURL += `&partIndex=0`; + decisionURL += `&path=${tuneData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key}`; + decisionURL += `&protocol=*`; + decisionURL += `&session=${session}`; + decisionURL += `&skipSubtitles=1`; + decisionURL += `&videoBitrate=200000`; + decisionURL += `&videoQuality=100`; + decisionURL += `&X-Plex-Client-Identifier=${session}`; + decisionURL += `&X-Plex-Platform=Android`; + const res = await axios.get(this.authorizeURL(decisionURL), { + timeout: this.requestTimeout + }); + console.log(res); + console.log('Starting...'); + // why bad request??? + const res1 = await axios.get(this.authorizeURL(startURL), { + timeout: 60000 + }); + console.log(res1); + console.log('____'); + return res.data.MediaContainer.Metadata[0].Media[0].Part[0].key; }; this.getContinueWatching = async () => { const hubs = await this.getHubs(); @@ -19310,47 +19389,32 @@ class PlayController { await this.playViaKodi(entity.value, processData, processData.type); break; case 'androidtv': - await this.playViaAndroidTV(entity.value, processData.key, instantPlay, provider); + if (!lodash.isNil(data.epg)) { + const session = `${Math.floor(Date.now() / 1000)}`; + const streamData = await this.plex.tune(data.channelIdentifier, session); + console.log(streamData); + /* + await this.playViaAndroidTV( + entity.value, + streamData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key, + instantPlay, + provider + ); + */ + } + else { + await this.playViaAndroidTV(entity.value, processData.guid, instantPlay, provider); + } break; case 'plexPlayer': await this.playViaPlexPlayer(entity.value, processData.key.split('/')[3]); break; case 'cast': if (!lodash.isNil(data.epg)) { - const session = `${Math.floor(Date.now() / 1000)}`; - const streamData = await this.plex.tune(data.channelIdentifier, session); - console.log(streamData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key); - let startURL = `/video/:/transcode/universal/start`; - startURL += `?hasMDE=1`; - startURL += `&path=${streamData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key}`; - startURL += `&mediaIndex=0`; - startURL += `&partIndex=0`; - startURL += `&protocol=http`; - startURL += `&fastSeek=1`; - startURL += `&directPlay=0`; - startURL += `&directStream=1`; - startURL += `&subtitleSize=100`; - startURL += `&audioBoost=100`; - startURL += `&location=lan`; - startURL += `&directStreamAudio=1`; - startURL += `&mediaBufferSize=30720`; - startURL += `&session=${session}`; - startURL += `&offset=0`; - startURL += `&subtitles=burn`; - startURL += `©ts=0`; - startURL += `&X-Plex-Session-Identifier=${session}`; - startURL += `&X-Plex-Client-Profile-Extra=add-transcode-target-audio-codec%28type%3DvideoProfile%26context%3Dstreaming%26protocol%3Dhttp%26audioCodec%3Dac3%29%2Badd-limitation%28scope%3DvideoAudioCodec%26scopeName%3Dac3%26type%3DupperBound%26name%3Daudio.channel%26value%3D6%29%2Badd-transcode-target-audio-codec%28type%3DvideoProfile%26context%3Dstreaming%26protocol%3Dhttp%26audioCodec%3Deac3%29%2Badd-limitation%28scope%3DvideoAudioCodec%26scopeName%3Deac3%26type%3DupperBound%26name%3Daudio.channel%26value%3D6%29%2Badd-limitation%28scope%3DvideoAudioCodec%26scopeName%3Daac%26type%3DupperBound%26name%3Daudio.channel%26value%3D2%29%2Badd-limitation%28scope%3DvideoTranscodeTarget%26scopeName%3Dhevc%26scopeType%3DvideoCodec%26context%3Dstreaming%26protocol%3Dhttp%26type%3Dmatch%26name%3Dvideo.colorTrc%26list%3Dbt709%7Cbt470m%7Cbt470bg%7Csmpte170m%7Csmpte240m%7Cbt2020-10%7Csmpte2084%26isRequired%3Dfalse%29`; - startURL += `&X-Plex-Chunked=1`; - startURL += `&X-Plex-Product=Plex%20Cast`; - startURL += `&X-Plex-Version=4.54.1`; - startURL += `&X-Plex-Client-Identifier=CHANGE_ME`; - startURL += `&X-Plex-Platform-Version=86.0`; - startURL += `&X-Plex-Device=Android`; - startURL += `&X-Plex-Device-Name=Chromecast`; - startURL += `&X-Plex-Device-Screen-Resolution=1280x720%2C960x540`; - startURL += `&X-Plex-Token=CHANGE_ME`; - console.log(startURL); - this.playViaCast(entity.value, startURL); + const session = `PlexMeetsHomeAssistant-${Math.floor(Date.now() / 1000)}`; + const streamURL = await this.plex.tune(data.channelIdentifier, session); + console.log(`${this.plex.getBasicURL()}${streamURL}`); + // this.playViaCast(entity.value, `${playlistLink}`); } else if (this.hass.services.plex) { const libraryName = lodash.isNil(processData.librarySectionTitle) @@ -19547,6 +19611,7 @@ class PlayController { command += ' --ez "android.intent.extra.START_PLAYBACK" true'; } command += ` -a android.intent.action.VIEW 'plex://server://${serverID}/${provider}${mediaID}'`; + console.log(command); this.hass.callService('androidtv', 'adb_command', { // eslint-disable-next-line @typescript-eslint/camelcase entity_id: entityName, diff --git a/src/const.ts b/src/const.ts index e4371a4..160edea 100644 --- a/src/const.ts +++ b/src/const.ts @@ -10,7 +10,7 @@ const CSS_STYLE: any = { const supported: any = { kodi: ['movie', 'episode'], - androidtv: ['movie', 'show', 'season', 'episode', 'clip'], + androidtv: ['movie', 'show', 'season', 'episode', 'clip', 'epg'], plexPlayer: ['movie', 'show', 'season', 'episode', 'clip'], cast: ['movie', 'episode', 'epg'] }; diff --git a/src/modules/PlayController.ts b/src/modules/PlayController.ts index 1872754..03ca7c2 100644 --- a/src/modules/PlayController.ts +++ b/src/modules/PlayController.ts @@ -125,48 +125,32 @@ class PlayController { await this.playViaKodi(entity.value, processData, processData.type); break; case 'androidtv': - await this.playViaAndroidTV(entity.value, processData.key, instantPlay, provider); + if (!_.isNil(data.epg)) { + const session = `${Math.floor(Date.now() / 1000)}`; + const streamData = await this.plex.tune(data.channelIdentifier, session); + console.log(streamData); + /* + await this.playViaAndroidTV( + entity.value, + streamData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key, + instantPlay, + provider + ); + */ + } else { + await this.playViaAndroidTV(entity.value, processData.guid, instantPlay, provider); + } + break; case 'plexPlayer': await this.playViaPlexPlayer(entity.value, processData.key.split('/')[3]); break; case 'cast': if (!_.isNil(data.epg)) { - const session = `${Math.floor(Date.now() / 1000)}`; - const streamData = await this.plex.tune(data.channelIdentifier, session); - console.log(streamData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key); - let startURL = `/video/:/transcode/universal/start`; - startURL += `?hasMDE=1`; - startURL += `&path=${streamData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key}`; - startURL += `&mediaIndex=0`; - startURL += `&partIndex=0`; - startURL += `&protocol=http`; - startURL += `&fastSeek=1`; - startURL += `&directPlay=0`; - startURL += `&directStream=1`; - startURL += `&subtitleSize=100`; - startURL += `&audioBoost=100`; - startURL += `&location=lan`; - startURL += `&directStreamAudio=1`; - startURL += `&mediaBufferSize=30720`; - startURL += `&session=${session}`; - startURL += `&offset=0`; - startURL += `&subtitles=burn`; - startURL += `©ts=0`; - startURL += `&X-Plex-Session-Identifier=${session}`; - startURL += `&X-Plex-Client-Profile-Extra=add-transcode-target-audio-codec%28type%3DvideoProfile%26context%3Dstreaming%26protocol%3Dhttp%26audioCodec%3Dac3%29%2Badd-limitation%28scope%3DvideoAudioCodec%26scopeName%3Dac3%26type%3DupperBound%26name%3Daudio.channel%26value%3D6%29%2Badd-transcode-target-audio-codec%28type%3DvideoProfile%26context%3Dstreaming%26protocol%3Dhttp%26audioCodec%3Deac3%29%2Badd-limitation%28scope%3DvideoAudioCodec%26scopeName%3Deac3%26type%3DupperBound%26name%3Daudio.channel%26value%3D6%29%2Badd-limitation%28scope%3DvideoAudioCodec%26scopeName%3Daac%26type%3DupperBound%26name%3Daudio.channel%26value%3D2%29%2Badd-limitation%28scope%3DvideoTranscodeTarget%26scopeName%3Dhevc%26scopeType%3DvideoCodec%26context%3Dstreaming%26protocol%3Dhttp%26type%3Dmatch%26name%3Dvideo.colorTrc%26list%3Dbt709%7Cbt470m%7Cbt470bg%7Csmpte170m%7Csmpte240m%7Cbt2020-10%7Csmpte2084%26isRequired%3Dfalse%29`; - startURL += `&X-Plex-Chunked=1`; - startURL += `&X-Plex-Product=Plex%20Cast`; - startURL += `&X-Plex-Version=4.54.1`; - startURL += `&X-Plex-Client-Identifier=CHANGE_ME`; - startURL += `&X-Plex-Platform-Version=86.0`; - startURL += `&X-Plex-Device=Android`; - startURL += `&X-Plex-Device-Name=Chromecast`; - startURL += `&X-Plex-Device-Screen-Resolution=1280x720%2C960x540`; - startURL += `&X-Plex-Token=CHANGE_ME`; - console.log(startURL); - - this.playViaCast(entity.value, startURL); + const session = `PlexMeetsHomeAssistant-${Math.floor(Date.now() / 1000)}`; + const streamURL = await this.plex.tune(data.channelIdentifier, session); + console.log(`${this.plex.getBasicURL()}${streamURL}`); + // this.playViaCast(entity.value, `${playlistLink}`); } else if (this.hass.services.plex) { const libraryName = _.isNil(processData.librarySectionTitle) ? this.libraryName @@ -385,6 +369,8 @@ class PlayController { command += ` -a android.intent.action.VIEW 'plex://server://${serverID}/${provider}${mediaID}'`; + console.log(command); + this.hass.callService('androidtv', 'adb_command', { // eslint-disable-next-line @typescript-eslint/camelcase entity_id: entityName, diff --git a/src/modules/Plex.ts b/src/modules/Plex.ts index 39e7e06..e65e15d 100644 --- a/src/modules/Plex.ts +++ b/src/modules/Plex.ts @@ -366,15 +366,104 @@ class Plex { }; tune = async (channelID: string, session: string): Promise => { + session = 'PlexMeetsHomeAssistant'; + console.log(channelID); // Todo: what is 12? do we need to get this from somewhere and change? - const url = this.authorizeURL( - `${this.getBasicURL()}/livetv/dvrs/12/channels/${channelID}/tune?X-Plex-Session-Identifier=${session}` + let url = this.authorizeURL( + `${this.getBasicURL()}/livetv/dvrs/12/channels/${channelID}/tune?X-Plex-Language=en-us` ); - return ( - await axios.post(url, { + console.log('Starting tune process...'); + url = `${this.getBasicURL()}/livetv/dvrs/12/channels/`; + url += `${channelID}`; + url += `/tune`; + url += `?X-Plex-Client-Identifier=${session}`; + url += `&X-Plex-Session-Identifier=${session}`; + + const tuneData = ( + await axios.post(this.authorizeURL(url), { timeout: this.requestTimeout }) ).data.MediaContainer; + + console.log(url); + console.log(tuneData.MediaSubscription[0].MediaGrabOperation[0].Metadata.title); + console.log('___'); + + let startURL = `${this.getBasicURL()}/video/:/transcode/universal/start.mpd`; + startURL += `?hasMDE=1`; + startURL += `&path=${tuneData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key}`; + startURL += `&mediaIndex=0`; + startURL += `&partIndex=0`; + startURL += `&protocol=dash`; + startURL += `&fastSeek=1`; + startURL += `&directPlay=0`; + startURL += `&directStream=1`; + startURL += `&subtitleSize=100`; + startURL += `&audioBoost=100`; + startURL += `&location=lan`; + startURL += `&addDebugOverlay=0`; + startURL += `&autoAdjustQuality=0`; + startURL += `&directStreamAudio=1`; + startURL += `&mediaBufferSize=102400`; + startURL += `&session=${session}`; + startURL += `&subtitles=burn`; + startURL += `©ts=0`; + startURL += `&Accept-Language=en-GB`; + startURL += `&X-Plex-Session-Identifier=${session}`; + startURL += `&X-Plex-Client-Profile-Extra=append-transcode-target-codec%28type%3DvideoProfile%26context%3Dstreaming%26audioCodec%3Daac%26protocol%3Ddash%29`; + startURL += `&X-Plex-Incomplete-Segments=1`; + startURL += `&X-Plex-Product=Plex%20Web`; + startURL += `&X-Plex-Version=4.59.2`; + startURL += `&X-Plex-Client-Identifier=${session}`; + startURL += `&X-Plex-Platform=Chrome`; + startURL += `&X-Plex-Platform-Version=92.0`; + startURL += `&X-Plex-Sync-Version=2`; + startURL += `&X-Plex-Features=external-media%2Cindirect-media`; + startURL += `&X-Plex-Model=bundled`; + startURL += `&X-Plex-Device=OSX`; + startURL += `&X-Plex-Device-Name=Chrome`; + startURL += `&X-Plex-Device-Screen-Resolution=1792x444%2C1792x1120`; + startURL += `&X-Plex-Language=en-GB`; + + console.log('Deciding...'); + + let decisionURL = `${this.getBasicURL()}/video/:/transcode/universal/decision`; + + decisionURL += `?advancedSubtitles=text`; + decisionURL += `&audioBoost=100`; + decisionURL += `&autoAdjustQuality=0`; + decisionURL += `&directPlay=1`; + decisionURL += `&directStream=1`; + decisionURL += `&directStreamAudio=1`; + decisionURL += `&fastSeek=1`; + decisionURL += `&hasMDE=1`; + decisionURL += `&location=lan`; + decisionURL += `&mediaIndex=0`; + decisionURL += `&partIndex=0`; + decisionURL += `&path=${tuneData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key}`; + decisionURL += `&protocol=*`; + decisionURL += `&session=${session}`; + decisionURL += `&skipSubtitles=1`; + decisionURL += `&videoBitrate=200000`; + decisionURL += `&videoQuality=100`; + decisionURL += `&X-Plex-Client-Identifier=${session}`; + decisionURL += `&X-Plex-Platform=Android`; + + const res = await axios.get(this.authorizeURL(decisionURL), { + timeout: this.requestTimeout + }); + console.log(res); + + console.log('Starting...'); + + // why bad request??? + const res1 = await axios.get(this.authorizeURL(startURL), { + timeout: 60000 + }); + console.log(res1); + console.log('____'); + + return res.data.MediaContainer.Metadata[0].Media[0].Part[0].key; }; getContinueWatching = async (): Promise => { From 29e174a1bfbed1cc2a247b5489fea3a40889c8cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Thu, 2 Sep 2021 21:16:19 +0200 Subject: [PATCH 04/16] Stream kinda works reliably now --- dist/plex-meets-homeassistant.js | 40 +++++++++++++++++-------- src/modules/Plex.ts | 51 ++++++++++++++++++++++---------- 2 files changed, 63 insertions(+), 28 deletions(-) diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index a3f2379..df18493 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -18943,7 +18943,7 @@ class Plex { })).data.MediaContainer; }; this.tune = async (channelID, session) => { - session = 'PlexMeetsHomeAssistant'; + session = 'PlexMeetsHomeAssistant3'; console.log(channelID); // Todo: what is 12? do we need to get this from somewhere and change? let url = this.authorizeURL(`${this.getBasicURL()}/livetv/dvrs/12/channels/${channelID}/tune?X-Plex-Language=en-us`); @@ -18956,12 +18956,10 @@ class Plex { const tuneData = (await axios.post(this.authorizeURL(url), { timeout: this.requestTimeout })).data.MediaContainer; - console.log(url); - console.log(tuneData.MediaSubscription[0].MediaGrabOperation[0].Metadata.title); - console.log('___'); + console.log('Tuning started.'); let startURL = `${this.getBasicURL()}/video/:/transcode/universal/start.mpd`; startURL += `?hasMDE=1`; - startURL += `&path=${tuneData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key}`; + startURL += `&path=${encodeURIComponent(tuneData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key)}`; startURL += `&mediaIndex=0`; startURL += `&partIndex=0`; startURL += `&protocol=dash`; @@ -18994,7 +18992,6 @@ class Plex { startURL += `&X-Plex-Device-Name=Chrome`; startURL += `&X-Plex-Device-Screen-Resolution=1792x444%2C1792x1120`; startURL += `&X-Plex-Language=en-GB`; - console.log('Deciding...'); let decisionURL = `${this.getBasicURL()}/video/:/transcode/universal/decision`; decisionURL += `?advancedSubtitles=text`; decisionURL += `&audioBoost=100`; @@ -19015,17 +19012,34 @@ class Plex { decisionURL += `&videoQuality=100`; decisionURL += `&X-Plex-Client-Identifier=${session}`; decisionURL += `&X-Plex-Platform=Android`; - const res = await axios.get(this.authorizeURL(decisionURL), { + const url2 = this.authorizeURL(`${this.getBasicURL()}${tuneData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key}?includeBandwidths=1&offset=-1&X-Plex-Incomplete-Segments=1&X-Plex-Session-Identifier=${session}`); + console.log('Getting info about channel stream...'); + const res2 = await axios.get(url2, { + timeout: 60000 + }); + console.log(res2.data); + if (lodash.isNil(res2.data.MediaContainer.Metadata[0].Media[0].TranscodeSession)) { + console.log('NOT STARTED - Starting...'); + const res1 = await axios.get(this.authorizeURL(startURL), { + timeout: 60000 + }); + console.log(res1); + console.log('____'); + } + const sleep = async (ms) => { + return new Promise(resolve => setTimeout(resolve, ms)); + }; + console.log('Deciding...'); + let res = await axios.get(this.authorizeURL(decisionURL), { timeout: this.requestTimeout }); console.log(res); - console.log('Starting...'); - // why bad request??? - const res1 = await axios.get(this.authorizeURL(startURL), { - timeout: 60000 + console.log('Waiting for new url...'); + await sleep(10000); + res = await axios.get(this.authorizeURL(decisionURL), { + timeout: this.requestTimeout }); - console.log(res1); - console.log('____'); + console.log(res); return res.data.MediaContainer.Metadata[0].Media[0].Part[0].key; }; this.getContinueWatching = async () => { diff --git a/src/modules/Plex.ts b/src/modules/Plex.ts index e65e15d..8e46059 100644 --- a/src/modules/Plex.ts +++ b/src/modules/Plex.ts @@ -366,7 +366,7 @@ class Plex { }; tune = async (channelID: string, session: string): Promise => { - session = 'PlexMeetsHomeAssistant'; + session = 'PlexMeetsHomeAssistant3'; console.log(channelID); // Todo: what is 12? do we need to get this from somewhere and change? let url = this.authorizeURL( @@ -385,13 +385,11 @@ class Plex { }) ).data.MediaContainer; - console.log(url); - console.log(tuneData.MediaSubscription[0].MediaGrabOperation[0].Metadata.title); - console.log('___'); + console.log('Tuning started.'); let startURL = `${this.getBasicURL()}/video/:/transcode/universal/start.mpd`; startURL += `?hasMDE=1`; - startURL += `&path=${tuneData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key}`; + startURL += `&path=${encodeURIComponent(tuneData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key)}`; startURL += `&mediaIndex=0`; startURL += `&partIndex=0`; startURL += `&protocol=dash`; @@ -425,8 +423,6 @@ class Plex { startURL += `&X-Plex-Device-Screen-Resolution=1792x444%2C1792x1120`; startURL += `&X-Plex-Language=en-GB`; - console.log('Deciding...'); - let decisionURL = `${this.getBasicURL()}/video/:/transcode/universal/decision`; decisionURL += `?advancedSubtitles=text`; @@ -449,19 +445,44 @@ class Plex { decisionURL += `&X-Plex-Client-Identifier=${session}`; decisionURL += `&X-Plex-Platform=Android`; - const res = await axios.get(this.authorizeURL(decisionURL), { + const url2 = this.authorizeURL( + `${this.getBasicURL()}${ + tuneData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key + }?includeBandwidths=1&offset=-1&X-Plex-Incomplete-Segments=1&X-Plex-Session-Identifier=${session}` + ); + + console.log('Getting info about channel stream...'); + const res2 = await axios.get(url2, { + timeout: 60000 + }); + + console.log(res2.data); + + if (_.isNil(res2.data.MediaContainer.Metadata[0].Media[0].TranscodeSession)) { + console.log('NOT STARTED - Starting...'); + const res1 = await axios.get(this.authorizeURL(startURL), { + timeout: 60000 + }); + console.log(res1); + console.log('____'); + } + + const sleep = async (ms: number): Promise => { + return new Promise(resolve => setTimeout(resolve, ms)); + }; + + console.log('Deciding...'); + let res = await axios.get(this.authorizeURL(decisionURL), { timeout: this.requestTimeout }); console.log(res); - console.log('Starting...'); - - // why bad request??? - const res1 = await axios.get(this.authorizeURL(startURL), { - timeout: 60000 + console.log('Waiting for new url...'); + await sleep(10000); + res = await axios.get(this.authorizeURL(decisionURL), { + timeout: this.requestTimeout }); - console.log(res1); - console.log('____'); + console.log(res); return res.data.MediaContainer.Metadata[0].Media[0].Part[0].key; }; From 7635cc351c3390c4e035e63221155bd9567dcbc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Thu, 2 Sep 2021 21:30:32 +0200 Subject: [PATCH 05/16] More reliable stream URLs --- dist/plex-meets-homeassistant.js | 16 +++++++++------- src/modules/Plex.ts | 20 +++++++++++--------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index df18493..4820348 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -19033,13 +19033,15 @@ class Plex { let res = await axios.get(this.authorizeURL(decisionURL), { timeout: this.requestTimeout }); - console.log(res); - console.log('Waiting for new url...'); - await sleep(10000); - res = await axios.get(this.authorizeURL(decisionURL), { - timeout: this.requestTimeout - }); - console.log(res); + while (parseFloat(res.data.MediaContainer.Metadata[0].Media[0].Part[0].key.split('offset=')[1].split('&')[0]) < 10) { + // eslint-disable-next-line no-await-in-loop + await sleep(500); + // eslint-disable-next-line no-await-in-loop + res = await axios.get(this.authorizeURL(decisionURL), { + timeout: this.requestTimeout + }); + console.log('Waiting for new url...'); + } return res.data.MediaContainer.Metadata[0].Media[0].Part[0].key; }; this.getContinueWatching = async () => { diff --git a/src/modules/Plex.ts b/src/modules/Plex.ts index 8e46059..41f3a67 100644 --- a/src/modules/Plex.ts +++ b/src/modules/Plex.ts @@ -475,15 +475,17 @@ class Plex { let res = await axios.get(this.authorizeURL(decisionURL), { timeout: this.requestTimeout }); - console.log(res); - - console.log('Waiting for new url...'); - await sleep(10000); - res = await axios.get(this.authorizeURL(decisionURL), { - timeout: this.requestTimeout - }); - console.log(res); - + while ( + parseFloat(res.data.MediaContainer.Metadata[0].Media[0].Part[0].key.split('offset=')[1].split('&')[0]) < 10 + ) { + // eslint-disable-next-line no-await-in-loop + await sleep(500); + // eslint-disable-next-line no-await-in-loop + res = await axios.get(this.authorizeURL(decisionURL), { + timeout: this.requestTimeout + }); + console.log('Waiting for new url...'); + } return res.data.MediaContainer.Metadata[0].Media[0].Part[0].key; }; From 8e42b969f5d19dcc8c3374bbdcd83cae791879cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Thu, 2 Sep 2021 21:51:11 +0200 Subject: [PATCH 06/16] Update: Experimenting with android tv / bubbleupnp to get stream working --- dist/plex-meets-homeassistant.js | 40 +++++++++++++++-------------- src/modules/PlayController.ts | 43 ++++++++++++++++++-------------- 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index 4820348..d1663dc 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -19407,16 +19407,9 @@ class PlayController { case 'androidtv': if (!lodash.isNil(data.epg)) { const session = `${Math.floor(Date.now() / 1000)}`; - const streamData = await this.plex.tune(data.channelIdentifier, session); - console.log(streamData); - /* - await this.playViaAndroidTV( - entity.value, - streamData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key, - instantPlay, - provider - ); - */ + const streamLink = await this.plex.tune(data.channelIdentifier, session); + console.log(streamLink); + await this.playViaAndroidTV(entity.value, streamLink, instantPlay, provider); } else { await this.playViaAndroidTV(entity.value, processData.guid, instantPlay, provider); @@ -19430,7 +19423,7 @@ class PlayController { const session = `PlexMeetsHomeAssistant-${Math.floor(Date.now() / 1000)}`; const streamURL = await this.plex.tune(data.channelIdentifier, session); console.log(`${this.plex.getBasicURL()}${streamURL}`); - // this.playViaCast(entity.value, `${playlistLink}`); + this.playViaCast(entity.value, `${streamURL}`); } else if (this.hass.services.plex) { const libraryName = lodash.isNil(processData.librarySectionTitle) @@ -19593,22 +19586,31 @@ class PlayController { } }; this.playViaCast = (entityName, mediaLink) => { - console.log({ + mediaLink = this.plex.authorizeURL(`${this.plex.getBasicURL()}${mediaLink}`); + const payload = { // eslint-disable-next-line @typescript-eslint/camelcase entity_id: entityName, // eslint-disable-next-line @typescript-eslint/camelcase - media_content_type: 'video', + media_content_type: 'application/vnd.apple.mpegurl', // eslint-disable-next-line @typescript-eslint/camelcase - media_content_id: this.plex.authorizeURL(`${this.plex.getBasicURL()}${mediaLink}`) - }); - this.hass.callService('media_player', 'play_media', { + media_content_id: mediaLink + }; + /* + payload = { // eslint-disable-next-line @typescript-eslint/camelcase entity_id: entityName, // eslint-disable-next-line @typescript-eslint/camelcase - media_content_type: 'video', + media_content_type: 'cast', // eslint-disable-next-line @typescript-eslint/camelcase - media_content_id: this.plex.authorizeURL(`${this.plex.getBasicURL()}${mediaLink}`) - }); + media_content_id: `{ + "app_name": "bubbleupnp", + "media_id": "${mediaLink}", + "media_type": "application/x-mpegURL" + }` + }; + */ + console.log(payload); + this.hass.callService('media_player', 'play_media', payload); }; this.playViaCastPlex = (entityName, contentType, mediaLink) => { return this.hass.callService('media_player', 'play_media', { diff --git a/src/modules/PlayController.ts b/src/modules/PlayController.ts index 03ca7c2..ec85757 100644 --- a/src/modules/PlayController.ts +++ b/src/modules/PlayController.ts @@ -127,16 +127,10 @@ class PlayController { case 'androidtv': if (!_.isNil(data.epg)) { const session = `${Math.floor(Date.now() / 1000)}`; - const streamData = await this.plex.tune(data.channelIdentifier, session); - console.log(streamData); - /* - await this.playViaAndroidTV( - entity.value, - streamData.MediaSubscription[0].MediaGrabOperation[0].Metadata.key, - instantPlay, - provider - ); - */ + const streamLink = await this.plex.tune(data.channelIdentifier, session); + console.log(streamLink); + + await this.playViaAndroidTV(entity.value, streamLink, instantPlay, provider); } else { await this.playViaAndroidTV(entity.value, processData.guid, instantPlay, provider); } @@ -150,7 +144,7 @@ class PlayController { const session = `PlexMeetsHomeAssistant-${Math.floor(Date.now() / 1000)}`; const streamURL = await this.plex.tune(data.channelIdentifier, session); console.log(`${this.plex.getBasicURL()}${streamURL}`); - // this.playViaCast(entity.value, `${playlistLink}`); + this.playViaCast(entity.value, `${streamURL}`); } else if (this.hass.services.plex) { const libraryName = _.isNil(processData.librarySectionTitle) ? this.libraryName @@ -325,22 +319,33 @@ class PlayController { }; private playViaCast = (entityName: string, mediaLink: string): void => { - console.log({ + mediaLink = this.plex.authorizeURL(`${this.plex.getBasicURL()}${mediaLink}`); + const payload: any = { // eslint-disable-next-line @typescript-eslint/camelcase entity_id: entityName, // eslint-disable-next-line @typescript-eslint/camelcase - media_content_type: 'video', + media_content_type: 'application/vnd.apple.mpegurl', // eslint-disable-next-line @typescript-eslint/camelcase - media_content_id: this.plex.authorizeURL(`${this.plex.getBasicURL()}${mediaLink}`) - }); - this.hass.callService('media_player', 'play_media', { + media_content_id: mediaLink + }; + + /* + payload = { // eslint-disable-next-line @typescript-eslint/camelcase entity_id: entityName, // eslint-disable-next-line @typescript-eslint/camelcase - media_content_type: 'video', + media_content_type: 'cast', // eslint-disable-next-line @typescript-eslint/camelcase - media_content_id: this.plex.authorizeURL(`${this.plex.getBasicURL()}${mediaLink}`) - }); + media_content_id: `{ + "app_name": "bubbleupnp", + "media_id": "${mediaLink}", + "media_type": "application/x-mpegURL" + }` + }; + */ + + console.log(payload); + this.hass.callService('media_player', 'play_media', payload); }; private playViaCastPlex = (entityName: string, contentType: string, mediaLink: string): Promise => { From 30f26257f3957a406c66238819f77adb38ffd413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Thu, 2 Sep 2021 22:52:20 +0200 Subject: [PATCH 07/16] Add: Live TV support in Kodi --- dist/plex-meets-homeassistant.js | 20 ++++++++++++++++---- src/const.ts | 2 +- src/modules/PlayController.ts | 17 ++++++++++++++--- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index d1663dc..9863ff9 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -17204,7 +17204,7 @@ const CSS_STYLE = { minimumEpisodeWidth: 300 }; const supported = { - kodi: ['movie', 'episode'], + kodi: ['movie', 'episode', 'epg'], androidtv: ['movie', 'show', 'season', 'episode', 'clip', 'epg'], plexPlayer: ['movie', 'show', 'season', 'episode', 'clip'], cast: ['movie', 'episode', 'epg'] @@ -19402,7 +19402,7 @@ class PlayController { } switch (entity.key) { case 'kodi': - await this.playViaKodi(entity.value, processData, processData.type); + await this.playViaKodi(entity.value, data, processData.type); break; case 'androidtv': if (!lodash.isNil(data.epg)) { @@ -19548,7 +19548,18 @@ class PlayController { } }; this.playViaKodi = async (entityName, data, type) => { - if (type === 'movie') { + if (!lodash.isNil(lodash.get(data, 'epg.Media[0].channelCallSign'))) { + const streamLink = `${this.plex.getBasicURL()}${await this.plex.tune(data.channelIdentifier, 'todo')}`; + await this.hass.callService('kodi', 'call_method', { + // eslint-disable-next-line @typescript-eslint/camelcase + entity_id: entityName, + method: 'Player.Open', + item: { + file: streamLink + } + }); + } + else if (type === 'movie') { const kodiData = await this.getKodiSearch(data.title); await this.hass.callService('kodi', 'call_method', { // eslint-disable-next-line @typescript-eslint/camelcase @@ -19755,7 +19766,8 @@ class PlayController { hasKodiMediaSearchInstalled) || (!lodash.isEqual(this.runBefore, false) && hasKodiMediaSearchInstalled)); } - return false; + return true; // temp + // return false; }; this.isCastSupported = (entityName) => { return ((this.hass.states[entityName] && diff --git a/src/const.ts b/src/const.ts index 160edea..18a03f7 100644 --- a/src/const.ts +++ b/src/const.ts @@ -9,7 +9,7 @@ const CSS_STYLE: any = { }; const supported: any = { - kodi: ['movie', 'episode'], + kodi: ['movie', 'episode', 'epg'], androidtv: ['movie', 'show', 'season', 'episode', 'clip', 'epg'], plexPlayer: ['movie', 'show', 'season', 'episode', 'clip'], cast: ['movie', 'episode', 'epg'] diff --git a/src/modules/PlayController.ts b/src/modules/PlayController.ts index ec85757..097a570 100644 --- a/src/modules/PlayController.ts +++ b/src/modules/PlayController.ts @@ -122,7 +122,7 @@ class PlayController { } switch (entity.key) { case 'kodi': - await this.playViaKodi(entity.value, processData, processData.type); + await this.playViaKodi(entity.value, data, processData.type); break; case 'androidtv': if (!_.isNil(data.epg)) { @@ -279,7 +279,17 @@ class PlayController { }; private playViaKodi = async (entityName: string, data: Record, type: string): Promise => { - if (type === 'movie') { + if (!_.isNil(_.get(data, 'epg.Media[0].channelCallSign'))) { + const streamLink = `${this.plex.getBasicURL()}${await this.plex.tune(data.channelIdentifier, 'todo')}`; + await this.hass.callService('kodi', 'call_method', { + // eslint-disable-next-line @typescript-eslint/camelcase + entity_id: entityName, + method: 'Player.Open', + item: { + file: streamLink + } + }); + } else if (type === 'movie') { const kodiData = await this.getKodiSearch(data.title); await this.hass.callService('kodi', 'call_method', { // eslint-disable-next-line @typescript-eslint/camelcase @@ -528,7 +538,8 @@ class PlayController { (!_.isEqual(this.runBefore, false) && hasKodiMediaSearchInstalled) ); } - return false; + return true; // temp + // return false; }; private isCastSupported = (entityName: string): boolean => { From 7c7a260174bc626b91331d2629a7485ac423ccf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Thu, 2 Sep 2021 23:59:19 +0200 Subject: [PATCH 08/16] Add: Use native Kodi channel functionality if available --- dist/plex-meets-homeassistant.js | 41 ++++++++++++++++++++++---------- src/modules/PlayController.ts | 39 +++++++++++++++++++++--------- src/modules/Plex.ts | 4 +--- 3 files changed, 58 insertions(+), 26 deletions(-) diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index 9863ff9..a35e0c3 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -19033,7 +19033,7 @@ class Plex { let res = await axios.get(this.authorizeURL(decisionURL), { timeout: this.requestTimeout }); - while (parseFloat(res.data.MediaContainer.Metadata[0].Media[0].Part[0].key.split('offset=')[1].split('&')[0]) < 10) { + while (parseFloat(res.data.MediaContainer.Metadata[0].Media[0].Part[0].key.split('offset=')[1].split('&')[0]) < 3) { // eslint-disable-next-line no-await-in-loop await sleep(500); // eslint-disable-next-line no-await-in-loop @@ -19333,7 +19333,7 @@ class PlayController { this.getKodiSearchResults = async () => { return JSON.parse((await getState(this.hass, 'sensor.kodi_media_sensor_search')).attributes.data); }; - this.getKodiSearch = async (search) => { + this.getKodiSearch = async (search, silent = false) => { await this.hass.callService('kodi_media_sensors', 'call_method', { // eslint-disable-next-line @typescript-eslint/camelcase entity_id: 'sensor.kodi_media_sensor_search', @@ -19351,8 +19351,12 @@ class PlayController { foundResult = result; return false; } + if (lodash.isEqual(result.label, search)) { + foundResult = result; + return false; + } }); - if (lodash.isEmpty(foundResult)) { + if (lodash.isEmpty(foundResult) && !silent) { // eslint-disable-next-line no-alert alert(`Title ${search} not found in Kodi.`); throw Error(`Title ${search} not found in Kodi.`); @@ -19549,15 +19553,28 @@ class PlayController { }; this.playViaKodi = async (entityName, data, type) => { if (!lodash.isNil(lodash.get(data, 'epg.Media[0].channelCallSign'))) { - const streamLink = `${this.plex.getBasicURL()}${await this.plex.tune(data.channelIdentifier, 'todo')}`; - await this.hass.callService('kodi', 'call_method', { - // eslint-disable-next-line @typescript-eslint/camelcase - entity_id: entityName, - method: 'Player.Open', - item: { - file: streamLink - } - }); + try { + const kodiData = await this.getKodiSearch(lodash.get(data, 'epg.Media[0].channelCallSign'), true); + await this.hass.callService('kodi', 'call_method', { + // eslint-disable-next-line @typescript-eslint/camelcase + entity_id: entityName, + method: 'Player.Open', + item: { + channelid: kodiData.channelid + } + }); + } + catch (err) { + const streamLink = `${this.plex.getBasicURL()}${await this.plex.tune(data.channelIdentifier, 'todo')}`; + await this.hass.callService('kodi', 'call_method', { + // eslint-disable-next-line @typescript-eslint/camelcase + entity_id: entityName, + method: 'Player.Open', + item: { + file: streamLink + } + }); + } } else if (type === 'movie') { const kodiData = await this.getKodiSearch(data.title); diff --git a/src/modules/PlayController.ts b/src/modules/PlayController.ts index 097a570..4caba55 100644 --- a/src/modules/PlayController.ts +++ b/src/modules/PlayController.ts @@ -49,7 +49,7 @@ class PlayController { return JSON.parse((await getState(this.hass, 'sensor.kodi_media_sensor_search')).attributes.data); }; - private getKodiSearch = async (search: string): Promise> => { + private getKodiSearch = async (search: string, silent = false): Promise> => { await this.hass.callService('kodi_media_sensors', 'call_method', { // eslint-disable-next-line @typescript-eslint/camelcase entity_id: 'sensor.kodi_media_sensor_search', @@ -67,8 +67,12 @@ class PlayController { foundResult = result; return false; } + if (_.isEqual(result.label, search)) { + foundResult = result; + return false; + } }); - if (_.isEmpty(foundResult)) { + if (_.isEmpty(foundResult) && !silent) { // eslint-disable-next-line no-alert alert(`Title ${search} not found in Kodi.`); throw Error(`Title ${search} not found in Kodi.`); @@ -280,15 +284,28 @@ class PlayController { private playViaKodi = async (entityName: string, data: Record, type: string): Promise => { if (!_.isNil(_.get(data, 'epg.Media[0].channelCallSign'))) { - const streamLink = `${this.plex.getBasicURL()}${await this.plex.tune(data.channelIdentifier, 'todo')}`; - await this.hass.callService('kodi', 'call_method', { - // eslint-disable-next-line @typescript-eslint/camelcase - entity_id: entityName, - method: 'Player.Open', - item: { - file: streamLink - } - }); + try { + const kodiData = await this.getKodiSearch(_.get(data, 'epg.Media[0].channelCallSign'), true); + + await this.hass.callService('kodi', 'call_method', { + // eslint-disable-next-line @typescript-eslint/camelcase + entity_id: entityName, + method: 'Player.Open', + item: { + channelid: kodiData.channelid + } + }); + } catch (err) { + const streamLink = `${this.plex.getBasicURL()}${await this.plex.tune(data.channelIdentifier, 'todo')}`; + await this.hass.callService('kodi', 'call_method', { + // eslint-disable-next-line @typescript-eslint/camelcase + entity_id: entityName, + method: 'Player.Open', + item: { + file: streamLink + } + }); + } } else if (type === 'movie') { const kodiData = await this.getKodiSearch(data.title); await this.hass.callService('kodi', 'call_method', { diff --git a/src/modules/Plex.ts b/src/modules/Plex.ts index 41f3a67..78ca757 100644 --- a/src/modules/Plex.ts +++ b/src/modules/Plex.ts @@ -475,9 +475,7 @@ class Plex { let res = await axios.get(this.authorizeURL(decisionURL), { timeout: this.requestTimeout }); - while ( - parseFloat(res.data.MediaContainer.Metadata[0].Media[0].Part[0].key.split('offset=')[1].split('&')[0]) < 10 - ) { + while (parseFloat(res.data.MediaContainer.Metadata[0].Media[0].Part[0].key.split('offset=')[1].split('&')[0]) < 3) { // eslint-disable-next-line no-await-in-loop await sleep(500); // eslint-disable-next-line no-await-in-loop From 9d692f745db005857e0b98eb212b3c6d8b45baef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Fri, 3 Sep 2021 00:04:37 +0200 Subject: [PATCH 09/16] Fix: Cast functionality for video --- dist/plex-meets-homeassistant.js | 69 ++++++++++++++++++------------- src/modules/PlayController.ts | 70 ++++++++++++++++++-------------- src/modules/Plex.ts | 1 + 3 files changed, 82 insertions(+), 58 deletions(-) diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index a35e0c3..ec52f94 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -18943,6 +18943,7 @@ class Plex { })).data.MediaContainer; }; this.tune = async (channelID, session) => { + // eslint-disable-next-line no-param-reassign session = 'PlexMeetsHomeAssistant3'; console.log(channelID); // Todo: what is 12? do we need to get this from somewhere and change? @@ -19427,7 +19428,7 @@ class PlayController { const session = `PlexMeetsHomeAssistant-${Math.floor(Date.now() / 1000)}`; const streamURL = await this.plex.tune(data.channelIdentifier, session); console.log(`${this.plex.getBasicURL()}${streamURL}`); - this.playViaCast(entity.value, `${streamURL}`); + this.playViaCast(entity.value, `${streamURL}`, 'epg'); } else if (this.hass.services.plex) { const libraryName = lodash.isNil(processData.librarySectionTitle) @@ -19613,32 +19614,45 @@ class PlayController { throw Error(`Plex type ${type} is not supported in Kodi.`); } }; - this.playViaCast = (entityName, mediaLink) => { - mediaLink = this.plex.authorizeURL(`${this.plex.getBasicURL()}${mediaLink}`); - const payload = { - // eslint-disable-next-line @typescript-eslint/camelcase - entity_id: entityName, - // eslint-disable-next-line @typescript-eslint/camelcase - media_content_type: 'application/vnd.apple.mpegurl', - // eslint-disable-next-line @typescript-eslint/camelcase - media_content_id: mediaLink - }; - /* - payload = { - // eslint-disable-next-line @typescript-eslint/camelcase - entity_id: entityName, - // eslint-disable-next-line @typescript-eslint/camelcase - media_content_type: 'cast', - // eslint-disable-next-line @typescript-eslint/camelcase - media_content_id: `{ - "app_name": "bubbleupnp", - "media_id": "${mediaLink}", - "media_type": "application/x-mpegURL" - }` - }; - */ - console.log(payload); - this.hass.callService('media_player', 'play_media', payload); + this.playViaCast = (entityName, mediaLink, contentType = 'video') => { + if (lodash.isEqual(contentType, 'video')) { + this.hass.callService('media_player', 'play_media', { + // eslint-disable-next-line @typescript-eslint/camelcase + entity_id: entityName, + // eslint-disable-next-line @typescript-eslint/camelcase + media_content_type: 'video', + // eslint-disable-next-line @typescript-eslint/camelcase + media_content_id: this.plex.authorizeURL(`${this.plex.getBasicURL()}${mediaLink}`) + }); + } + else if (lodash.isEqual(contentType, 'epg')) { + // eslint-disable-next-line no-param-reassign + mediaLink = this.plex.authorizeURL(`${this.plex.getBasicURL()}${mediaLink}`); + const payload = { + // eslint-disable-next-line @typescript-eslint/camelcase + entity_id: entityName, + // eslint-disable-next-line @typescript-eslint/camelcase + media_content_type: 'application/vnd.apple.mpegurl', + // eslint-disable-next-line @typescript-eslint/camelcase + media_content_id: mediaLink + }; + /* + payload = { + // eslint-disable-next-line @typescript-eslint/camelcase + entity_id: entityName, + // eslint-disable-next-line @typescript-eslint/camelcase + media_content_type: 'cast', + // eslint-disable-next-line @typescript-eslint/camelcase + media_content_id: `{ + "app_name": "bubbleupnp", + "media_id": "${mediaLink}", + "media_type": "application/x-mpegURL" + }` + }; + */ + console.log(payload); + this.hass.callService('media_player', 'play_media', payload); + } }; this.playViaCastPlex = (entityName, contentType, mediaLink) => { return this.hass.callService('media_player', 'play_media', { @@ -19657,7 +19671,6 @@ class PlayController { command += ' --ez "android.intent.extra.START_PLAYBACK" true'; } command += ` -a android.intent.action.VIEW 'plex://server://${serverID}/${provider}${mediaID}'`; - console.log(command); this.hass.callService('androidtv', 'adb_command', { // eslint-disable-next-line @typescript-eslint/camelcase entity_id: entityName, diff --git a/src/modules/PlayController.ts b/src/modules/PlayController.ts index 4caba55..5254f88 100644 --- a/src/modules/PlayController.ts +++ b/src/modules/PlayController.ts @@ -148,7 +148,7 @@ class PlayController { const session = `PlexMeetsHomeAssistant-${Math.floor(Date.now() / 1000)}`; const streamURL = await this.plex.tune(data.channelIdentifier, session); console.log(`${this.plex.getBasicURL()}${streamURL}`); - this.playViaCast(entity.value, `${streamURL}`); + this.playViaCast(entity.value, `${streamURL}`, 'epg'); } else if (this.hass.services.plex) { const libraryName = _.isNil(processData.librarySectionTitle) ? this.libraryName @@ -345,34 +345,46 @@ class PlayController { } }; - private playViaCast = (entityName: string, mediaLink: string): void => { - mediaLink = this.plex.authorizeURL(`${this.plex.getBasicURL()}${mediaLink}`); - const payload: any = { - // eslint-disable-next-line @typescript-eslint/camelcase - entity_id: entityName, - // eslint-disable-next-line @typescript-eslint/camelcase - media_content_type: 'application/vnd.apple.mpegurl', - // eslint-disable-next-line @typescript-eslint/camelcase - media_content_id: mediaLink - }; - - /* - payload = { - // eslint-disable-next-line @typescript-eslint/camelcase - entity_id: entityName, - // eslint-disable-next-line @typescript-eslint/camelcase - media_content_type: 'cast', - // eslint-disable-next-line @typescript-eslint/camelcase - media_content_id: `{ - "app_name": "bubbleupnp", - "media_id": "${mediaLink}", - "media_type": "application/x-mpegURL" - }` - }; - */ + private playViaCast = (entityName: string, mediaLink: string, contentType = 'video'): void => { + if (_.isEqual(contentType, 'video')) { + this.hass.callService('media_player', 'play_media', { + // eslint-disable-next-line @typescript-eslint/camelcase + entity_id: entityName, + // eslint-disable-next-line @typescript-eslint/camelcase + media_content_type: 'video', + // eslint-disable-next-line @typescript-eslint/camelcase + media_content_id: this.plex.authorizeURL(`${this.plex.getBasicURL()}${mediaLink}`) + }); + } else if (_.isEqual(contentType, 'epg')) { + // eslint-disable-next-line no-param-reassign + mediaLink = this.plex.authorizeURL(`${this.plex.getBasicURL()}${mediaLink}`); + const payload: any = { + // eslint-disable-next-line @typescript-eslint/camelcase + entity_id: entityName, + // eslint-disable-next-line @typescript-eslint/camelcase + media_content_type: 'application/vnd.apple.mpegurl', + // eslint-disable-next-line @typescript-eslint/camelcase + media_content_id: mediaLink + }; - console.log(payload); - this.hass.callService('media_player', 'play_media', payload); + /* + payload = { + // eslint-disable-next-line @typescript-eslint/camelcase + entity_id: entityName, + // eslint-disable-next-line @typescript-eslint/camelcase + media_content_type: 'cast', + // eslint-disable-next-line @typescript-eslint/camelcase + media_content_id: `{ + "app_name": "bubbleupnp", + "media_id": "${mediaLink}", + "media_type": "application/x-mpegURL" + }` + }; + */ + + console.log(payload); + this.hass.callService('media_player', 'play_media', payload); + } }; private playViaCastPlex = (entityName: string, contentType: string, mediaLink: string): Promise => { @@ -401,8 +413,6 @@ class PlayController { command += ` -a android.intent.action.VIEW 'plex://server://${serverID}/${provider}${mediaID}'`; - console.log(command); - this.hass.callService('androidtv', 'adb_command', { // eslint-disable-next-line @typescript-eslint/camelcase entity_id: entityName, diff --git a/src/modules/Plex.ts b/src/modules/Plex.ts index 78ca757..419601e 100644 --- a/src/modules/Plex.ts +++ b/src/modules/Plex.ts @@ -366,6 +366,7 @@ class Plex { }; tune = async (channelID: string, session: string): Promise => { + // eslint-disable-next-line no-param-reassign session = 'PlexMeetsHomeAssistant3'; console.log(channelID); // Todo: what is 12? do we need to get this from somewhere and change? From 20d06ff7957d7060442418abe4a1108e9ca1b88e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Fri, 3 Sep 2021 00:05:34 +0200 Subject: [PATCH 10/16] Remove: EPG ability for cast/androidtv --- dist/plex-meets-homeassistant.js | 4 ++-- src/const.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index ec52f94..4dc7475 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -17205,9 +17205,9 @@ const CSS_STYLE = { }; const supported = { kodi: ['movie', 'episode', 'epg'], - androidtv: ['movie', 'show', 'season', 'episode', 'clip', 'epg'], + androidtv: ['movie', 'show', 'season', 'episode', 'clip'], plexPlayer: ['movie', 'show', 'season', 'episode', 'clip'], - cast: ['movie', 'episode', 'epg'] + cast: ['movie', 'episode'] }; var bind = function bind(fn, thisArg) { diff --git a/src/const.ts b/src/const.ts index 18a03f7..685bd6a 100644 --- a/src/const.ts +++ b/src/const.ts @@ -10,9 +10,9 @@ const CSS_STYLE: any = { const supported: any = { kodi: ['movie', 'episode', 'epg'], - androidtv: ['movie', 'show', 'season', 'episode', 'clip', 'epg'], + androidtv: ['movie', 'show', 'season', 'episode', 'clip'], plexPlayer: ['movie', 'show', 'season', 'episode', 'clip'], - cast: ['movie', 'episode', 'epg'] + cast: ['movie', 'episode'] }; const LOREM_IPSUM = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec semper risus vitae aliquet interdum. Nulla facilisi. Pellentesque viverra sagittis lorem eget aliquet. Cras vehicula, purus vel consectetur mattis, ipsum arcu ullamcorper mi, id viverra purus ex eu dolor. Integer vehicula lacinia sem convallis iaculis. Nulla fermentum erat interdum, efficitur felis in, mollis neque. Vivamus luctus metus eget nisl pellentesque, placerat elementum magna eleifend. From 02a23427100286c3dbc4866d9c140ad1bf7ebe0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Fri, 3 Sep 2021 00:07:55 +0200 Subject: [PATCH 11/16] Cleanup --- dist/plex-meets-homeassistant.js | 5 ++--- src/modules/PlayController.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index 4dc7475..b3c5a59 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -19417,7 +19417,7 @@ class PlayController { await this.playViaAndroidTV(entity.value, streamLink, instantPlay, provider); } else { - await this.playViaAndroidTV(entity.value, processData.guid, instantPlay, provider); + await this.playViaAndroidTV(entity.value, processData.key, instantPlay, provider); } break; case 'plexPlayer': @@ -19796,8 +19796,7 @@ class PlayController { hasKodiMediaSearchInstalled) || (!lodash.isEqual(this.runBefore, false) && hasKodiMediaSearchInstalled)); } - return true; // temp - // return false; + return false; }; this.isCastSupported = (entityName) => { return ((this.hass.states[entityName] && diff --git a/src/modules/PlayController.ts b/src/modules/PlayController.ts index 5254f88..d42f2f4 100644 --- a/src/modules/PlayController.ts +++ b/src/modules/PlayController.ts @@ -136,7 +136,7 @@ class PlayController { await this.playViaAndroidTV(entity.value, streamLink, instantPlay, provider); } else { - await this.playViaAndroidTV(entity.value, processData.guid, instantPlay, provider); + await this.playViaAndroidTV(entity.value, processData.key, instantPlay, provider); } break; @@ -565,8 +565,7 @@ class PlayController { (!_.isEqual(this.runBefore, false) && hasKodiMediaSearchInstalled) ); } - return true; // temp - // return false; + return false; }; private isCastSupported = (entityName: string): boolean => { From 736fa3e413a78a39484df2a71c64eda0df3bb98e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Fri, 3 Sep 2021 00:17:59 +0200 Subject: [PATCH 12/16] More cleanup --- dist/plex-meets-homeassistant.js | 10 ++++++---- src/modules/PlayController.ts | 6 +++--- src/plex-meets-homeassistant.ts | 4 +++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index b3c5a59..06a9a97 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -19407,7 +19407,7 @@ class PlayController { } switch (entity.key) { case 'kodi': - await this.playViaKodi(entity.value, data, processData.type); + await this.playViaKodi(entity.value, data, data.type); break; case 'androidtv': if (!lodash.isNil(data.epg)) { @@ -19553,9 +19553,9 @@ class PlayController { } }; this.playViaKodi = async (entityName, data, type) => { - if (!lodash.isNil(lodash.get(data, 'epg.Media[0].channelCallSign'))) { + if (type === 'epg') { try { - const kodiData = await this.getKodiSearch(lodash.get(data, 'epg.Media[0].channelCallSign'), true); + const kodiData = await this.getKodiSearch(lodash.get(data, 'channelCallSign'), true); await this.hass.callService('kodi', 'call_method', { // eslint-disable-next-line @typescript-eslint/camelcase entity_id: entityName, @@ -21452,6 +21452,9 @@ class PlexMeetsHomeAssistant extends HTMLElement { const liveTV = await this.plex.getLiveTV(); lodash.forEach(liveTV, (data, key) => { this.data[key] = data; + lodash.forEach(this.data[key], (value, innerKey) => { + this.data[key][innerKey].type = 'epg'; + }); }); } }; @@ -21491,7 +21494,6 @@ class PlexMeetsHomeAssistant extends HTMLElement { lodash.forEach(this.data[key], (libraryData, libraryKey) => { if (!lodash.isNil(this.epgData[key][libraryData.channelCallSign])) { this.data[key][libraryKey].epg = this.epgData[key][libraryData.channelCallSign]; - this.data[key][libraryKey].type = 'epg'; } }); }); diff --git a/src/modules/PlayController.ts b/src/modules/PlayController.ts index d42f2f4..80f62f5 100644 --- a/src/modules/PlayController.ts +++ b/src/modules/PlayController.ts @@ -126,7 +126,7 @@ class PlayController { } switch (entity.key) { case 'kodi': - await this.playViaKodi(entity.value, data, processData.type); + await this.playViaKodi(entity.value, data, data.type); break; case 'androidtv': if (!_.isNil(data.epg)) { @@ -283,9 +283,9 @@ class PlayController { }; private playViaKodi = async (entityName: string, data: Record, type: string): Promise => { - if (!_.isNil(_.get(data, 'epg.Media[0].channelCallSign'))) { + if (type === 'epg') { try { - const kodiData = await this.getKodiSearch(_.get(data, 'epg.Media[0].channelCallSign'), true); + const kodiData = await this.getKodiSearch(_.get(data, 'channelCallSign'), true); await this.hass.callService('kodi', 'call_method', { // eslint-disable-next-line @typescript-eslint/camelcase diff --git a/src/plex-meets-homeassistant.ts b/src/plex-meets-homeassistant.ts index 5d684b9..7648217 100644 --- a/src/plex-meets-homeassistant.ts +++ b/src/plex-meets-homeassistant.ts @@ -410,6 +410,9 @@ class PlexMeetsHomeAssistant extends HTMLElement { const liveTV = await this.plex.getLiveTV(); _.forEach(liveTV, (data, key) => { this.data[key] = data; + _.forEach(this.data[key], (value, innerKey) => { + this.data[key][innerKey].type = 'epg'; + }); }); } }; @@ -450,7 +453,6 @@ class PlexMeetsHomeAssistant extends HTMLElement { _.forEach(this.data[key], (libraryData, libraryKey) => { if (!_.isNil(this.epgData[key][libraryData.channelCallSign])) { this.data[key][libraryKey].epg = this.epgData[key][libraryData.channelCallSign]; - this.data[key][libraryKey].type = 'epg'; } }); }); From 415db4c4bd4333fb3ac7de88f26e66235d53afa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Fri, 3 Sep 2021 00:20:30 +0200 Subject: [PATCH 13/16] More cleanup --- dist/plex-meets-homeassistant.js | 6 +++--- src/modules/PlayController.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index 06a9a97..9f1b094 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -19401,7 +19401,7 @@ class PlayController { const entity = this.getPlayService(data); let processData = data; let provider; - if (!lodash.isNil(data.epg)) { + if (lodash.isEqual(data.type, 'epg')) { processData = data.epg; provider = ''; } @@ -19410,7 +19410,7 @@ class PlayController { await this.playViaKodi(entity.value, data, data.type); break; case 'androidtv': - if (!lodash.isNil(data.epg)) { + if (lodash.isEqual(data.type, 'epg')) { const session = `${Math.floor(Date.now() / 1000)}`; const streamLink = await this.plex.tune(data.channelIdentifier, session); console.log(streamLink); @@ -19424,7 +19424,7 @@ class PlayController { await this.playViaPlexPlayer(entity.value, processData.key.split('/')[3]); break; case 'cast': - if (!lodash.isNil(data.epg)) { + if (lodash.isEqual(data.type, 'epg')) { const session = `PlexMeetsHomeAssistant-${Math.floor(Date.now() / 1000)}`; const streamURL = await this.plex.tune(data.channelIdentifier, session); console.log(`${this.plex.getBasicURL()}${streamURL}`); diff --git a/src/modules/PlayController.ts b/src/modules/PlayController.ts index 80f62f5..0dc0193 100644 --- a/src/modules/PlayController.ts +++ b/src/modules/PlayController.ts @@ -120,7 +120,7 @@ class PlayController { let processData = data; let provider; - if (!_.isNil(data.epg)) { + if (_.isEqual(data.type, 'epg')) { processData = data.epg; provider = ''; } @@ -129,7 +129,7 @@ class PlayController { await this.playViaKodi(entity.value, data, data.type); break; case 'androidtv': - if (!_.isNil(data.epg)) { + if (_.isEqual(data.type, 'epg')) { const session = `${Math.floor(Date.now() / 1000)}`; const streamLink = await this.plex.tune(data.channelIdentifier, session); console.log(streamLink); @@ -144,7 +144,7 @@ class PlayController { await this.playViaPlexPlayer(entity.value, processData.key.split('/')[3]); break; case 'cast': - if (!_.isNil(data.epg)) { + if (_.isEqual(data.type, 'epg')) { const session = `PlexMeetsHomeAssistant-${Math.floor(Date.now() / 1000)}`; const streamURL = await this.plex.tune(data.channelIdentifier, session); console.log(`${this.plex.getBasicURL()}${streamURL}`); From 5683870a880d322918ad406c7977dba91e981970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Fri, 3 Sep 2021 00:21:41 +0200 Subject: [PATCH 14/16] More cleanup --- dist/plex-meets-homeassistant.js | 2 +- src/modules/PlayController.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index 9f1b094..e7c5d17 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -19620,7 +19620,7 @@ class PlayController { // eslint-disable-next-line @typescript-eslint/camelcase entity_id: entityName, // eslint-disable-next-line @typescript-eslint/camelcase - media_content_type: 'video', + media_content_type: contentType, // eslint-disable-next-line @typescript-eslint/camelcase media_content_id: this.plex.authorizeURL(`${this.plex.getBasicURL()}${mediaLink}`) }); diff --git a/src/modules/PlayController.ts b/src/modules/PlayController.ts index 0dc0193..83b391c 100644 --- a/src/modules/PlayController.ts +++ b/src/modules/PlayController.ts @@ -351,7 +351,7 @@ class PlayController { // eslint-disable-next-line @typescript-eslint/camelcase entity_id: entityName, // eslint-disable-next-line @typescript-eslint/camelcase - media_content_type: 'video', + media_content_type: contentType, // eslint-disable-next-line @typescript-eslint/camelcase media_content_id: this.plex.authorizeURL(`${this.plex.getBasicURL()}${mediaLink}`) }); From b796d1979b45a6648c977474456952ff4d1faf5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Fri, 3 Sep 2021 10:47:09 +0200 Subject: [PATCH 15/16] Update: Readme --- DETAILED_CONFIGURATION.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/DETAILED_CONFIGURATION.md b/DETAILED_CONFIGURATION.md index a3b639a..eccecf0 100644 --- a/DETAILED_CONFIGURATION.md +++ b/DETAILED_CONFIGURATION.md @@ -17,6 +17,8 @@ _Available special libraries:_ | Continue Watching | Shows movies and tv shows in progress, uses old Plex API. | | Deck | Shows tv shows on deck, uses old Plex API. | +You can also use Live TV library by specifying its name, usually "Live TV & DVR". + **entity**: You need to configure at least one supported media_player entity. - **androidtv**: Entity id of your media_player configured via [Android TV](https://www.home-assistant.io/integrations/androidtv/). See [detailed instructions](#android-tv-or-fire-tv). It is also possible to use short declaration with androidtv. @@ -28,6 +30,8 @@ _Available special libraries:_ **protocol**: _Optional_ Protocol to use for Plex. Defaults to "http". +**maxRows**: _Optional_ Maximum number of rows to display in card. Especially useful when using sidebar card with dock for example. + **maxCount**: _Optional_ Maximum number of items to display in card. **sort**: _Optional_ Define sort by. See [detailed instructions](#sorting) @@ -167,6 +171,8 @@ Play button is only visible if all the conditions inside Availability section of ✅ Episodes +❌ Live TV + ### Kodi **Difficulty to setup**: Moderate @@ -175,7 +181,7 @@ Play button is only visible if all the conditions inside Availability section of - Install and configure [PlexKodiConnect](https://github.com/croneter/PlexKodiConnect#download-and-installation) on Kodi itself. - Setup [Kodi](https://www.home-assistant.io/integrations/kodi/) integration for your device. -- Install and configure integration [Kodi Recently Added Media](https://github.com/jtbgroup/kodi-media-sensors#installation) and its sensor **kodi_media_sensor_search** +- Install and configure integration [Kodi Recently Added Media](https://github.com/jtbgroup/kodi-media-sensors#installation) and its sensor **kodi_media_sensor_search**. For support of Live TV, if [this PR](https://github.com/jtbgroup/kodi-media-sensors/pull/5) has not been merged yet, you need to use [this modified](https://github.com/JurajNyiri/kodi-media-sensors/tree/add_channels_search) integration with support for PVR.
Images of installation of Kodi Recently Added Media @@ -209,6 +215,8 @@ Play button is only visible if all the conditions inside Availability section of ✅ Episodes +✅ Live TV + ### Google Cast **Difficulty to setup**: Very easy @@ -235,6 +243,8 @@ Play button is only visible if all the conditions inside Availability section of ✅ Episodes +❌ Live TV + ### All other plex clients **Difficulty to setup**: Very Easy to Moderate @@ -296,6 +306,8 @@ entity: ✅ Episodes +❌ Live TV + **Shared Plex servers configuration** plexPlayer can be configured in multiple ways, achieving the same thing: From cd56b337fc4b216263498c926108d1e2beca6326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Fri, 3 Sep 2021 11:50:21 +0200 Subject: [PATCH 16/16] Update: Warning message about live tv --- dist/plex-meets-homeassistant.js | 2 +- src/editor.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index e7c5d17..3176f20 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -20289,7 +20289,7 @@ class PlexMeetsHomeAssistantEditor extends HTMLElement { libraryItems.appendChild(addDropdownItem('Live TV', true)); lodash.forEach(lodash.keys(this.livetv), (livetv) => { if (lodash.isEqual(this.config.libraryName, livetv)) { - warningLibrary.textContent = `Warning: ${this.config.libraryName} play action currently not supported by Plex.`; + warningLibrary.innerHTML = `Warning: ${this.config.libraryName} play action currently only supported with Kodi.
You might also need custom build of kodi-media-sensors, see detailed configuration for more information.`; } libraryItems.appendChild(addDropdownItem(livetv)); }); diff --git a/src/editor.ts b/src/editor.ts index 0904995..12f5414 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -530,7 +530,7 @@ class PlexMeetsHomeAssistantEditor extends HTMLElement { libraryItems.appendChild(addDropdownItem('Live TV', true)); _.forEach(_.keys(this.livetv), (livetv: string) => { if (_.isEqual(this.config.libraryName, livetv)) { - warningLibrary.textContent = `Warning: ${this.config.libraryName} play action currently not supported by Plex.`; + warningLibrary.innerHTML = `Warning: ${this.config.libraryName} play action currently only supported with Kodi.
You might also need custom build of kodi-media-sensors, see detailed configuration for more information.`; } libraryItems.appendChild(addDropdownItem(livetv)); });