diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index 6c5964d..e83cc03 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -18674,6 +18674,9 @@ class Plex { this.clients = []; this.requestTimeout = 10000; this.sections = []; + this.providers = []; + this.livetv = {}; + this.livetvepg = {}; this.collections = false; this.playlists = []; this.init = async () => { @@ -18692,6 +18695,67 @@ class Plex { } return this.clients; }; + this.getProviders = async () => { + if (lodash.isEmpty(this.providers)) { + const url = this.authorizeURL(`${this.getBasicURL()}/media/providers`); + const providersData = await axios.get(url, { + timeout: this.requestTimeout + }); + this.providers = providersData.data.MediaContainer.MediaProvider; + } + return this.providers; + }; + this.getLiveTV = async () => { + if (lodash.isEmpty(this.livetv)) { + const returnData = {}; + const providers = await this.getProviders(); + const liveTVRequests = []; + const liveTVRequestsNames = []; + lodash.forEach(providers, provider => { + if (lodash.isEqual(provider.protocols, 'livetv')) { + const url = this.authorizeURL(`${this.getBasicURL()}/${provider.identifier}/tags?type=310`); + liveTVRequests.push(axios.get(url, { + timeout: this.requestTimeout + })); + liveTVRequestsNames.push(provider.title); + } + }); + const allResults = await Promise.all(liveTVRequests); + lodash.forEach(allResults, (result, key) => { + returnData[liveTVRequestsNames[key]] = result.data.MediaContainer.Directory; + }); + this.livetv = returnData; + } + return this.livetv; + }; + this.getEPG = async () => { + if (lodash.isEmpty(this.livetvepg)) { + const returnData = {}; + const providers = await this.getProviders(); + const liveTVRequests = []; + const liveTVRequestsNames = []; + lodash.forEach(providers, provider => { + if (lodash.isEqual(provider.protocols, 'livetv')) { + let url = this.authorizeURL(`${this.getBasicURL()}/${provider.identifier}/grid?type=1&sort=beginsAt`); + url += `&endsAt>=${Math.floor(Date.now() / 1000)}`; + url += `&beginsAt<=${Math.floor(Date.now() / 1000)}`; + liveTVRequests.push(axios.get(url, { + timeout: this.requestTimeout + })); + liveTVRequestsNames.push(provider.title); + } + }); + const allResults = await Promise.all(liveTVRequests); + lodash.forEach(allResults, (result, key) => { + returnData[liveTVRequestsNames[key]] = {}; + lodash.forEach(result.data.MediaContainer.Metadata, data => { + returnData[liveTVRequestsNames[key]][data.Media[0].channelCallSign] = data; + }); + }); + this.livetvepg = returnData; + } + return this.livetvepg; + }; this.getServerID = async () => { if (lodash.isEmpty(this.serverInfo)) { await this.getServerInfo(); @@ -19228,26 +19292,34 @@ class PlayController { } } const entity = this.getPlayService(data); + let processData = data; + let provider; + if (!lodash.isNil(data.epg)) { + processData = data.epg; + provider = ''; + } switch (entity.key) { case 'kodi': - await this.playViaKodi(entity.value, data, data.type); + await this.playViaKodi(entity.value, processData, processData.type); break; case 'androidtv': - await this.playViaAndroidTV(entity.value, data.key.split('/')[3], instantPlay); + await this.playViaAndroidTV(entity.value, processData.key, instantPlay, provider); break; case 'plexPlayer': - await this.playViaPlexPlayer(entity.value, data.key.split('/')[3]); + await this.playViaPlexPlayer(entity.value, processData.key.split('/')[3]); break; case 'cast': if (this.hass.services.plex) { - const libraryName = lodash.isNil(data.librarySectionTitle) ? this.libraryName : data.librarySectionTitle; + const libraryName = lodash.isNil(processData.librarySectionTitle) + ? this.libraryName + : processData.librarySectionTitle; try { - switch (data.type) { + switch (processData.type) { case 'movie': await this.playViaCastPlex(entity.value, 'movie', `plex://${JSON.stringify({ // eslint-disable-next-line @typescript-eslint/camelcase library_name: libraryName, - title: data.title + title: processData.title })}`); break; case 'episode': @@ -19255,28 +19327,28 @@ class PlayController { // eslint-disable-next-line @typescript-eslint/camelcase library_name: libraryName, // eslint-disable-next-line @typescript-eslint/camelcase - show_name: data.grandparentTitle, + show_name: processData.grandparentTitle, // eslint-disable-next-line @typescript-eslint/camelcase - season_number: data.parentIndex, + season_number: processData.parentIndex, // eslint-disable-next-line @typescript-eslint/camelcase - episode_number: data.index + episode_number: processData.index })}`); break; default: - this.playViaCast(entity.value, data.Media[0].Part[0].key); + this.playViaCast(entity.value, processData.Media[0].Part[0].key); } } catch (err) { console.log(err); - this.playViaCast(entity.value, data.Media[0].Part[0].key); + this.playViaCast(entity.value, processData.Media[0].Part[0].key); } } else { - this.playViaCast(entity.value, data.Media[0].Part[0].key); + this.playViaCast(entity.value, processData.Media[0].Part[0].key); } break; default: - throw Error(`No service available to play ${data.title}!`); + throw Error(`No service available to play ${processData.title}!`); } if (lodash.isArray(this.runAfter)) { await this.hass.callService(this.runAfter[0], this.runAfter[1], {}); @@ -19417,13 +19489,13 @@ class PlayController { media_content_id: mediaLink }); }; - this.playViaAndroidTV = async (entityName, mediaID, instantPlay = false) => { + this.playViaAndroidTV = async (entityName, mediaID, instantPlay = false, provider = 'com.plexapp.plugins.library') => { const serverID = await this.plex.getServerID(); let command = `am start`; if (instantPlay) { command += ' --ez "android.intent.extra.START_PLAYBACK" true'; } - command += ` -a android.intent.action.VIEW 'plex://server://${serverID}/com.plexapp.plugins.library/library/metadata/${mediaID}'`; + command += ` -a android.intent.action.VIEW 'plex://server://${serverID}/${provider}${mediaID}'`; this.hass.callService('androidtv', 'adb_command', { // eslint-disable-next-line @typescript-eslint/camelcase entity_id: entityName, @@ -19611,6 +19683,7 @@ class PlexMeetsHomeAssistantEditor extends HTMLElement { this.entitiesRegistry = false; this.plexValidSection = document.createElement('div'); this.loaded = false; + this.livetv = {}; this.fireEvent = (node, type, detail, options = {}) => { // eslint-disable-next-line no-param-reassign detail = detail === null || detail === undefined ? {} : detail; @@ -19820,10 +19893,14 @@ class PlexMeetsHomeAssistantEditor extends HTMLElement { this.libraryName.appendChild(libraryItems); this.libraryName.style.width = '100%'; this.libraryName.addEventListener('value-changed', this.valueUpdated); + const warningLibrary = document.createElement('div'); + warningLibrary.style.color = 'red'; this.content.appendChild(this.libraryName); + this.content.appendChild(warningLibrary); this.appendChild(this.content); this.plex = new Plex(this.config.ip, this.plexPort, this.config.token, this.plexProtocol, this.config.sort); this.sections = await this.plex.getSections(); + this.livetv = await this.plex.getLiveTV(); this.collections = await this.plex.getCollections(); this.playlists = await this.plex.getPlaylists(); this.clients = await this.plex.getClients(); @@ -20033,6 +20110,15 @@ class PlexMeetsHomeAssistantEditor extends HTMLElement { this.runAfter.addEventListener('value-changed', this.valueUpdated); this.runAfter.value = this.config.runAfter; this.plexValidSection.appendChild(this.runAfter); + if (!lodash.isEmpty(this.livetv)) { + 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.`; + } + libraryItems.appendChild(addDropdownItem(livetv)); + }); + } if (!lodash.isEmpty(this.sections)) { libraryItems.appendChild(addDropdownItem('Libraries', true)); lodash.forEach(this.sections, (section) => { @@ -20911,6 +20997,7 @@ class PlexMeetsHomeAssistant extends HTMLElement { this.searchInputElem = document.createElement('input'); this.plexProtocol = 'http'; this.plexPort = false; + this.epgData = {}; this.detailsShown = false; this.entityRegistry = []; this.runBefore = ''; @@ -21185,6 +21272,19 @@ class PlexMeetsHomeAssistant extends HTMLElement { } } }; + const getLiveTV = async () => { + if (this.plex) { + const liveTV = await this.plex.getLiveTV(); + lodash.forEach(liveTV, (data, key) => { + this.data[key] = data; + }); + } + }; + const getEPG = async () => { + if (this.plex) { + this.epgData = await this.plex.getEPG(); + } + }; let sectionKey = 0; lodash.forEach(plexAllSections, (section) => { if (lodash.isEqual(section.title, this.config.libraryName)) { @@ -21209,7 +21309,17 @@ class PlexMeetsHomeAssistant extends HTMLElement { else if (lodash.isEqual(this.config.libraryName, 'Recently Added')) { loadDataRequests.push(getRecentyAdded()); } + loadDataRequests.push(getLiveTV()); + loadDataRequests.push(getEPG()); const [plexSections] = await Promise.all(loadDataRequests); + lodash.forEach(this.epgData, (value, key) => { + 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'; + } + }); + }); if (plexSections && sectionKey) { lodash.forEach(plexSections, section => { this.data[section.title1] = section.Metadata; @@ -21662,8 +21772,14 @@ class PlexMeetsHomeAssistant extends HTMLElement { this.activeMovieElem = undefined; for (let i = 0; i < this.movieElems.length; i += 1) { if (parseInt(this.movieElems[i].style.width, 10) > CSS_STYLE.width) { - this.movieElems[i].style.width = `${CSS_STYLE.width}px`; - this.movieElems[i].style.height = `${CSS_STYLE.height}px`; + if (lodash.isEqual(this.movieElems[i].style.width, this.movieElems[i].style.height)) { + this.movieElems[i].style.width = `${CSS_STYLE.width}px`; + this.movieElems[i].style.height = `${CSS_STYLE.width}px`; + } + else { + this.movieElems[i].style.width = `${CSS_STYLE.width}px`; + this.movieElems[i].style.height = `${CSS_STYLE.height}px`; + } this.movieElems[i].style['z-index'] = 1; this.movieElems[i].style.position = 'absolute'; this.movieElems[i].style.left = `${this.movieElems[i].dataset.left}px`; @@ -21810,8 +21926,21 @@ class PlexMeetsHomeAssistant extends HTMLElement { genreElem.parentElement.style.display = 'none'; } } - this.getElementsByClassName('detailsTitle')[0].innerHTML = escapeHtml(mainData.title); - this.getElementsByClassName('detailsYear')[0].innerHTML = escapeHtml(mainData.year); + if (!lodash.isNil(mainData.channelCallSign)) { + this.getElementsByClassName('detailsTitle')[0].innerHTML = escapeHtml(mainData.channelCallSign); + } + else { + this.getElementsByClassName('detailsTitle')[0].innerHTML = escapeHtml(mainData.title); + } + if (!lodash.isNil(mainData.year)) { + this.getElementsByClassName('detailsYear')[0].innerHTML = escapeHtml(mainData.year); + } + else if (!lodash.isNil(mainData.epg) && !lodash.isNil(mainData.epg.title)) { + this.getElementsByClassName('detailsYear')[0].innerHTML = escapeHtml(mainData.epg.title); + } + else { + this.getElementsByClassName('detailsYear')[0].innerHTML = ''; + } this.getElementsByClassName('metaInfo')[0].innerHTML = `${(mainData.duration !== undefined ? `${Math.round(parseInt(escapeHtml(mainData.duration), 10) / 60 / 1000)} min` : '') + @@ -21821,7 +21950,15 @@ class PlexMeetsHomeAssistant extends HTMLElement { (mainData.rating !== undefined ? `${mainData.rating < 5 ? '🗑' : '⭐'} ${Math.round(parseFloat(escapeHtml(mainData.rating)) * 10) / 10}` : '')}
`; - this.getElementsByClassName('detailDesc')[0].innerHTML = escapeHtml(mainData.summary); + if (!lodash.isNil(mainData.summary)) { + this.getElementsByClassName('detailDesc')[0].innerHTML = escapeHtml(mainData.summary); + } + else if (!lodash.isNil(mainData.epg) && !lodash.isNil(mainData.epg.summary)) { + this.getElementsByClassName('detailDesc')[0].innerHTML = escapeHtml(mainData.epg.summary); + } + else { + this.getElementsByClassName('detailDesc')[0].innerHTML = ''; + } /* todo temp disabled if (data.type === 'movie') { (this.detailElem.children[5] as HTMLElement).style.visibility = 'visible'; @@ -21843,97 +21980,100 @@ class PlexMeetsHomeAssistant extends HTMLElement { else if (data.childCount > 0) { seasonsData = await this.plex.getLibraryData(data.key.split('/')[3]); } - const dataDetails = await this.plex.getDetails(data.key.split('/')[3]); - if (this.videoElem) { - const art = this.plex.authorizeURL(this.plex.getBasicURL() + data.art); - const trailerURL = findTrailerURL(dataDetails); - if (trailerURL !== '' && !lodash.isEqual(this.playTrailer, false)) { - const videoPlayer = this.getElementsByClassName('videoPlayer')[0]; - const video = document.createElement('video'); - video.style.height = '100%'; - video.style.width = '100%'; - video.controls = false; - if (lodash.isEqual(this.playTrailer, 'muted')) { - video.muted = true; - } - const source = document.createElement('source'); - source.type = 'video/mp4'; - source.src = this.plex.authorizeURL(`${this.plex.getBasicURL()}${dataDetails.Extras.Metadata[0].Media[0].Part[0].key}`); - video.appendChild(source); - videoPlayer.appendChild(video); - video.load(); - video.play(); - let playingFired = false; - const videobgs1 = this.getElementsByClassName('videobg1'); - const videobgs2 = this.getElementsByClassName('videobg2'); - video.addEventListener('click', event => { - if (isVideoFullScreen(this)) { - event.stopPropagation(); + let dataDetails = {}; + if (!lodash.isNil(data.key)) { + dataDetails = await this.plex.getDetails(data.key.split('/')[3]); + if (this.videoElem) { + const art = this.plex.authorizeURL(this.plex.getBasicURL() + data.art); + const trailerURL = findTrailerURL(dataDetails); + if (trailerURL !== '' && !lodash.isEqual(this.playTrailer, false)) { + const videoPlayer = this.getElementsByClassName('videoPlayer')[0]; + const video = document.createElement('video'); + video.style.height = '100%'; + video.style.width = '100%'; + video.controls = false; + if (lodash.isEqual(this.playTrailer, 'muted')) { + video.muted = true; } - }); - const fullScreenChangeAction = () => { - if (this.videoElem) { + const source = document.createElement('source'); + source.type = 'video/mp4'; + source.src = this.plex.authorizeURL(`${this.plex.getBasicURL()}${dataDetails.Extras.Metadata[0].Media[0].Part[0].key}`); + video.appendChild(source); + videoPlayer.appendChild(video); + video.load(); + video.play(); + let playingFired = false; + const videobgs1 = this.getElementsByClassName('videobg1'); + const videobgs2 = this.getElementsByClassName('videobg2'); + video.addEventListener('click', event => { if (isVideoFullScreen(this)) { - // eslint-disable-next-line no-restricted-syntax - for (const videobg1 of videobgs1) { - videobg1.classList.add('transparent'); - } - // eslint-disable-next-line no-restricted-syntax - for (const videobg2 of videobgs2) { - videobg2.classList.add('transparent'); - } - this.videoElem.classList.add('maxZIndex'); - video.controls = true; - video.muted = false; + event.stopPropagation(); } - else { - // eslint-disable-next-line no-restricted-syntax - for (const videobg1 of videobgs1) { - videobg1.classList.remove('transparent'); - } - // eslint-disable-next-line no-restricted-syntax - for (const videobg2 of videobgs2) { - videobg2.classList.remove('transparent'); + }); + const fullScreenChangeAction = () => { + if (this.videoElem) { + if (isVideoFullScreen(this)) { + // eslint-disable-next-line no-restricted-syntax + for (const videobg1 of videobgs1) { + videobg1.classList.add('transparent'); + } + // eslint-disable-next-line no-restricted-syntax + for (const videobg2 of videobgs2) { + videobg2.classList.add('transparent'); + } + this.videoElem.classList.add('maxZIndex'); + video.controls = true; + video.muted = false; } - this.videoElem.classList.remove('maxZIndex'); - video.controls = false; - window.scroll({ - top: getOffset(this.activeMovieElem).top - 70, - behavior: 'smooth' - }); - if (lodash.isEqual(this.playTrailer, 'muted')) { - video.muted = true; + else { + // eslint-disable-next-line no-restricted-syntax + for (const videobg1 of videobgs1) { + videobg1.classList.remove('transparent'); + } + // eslint-disable-next-line no-restricted-syntax + for (const videobg2 of videobgs2) { + videobg2.classList.remove('transparent'); + } + this.videoElem.classList.remove('maxZIndex'); + video.controls = false; + window.scroll({ + top: getOffset(this.activeMovieElem).top - 70, + behavior: 'smooth' + }); + if (lodash.isEqual(this.playTrailer, 'muted')) { + video.muted = true; + } } } - } - }; - video.addEventListener('fullscreenchange', fullScreenChangeAction); - video.addEventListener('mozfullscreenchange', fullScreenChangeAction); - video.addEventListener('webkitfullscreenchange', fullScreenChangeAction); - video.addEventListener('msfullscreenchange', fullScreenChangeAction); - video.addEventListener('playing', () => { - if (this.videoElem && !playingFired) { - const contentbg = this.getElementsByClassName('contentbg')[0]; - const fullscreenTrailer = this.getElementsByClassName('detailPlayAction')[0]; - fullscreenTrailer.style.visibility = 'visible'; - contentbg.classList.add('no-transparency'); - playingFired = true; - this.videoElem.style.width = `${this.getElementsByClassName('searchContainer')[0].offsetWidth}px`; - this.videoElem.style.visibility = 'visible'; - this.videoElem.style.top = `${top}px`; - } - }); - } - else if (!lodash.isEmpty(art)) { - const contentArt = this.getElementsByClassName('contentArt')[0]; - const contentbg = this.getElementsByClassName('contentbg')[0]; - contentArt.style.width = `${window.innerWidth}px`; - contentArt.style.height = `${window.innerHeight}px`; - contentArt.style.backgroundImage = `url('${art}')`; - contentArt.style.top = `${top - 8}px`; - contentArt.style.transition = '0.5s'; - contentArt.style.display = 'block'; - contentbg.classList.add('no-transparency'); + }; + video.addEventListener('fullscreenchange', fullScreenChangeAction); + video.addEventListener('mozfullscreenchange', fullScreenChangeAction); + video.addEventListener('webkitfullscreenchange', fullScreenChangeAction); + video.addEventListener('msfullscreenchange', fullScreenChangeAction); + video.addEventListener('playing', () => { + if (this.videoElem && !playingFired) { + const contentbg = this.getElementsByClassName('contentbg')[0]; + const fullscreenTrailer = this.getElementsByClassName('detailPlayAction')[0]; + fullscreenTrailer.style.visibility = 'visible'; + contentbg.classList.add('no-transparency'); + playingFired = true; + this.videoElem.style.width = `${this.getElementsByClassName('searchContainer')[0].offsetWidth}px`; + this.videoElem.style.visibility = 'visible'; + this.videoElem.style.top = `${top}px`; + } + }); + } + else if (!lodash.isEmpty(art)) { + const contentArt = this.getElementsByClassName('contentArt')[0]; + const contentbg = this.getElementsByClassName('contentbg')[0]; + contentArt.style.width = `${window.innerWidth}px`; + contentArt.style.height = `${window.innerHeight}px`; + contentArt.style.backgroundImage = `url('${art}')`; + contentArt.style.top = `${top - 8}px`; + contentArt.style.transition = '0.5s'; + contentArt.style.display = 'block'; + contentbg.classList.add('no-transparency'); + } } } if (!lodash.isEmpty(seasonsData)) { @@ -22115,7 +22255,7 @@ class PlexMeetsHomeAssistant extends HTMLElement { } }); } - else if (this.showExtras) { + else if (this.showExtras && !lodash.isNil(dataDetails.Extras)) { const extras = dataDetails.Extras.Metadata; lodash.forEach(extras, extrasData => { if (this.episodesElem && this.playController && this.plex) { @@ -22175,8 +22315,14 @@ class PlexMeetsHomeAssistant extends HTMLElement { this.minimizeAll(); this.activeMovieElem = undefined; this.hideDetails(); - movieElemLocal.style.width = `${CSS_STYLE.width}px`; - movieElemLocal.style.height = `${CSS_STYLE.height}px`; + if (lodash.isEqual(movieElem.style.width, movieElem.style.height)) { + movieElemLocal.style.width = `${CSS_STYLE.width}px`; + movieElemLocal.style.height = `${CSS_STYLE.width}px`; + } + else { + movieElemLocal.style.width = `${CSS_STYLE.width}px`; + movieElemLocal.style.height = `${CSS_STYLE.height}px`; + } movieElemLocal.style.zIndex = '1'; movieElemLocal.style.top = `${movieElem.dataset.top}px`; movieElemLocal.style.left = `${movieElem.dataset.left}px`; @@ -22189,8 +22335,14 @@ class PlexMeetsHomeAssistant extends HTMLElement { const top = this.getTop(); this.showDetails(this.activeMovieElemData); this.showBackground(); - movieElemLocal.style.width = `${CSS_STYLE.expandedWidth}px`; - movieElemLocal.style.height = `${CSS_STYLE.expandedHeight}px`; + if (lodash.isEqual(movieElem.style.width, movieElem.style.height)) { + movieElemLocal.style.width = `${CSS_STYLE.expandedWidth}px`; + movieElemLocal.style.height = `${CSS_STYLE.expandedWidth}px`; + } + else { + movieElemLocal.style.width = `${CSS_STYLE.expandedWidth}px`; + movieElemLocal.style.height = `${CSS_STYLE.expandedHeight}px`; + } movieElemLocal.style.zIndex = '3'; movieElemLocal.style.left = '16px'; movieElemLocal.style.top = `${top + 16}px`; @@ -22229,10 +22381,20 @@ class PlexMeetsHomeAssistant extends HTMLElement { else { container.style.height = `${CSS_STYLE.height + 30}px`; } + if (!lodash.isNil(data.channelCallSign)) { + container.style.marginBottom = '50px'; + } const movieElem = document.createElement('div'); movieElem.className = 'movieElem'; movieElem.style.width = `${CSS_STYLE.width}px`; movieElem.style.height = `${CSS_STYLE.height}px`; + if (!lodash.isNil(data.channelCallSign)) { + movieElem.style.backgroundSize = '80%'; + movieElem.style.backgroundColor = 'rgba(0,0,0,0.2)'; + movieElem.style.backgroundPosition = 'center'; + container.style.height = container.style.width; + movieElem.style.height = `${CSS_STYLE.width}px`; + } movieElem.style.backgroundImage = `url('${thumbURL}')`; if (this.playController && !this.playController.isPlaySupported(data)) { movieElem.style.cursor = 'pointer'; @@ -22278,18 +22440,29 @@ class PlexMeetsHomeAssistant extends HTMLElement { if (lodash.isEqual(data.type, 'episode')) { titleElem.innerHTML = escapeHtml(data.grandparentTitle); } + else if (!lodash.isNil(data.channelCallSign)) { + titleElem.innerHTML = escapeHtml(data.channelCallSign); + } else { titleElem.innerHTML = escapeHtml(data.title); } titleElem.className = 'titleElem'; - titleElem.style.marginTop = `${CSS_STYLE.height}px`; + if (!lodash.isNil(data.channelCallSign)) { + titleElem.style.marginTop = `${CSS_STYLE.width}px`; + } + else { + titleElem.style.marginTop = `${CSS_STYLE.height}px`; + } const yearElem = document.createElement('div'); if (lodash.isEqual(data.type, 'episode')) { yearElem.innerHTML = escapeHtml(data.title); } - else { + else if (!lodash.isNil(data.year)) { yearElem.innerHTML = escapeHtml(data.year); } + else if (!lodash.isNil(data.epg)) { + yearElem.innerHTML = escapeHtml(data.epg.title); + } yearElem.className = 'yearElem'; const additionalElem = document.createElement('div'); if (lodash.isEqual(data.type, 'episode')) { diff --git a/src/editor.ts b/src/editor.ts index b25f844..0904995 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -72,6 +72,8 @@ class PlexMeetsHomeAssistantEditor extends HTMLElement { loaded = false; + livetv: Record = {}; + fireEvent = ( node: HTMLElement, type: string, @@ -294,12 +296,17 @@ class PlexMeetsHomeAssistantEditor extends HTMLElement { this.libraryName.appendChild(libraryItems); this.libraryName.style.width = '100%'; this.libraryName.addEventListener('value-changed', this.valueUpdated); + + const warningLibrary = document.createElement('div'); + warningLibrary.style.color = 'red'; this.content.appendChild(this.libraryName); + this.content.appendChild(warningLibrary); this.appendChild(this.content); this.plex = new Plex(this.config.ip, this.plexPort, this.config.token, this.plexProtocol, this.config.sort); this.sections = await this.plex.getSections(); + this.livetv = await this.plex.getLiveTV(); this.collections = await this.plex.getCollections(); this.playlists = await this.plex.getPlaylists(); this.clients = await this.plex.getClients(); @@ -519,6 +526,15 @@ class PlexMeetsHomeAssistantEditor extends HTMLElement { this.runAfter.value = this.config.runAfter; this.plexValidSection.appendChild(this.runAfter); + if (!_.isEmpty(this.livetv)) { + 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.`; + } + libraryItems.appendChild(addDropdownItem(livetv)); + }); + } if (!_.isEmpty(this.sections)) { libraryItems.appendChild(addDropdownItem('Libraries', true)); _.forEach(this.sections, (section: Record) => { diff --git a/src/modules/PlayController.ts b/src/modules/PlayController.ts index f63dc46..91e04ae 100644 --- a/src/modules/PlayController.ts +++ b/src/modules/PlayController.ts @@ -113,21 +113,30 @@ class PlayController { } } const entity = this.getPlayService(data); + + let processData = data; + let provider; + if (!_.isNil(data.epg)) { + processData = data.epg; + provider = ''; + } switch (entity.key) { case 'kodi': - await this.playViaKodi(entity.value, data, data.type); + await this.playViaKodi(entity.value, processData, processData.type); break; case 'androidtv': - await this.playViaAndroidTV(entity.value, data.key.split('/')[3], instantPlay); + await this.playViaAndroidTV(entity.value, processData.key, instantPlay, provider); break; case 'plexPlayer': - await this.playViaPlexPlayer(entity.value, data.key.split('/')[3]); + await this.playViaPlexPlayer(entity.value, processData.key.split('/')[3]); break; case 'cast': if (this.hass.services.plex) { - const libraryName = _.isNil(data.librarySectionTitle) ? this.libraryName : data.librarySectionTitle; + const libraryName = _.isNil(processData.librarySectionTitle) + ? this.libraryName + : processData.librarySectionTitle; try { - switch (data.type) { + switch (processData.type) { case 'movie': await this.playViaCastPlex( entity.value, @@ -135,7 +144,7 @@ class PlayController { `plex://${JSON.stringify({ // eslint-disable-next-line @typescript-eslint/camelcase library_name: libraryName, - title: data.title + title: processData.title })}` ); break; @@ -147,27 +156,27 @@ class PlayController { // eslint-disable-next-line @typescript-eslint/camelcase library_name: libraryName, // eslint-disable-next-line @typescript-eslint/camelcase - show_name: data.grandparentTitle, + show_name: processData.grandparentTitle, // eslint-disable-next-line @typescript-eslint/camelcase - season_number: data.parentIndex, + season_number: processData.parentIndex, // eslint-disable-next-line @typescript-eslint/camelcase - episode_number: data.index + episode_number: processData.index })}` ); break; default: - this.playViaCast(entity.value, data.Media[0].Part[0].key); + this.playViaCast(entity.value, processData.Media[0].Part[0].key); } } catch (err) { console.log(err); - this.playViaCast(entity.value, data.Media[0].Part[0].key); + this.playViaCast(entity.value, processData.Media[0].Part[0].key); } } else { - this.playViaCast(entity.value, data.Media[0].Part[0].key); + this.playViaCast(entity.value, processData.Media[0].Part[0].key); } break; default: - throw Error(`No service available to play ${data.title}!`); + throw Error(`No service available to play ${processData.title}!`); } if (_.isArray(this.runAfter)) { await this.hass.callService(this.runAfter[0], this.runAfter[1], {}); @@ -317,7 +326,12 @@ class PlayController { }); }; - private playViaAndroidTV = async (entityName: string, mediaID: number, instantPlay = false): Promise => { + private playViaAndroidTV = async ( + entityName: string, + mediaID: string, + instantPlay = false, + provider = 'com.plexapp.plugins.library' + ): Promise => { const serverID = await this.plex.getServerID(); let command = `am start`; @@ -325,7 +339,7 @@ class PlayController { command += ' --ez "android.intent.extra.START_PLAYBACK" true'; } - command += ` -a android.intent.action.VIEW 'plex://server://${serverID}/com.plexapp.plugins.library/library/metadata/${mediaID}'`; + command += ` -a android.intent.action.VIEW 'plex://server://${serverID}/${provider}${mediaID}'`; this.hass.callService('androidtv', 'adb_command', { // eslint-disable-next-line @typescript-eslint/camelcase diff --git a/src/modules/Plex.ts b/src/modules/Plex.ts index cfefa4e..2bea9bf 100644 --- a/src/modules/Plex.ts +++ b/src/modules/Plex.ts @@ -21,6 +21,12 @@ class Plex { sections: Array> = []; + providers: Array> = []; + + livetv: Record = {}; + + livetvepg: Record = {}; + collections: Array> | false = false; playlists: Array> = []; @@ -56,6 +62,74 @@ class Plex { return this.clients; }; + getProviders = async (): Promise => { + if (_.isEmpty(this.providers)) { + const url = this.authorizeURL(`${this.getBasicURL()}/media/providers`); + const providersData = await axios.get(url, { + timeout: this.requestTimeout + }); + this.providers = providersData.data.MediaContainer.MediaProvider; + } + return this.providers; + }; + + getLiveTV = async (): Promise> => { + if (_.isEmpty(this.livetv)) { + const returnData: Record = {}; + const providers = await this.getProviders(); + const liveTVRequests: Array> = []; + const liveTVRequestsNames: Array = []; + _.forEach(providers, provider => { + if (_.isEqual(provider.protocols, 'livetv')) { + const url = this.authorizeURL(`${this.getBasicURL()}/${provider.identifier}/tags?type=310`); + liveTVRequests.push( + axios.get(url, { + timeout: this.requestTimeout + }) + ); + liveTVRequestsNames.push(provider.title); + } + }); + const allResults = await Promise.all(liveTVRequests); + _.forEach(allResults, (result, key) => { + returnData[liveTVRequestsNames[key]] = result.data.MediaContainer.Directory; + }); + this.livetv = returnData; + } + return this.livetv; + }; + + getEPG = async (): Promise> => { + if (_.isEmpty(this.livetvepg)) { + const returnData: Record = {}; + const providers = await this.getProviders(); + const liveTVRequests: Array> = []; + const liveTVRequestsNames: Array = []; + _.forEach(providers, provider => { + if (_.isEqual(provider.protocols, 'livetv')) { + let url = this.authorizeURL(`${this.getBasicURL()}/${provider.identifier}/grid?type=1&sort=beginsAt`); + url += `&endsAt>=${Math.floor(Date.now() / 1000)}`; + url += `&beginsAt<=${Math.floor(Date.now() / 1000)}`; + liveTVRequests.push( + axios.get(url, { + timeout: this.requestTimeout + }) + ); + liveTVRequestsNames.push(provider.title); + } + }); + const allResults = await Promise.all(liveTVRequests); + _.forEach(allResults, (result, key) => { + returnData[liveTVRequestsNames[key]] = {}; + _.forEach(result.data.MediaContainer.Metadata, data => { + returnData[liveTVRequestsNames[key]][data.Media[0].channelCallSign] = data; + }); + }); + this.livetvepg = returnData; + } + return this.livetvepg; + }; + getServerID = async (): Promise => { if (_.isEmpty(this.serverInfo)) { await this.getServerInfo(); diff --git a/src/plex-meets-homeassistant.ts b/src/plex-meets-homeassistant.ts index 08ee42a..0a39bc0 100644 --- a/src/plex-meets-homeassistant.ts +++ b/src/plex-meets-homeassistant.ts @@ -35,6 +35,8 @@ class PlexMeetsHomeAssistant extends HTMLElement { plexPort: number | false = false; + epgData: Record = {}; + detailsShown = false; entityRegistry: Array> = []; @@ -403,6 +405,21 @@ class PlexMeetsHomeAssistant extends HTMLElement { } }; + const getLiveTV = async (): Promise => { + if (this.plex) { + const liveTV = await this.plex.getLiveTV(); + _.forEach(liveTV, (data, key) => { + this.data[key] = data; + }); + } + }; + + const getEPG = async (): Promise => { + if (this.plex) { + this.epgData = await this.plex.getEPG(); + } + }; + let sectionKey: number | false = 0; _.forEach(plexAllSections, (section: Record) => { if (_.isEqual(section.title, this.config.libraryName)) { @@ -425,7 +442,18 @@ class PlexMeetsHomeAssistant extends HTMLElement { loadDataRequests.push(getRecentyAdded()); } + loadDataRequests.push(getLiveTV()); + loadDataRequests.push(getEPG()); + const [plexSections] = await Promise.all(loadDataRequests); + _.forEach(this.epgData, (value, key) => { + _.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'; + } + }); + }); if (plexSections && sectionKey) { _.forEach(plexSections, section => { @@ -929,8 +957,14 @@ class PlexMeetsHomeAssistant extends HTMLElement { this.activeMovieElem = undefined; for (let i = 0; i < this.movieElems.length; i += 1) { if (parseInt(this.movieElems[i].style.width, 10) > CSS_STYLE.width) { - this.movieElems[i].style.width = `${CSS_STYLE.width}px`; - this.movieElems[i].style.height = `${CSS_STYLE.height}px`; + if (_.isEqual(this.movieElems[i].style.width, this.movieElems[i].style.height)) { + this.movieElems[i].style.width = `${CSS_STYLE.width}px`; + this.movieElems[i].style.height = `${CSS_STYLE.width}px`; + } else { + this.movieElems[i].style.width = `${CSS_STYLE.width}px`; + this.movieElems[i].style.height = `${CSS_STYLE.height}px`; + } + this.movieElems[i].style['z-index'] = 1; this.movieElems[i].style.position = 'absolute'; this.movieElems[i].style.left = `${this.movieElems[i].dataset.left}px`; @@ -1080,8 +1114,22 @@ class PlexMeetsHomeAssistant extends HTMLElement { genreElem.parentElement.style.display = 'none'; } } - (this.getElementsByClassName('detailsTitle')[0] as HTMLElement).innerHTML = escapeHtml(mainData.title); - (this.getElementsByClassName('detailsYear')[0] as HTMLElement).innerHTML = escapeHtml(mainData.year); + if (!_.isNil(mainData.channelCallSign)) { + (this.getElementsByClassName('detailsTitle')[0] as HTMLElement).innerHTML = escapeHtml( + mainData.channelCallSign + ); + } else { + (this.getElementsByClassName('detailsTitle')[0] as HTMLElement).innerHTML = escapeHtml(mainData.title); + } + + if (!_.isNil(mainData.year)) { + (this.getElementsByClassName('detailsYear')[0] as HTMLElement).innerHTML = escapeHtml(mainData.year); + } else if (!_.isNil(mainData.epg) && !_.isNil(mainData.epg.title)) { + (this.getElementsByClassName('detailsYear')[0] as HTMLElement).innerHTML = escapeHtml(mainData.epg.title); + } else { + (this.getElementsByClassName('detailsYear')[0] as HTMLElement).innerHTML = ''; + } + (this.getElementsByClassName('metaInfo')[0] as HTMLElement).innerHTML = `${(mainData.duration !== undefined ? `${Math.round( parseInt(escapeHtml(mainData.duration), 10) / 60 / 1000 @@ -1095,7 +1143,14 @@ class PlexMeetsHomeAssistant extends HTMLElement { parseFloat(escapeHtml(mainData.rating)) * 10 ) / 10}` : '')}
`; - (this.getElementsByClassName('detailDesc')[0] as HTMLElement).innerHTML = escapeHtml(mainData.summary); + + if (!_.isNil(mainData.summary)) { + (this.getElementsByClassName('detailDesc')[0] as HTMLElement).innerHTML = escapeHtml(mainData.summary); + } else if (!_.isNil(mainData.epg) && !_.isNil(mainData.epg.summary)) { + (this.getElementsByClassName('detailDesc')[0] as HTMLElement).innerHTML = escapeHtml(mainData.epg.summary); + } else { + (this.getElementsByClassName('detailDesc')[0] as HTMLElement).innerHTML = ''; + } /* todo temp disabled if (data.type === 'movie') { @@ -1118,105 +1173,108 @@ class PlexMeetsHomeAssistant extends HTMLElement { } else if (data.childCount > 0) { seasonsData = await this.plex.getLibraryData(data.key.split('/')[3]); } - const dataDetails = await this.plex.getDetails(data.key.split('/')[3]); - if (this.videoElem) { - const art = this.plex.authorizeURL(this.plex.getBasicURL() + data.art); - const trailerURL = findTrailerURL(dataDetails); - if (trailerURL !== '' && !_.isEqual(this.playTrailer, false)) { - const videoPlayer = this.getElementsByClassName('videoPlayer')[0] as HTMLElement; - const video = document.createElement('video'); - video.style.height = '100%'; - video.style.width = '100%'; - video.controls = false; - if (_.isEqual(this.playTrailer, 'muted')) { - video.muted = true; - } - const source = document.createElement('source'); - source.type = 'video/mp4'; - source.src = this.plex.authorizeURL( - `${this.plex.getBasicURL()}${dataDetails.Extras.Metadata[0].Media[0].Part[0].key}` - ); - video.appendChild(source); - videoPlayer.appendChild(video); - - video.load(); - video.play(); - let playingFired = false; - - const videobgs1 = this.getElementsByClassName('videobg1'); - const videobgs2 = this.getElementsByClassName('videobg2'); - video.addEventListener('click', event => { - if (isVideoFullScreen(this)) { - event.stopPropagation(); + let dataDetails: Record = {}; + if (!_.isNil(data.key)) { + dataDetails = await this.plex.getDetails(data.key.split('/')[3]); + if (this.videoElem) { + const art = this.plex.authorizeURL(this.plex.getBasicURL() + data.art); + const trailerURL = findTrailerURL(dataDetails); + if (trailerURL !== '' && !_.isEqual(this.playTrailer, false)) { + const videoPlayer = this.getElementsByClassName('videoPlayer')[0] as HTMLElement; + const video = document.createElement('video'); + video.style.height = '100%'; + video.style.width = '100%'; + video.controls = false; + if (_.isEqual(this.playTrailer, 'muted')) { + video.muted = true; } - }); - const fullScreenChangeAction = (): void => { - if (this.videoElem) { + const source = document.createElement('source'); + source.type = 'video/mp4'; + source.src = this.plex.authorizeURL( + `${this.plex.getBasicURL()}${dataDetails.Extras.Metadata[0].Media[0].Part[0].key}` + ); + video.appendChild(source); + videoPlayer.appendChild(video); + + video.load(); + video.play(); + let playingFired = false; + + const videobgs1 = this.getElementsByClassName('videobg1'); + const videobgs2 = this.getElementsByClassName('videobg2'); + video.addEventListener('click', event => { if (isVideoFullScreen(this)) { - // eslint-disable-next-line no-restricted-syntax - for (const videobg1 of videobgs1) { - videobg1.classList.add('transparent'); - } - // eslint-disable-next-line no-restricted-syntax - for (const videobg2 of videobgs2) { - videobg2.classList.add('transparent'); - } + event.stopPropagation(); + } + }); + const fullScreenChangeAction = (): void => { + if (this.videoElem) { + if (isVideoFullScreen(this)) { + // eslint-disable-next-line no-restricted-syntax + for (const videobg1 of videobgs1) { + videobg1.classList.add('transparent'); + } + // eslint-disable-next-line no-restricted-syntax + for (const videobg2 of videobgs2) { + videobg2.classList.add('transparent'); + } - this.videoElem.classList.add('maxZIndex'); - video.controls = true; - video.muted = false; - } else { - // eslint-disable-next-line no-restricted-syntax - for (const videobg1 of videobgs1) { - videobg1.classList.remove('transparent'); - } - // eslint-disable-next-line no-restricted-syntax - for (const videobg2 of videobgs2) { - videobg2.classList.remove('transparent'); - } + this.videoElem.classList.add('maxZIndex'); + video.controls = true; + video.muted = false; + } else { + // eslint-disable-next-line no-restricted-syntax + for (const videobg1 of videobgs1) { + videobg1.classList.remove('transparent'); + } + // eslint-disable-next-line no-restricted-syntax + for (const videobg2 of videobgs2) { + videobg2.classList.remove('transparent'); + } - this.videoElem.classList.remove('maxZIndex'); - video.controls = false; - window.scroll({ - top: getOffset(this.activeMovieElem as Element).top - 70, - behavior: 'smooth' - }); - if (_.isEqual(this.playTrailer, 'muted')) { - video.muted = true; + this.videoElem.classList.remove('maxZIndex'); + video.controls = false; + window.scroll({ + top: getOffset(this.activeMovieElem as Element).top - 70, + behavior: 'smooth' + }); + if (_.isEqual(this.playTrailer, 'muted')) { + video.muted = true; + } } } - } - }; - video.addEventListener('fullscreenchange', fullScreenChangeAction); - video.addEventListener('mozfullscreenchange', fullScreenChangeAction); - video.addEventListener('webkitfullscreenchange', fullScreenChangeAction); - video.addEventListener('msfullscreenchange', fullScreenChangeAction); - - video.addEventListener('playing', () => { - if (this.videoElem && !playingFired) { - const contentbg = this.getElementsByClassName('contentbg')[0] as HTMLElement; - const fullscreenTrailer = this.getElementsByClassName('detailPlayAction')[0] as HTMLElement; - fullscreenTrailer.style.visibility = 'visible'; - contentbg.classList.add('no-transparency'); - playingFired = true; - this.videoElem.style.width = `${ - (this.getElementsByClassName('searchContainer')[0] as HTMLElement).offsetWidth - }px`; - this.videoElem.style.visibility = 'visible'; - this.videoElem.style.top = `${top}px`; - } - }); - } else if (!_.isEmpty(art)) { - const contentArt = this.getElementsByClassName('contentArt')[0] as HTMLElement; - const contentbg = this.getElementsByClassName('contentbg')[0] as HTMLElement; - contentArt.style.width = `${window.innerWidth}px`; - contentArt.style.height = `${window.innerHeight}px`; - contentArt.style.backgroundImage = `url('${art}')`; - contentArt.style.top = `${top - 8}px`; - contentArt.style.transition = '0.5s'; - - contentArt.style.display = 'block'; - contentbg.classList.add('no-transparency'); + }; + video.addEventListener('fullscreenchange', fullScreenChangeAction); + video.addEventListener('mozfullscreenchange', fullScreenChangeAction); + video.addEventListener('webkitfullscreenchange', fullScreenChangeAction); + video.addEventListener('msfullscreenchange', fullScreenChangeAction); + + video.addEventListener('playing', () => { + if (this.videoElem && !playingFired) { + const contentbg = this.getElementsByClassName('contentbg')[0] as HTMLElement; + const fullscreenTrailer = this.getElementsByClassName('detailPlayAction')[0] as HTMLElement; + fullscreenTrailer.style.visibility = 'visible'; + contentbg.classList.add('no-transparency'); + playingFired = true; + this.videoElem.style.width = `${ + (this.getElementsByClassName('searchContainer')[0] as HTMLElement).offsetWidth + }px`; + this.videoElem.style.visibility = 'visible'; + this.videoElem.style.top = `${top}px`; + } + }); + } else if (!_.isEmpty(art)) { + const contentArt = this.getElementsByClassName('contentArt')[0] as HTMLElement; + const contentbg = this.getElementsByClassName('contentbg')[0] as HTMLElement; + contentArt.style.width = `${window.innerWidth}px`; + contentArt.style.height = `${window.innerHeight}px`; + contentArt.style.backgroundImage = `url('${art}')`; + contentArt.style.top = `${top - 8}px`; + contentArt.style.transition = '0.5s'; + + contentArt.style.display = 'block'; + contentbg.classList.add('no-transparency'); + } } } if (!_.isEmpty(seasonsData)) { @@ -1419,7 +1477,7 @@ class PlexMeetsHomeAssistant extends HTMLElement { this.episodesElem.append(createEpisodesView(this.playController, this.plex, episodeData)); } }); - } else if (this.showExtras) { + } else if (this.showExtras && !_.isNil(dataDetails.Extras)) { const extras = dataDetails.Extras.Metadata; _.forEach(extras, extrasData => { if (this.episodesElem && this.playController && this.plex) { @@ -1485,8 +1543,13 @@ class PlexMeetsHomeAssistant extends HTMLElement { this.minimizeAll(); this.activeMovieElem = undefined; this.hideDetails(); - movieElemLocal.style.width = `${CSS_STYLE.width}px`; - movieElemLocal.style.height = `${CSS_STYLE.height}px`; + if (_.isEqual(movieElem.style.width, movieElem.style.height)) { + movieElemLocal.style.width = `${CSS_STYLE.width}px`; + movieElemLocal.style.height = `${CSS_STYLE.width}px`; + } else { + movieElemLocal.style.width = `${CSS_STYLE.width}px`; + movieElemLocal.style.height = `${CSS_STYLE.height}px`; + } movieElemLocal.style.zIndex = '1'; movieElemLocal.style.top = `${movieElem.dataset.top}px`; movieElemLocal.style.left = `${movieElem.dataset.left}px`; @@ -1500,8 +1563,13 @@ class PlexMeetsHomeAssistant extends HTMLElement { const top = this.getTop(); this.showDetails(this.activeMovieElemData); this.showBackground(); - movieElemLocal.style.width = `${CSS_STYLE.expandedWidth}px`; - movieElemLocal.style.height = `${CSS_STYLE.expandedHeight}px`; + if (_.isEqual(movieElem.style.width, movieElem.style.height)) { + movieElemLocal.style.width = `${CSS_STYLE.expandedWidth}px`; + movieElemLocal.style.height = `${CSS_STYLE.expandedWidth}px`; + } else { + movieElemLocal.style.width = `${CSS_STYLE.expandedWidth}px`; + movieElemLocal.style.height = `${CSS_STYLE.expandedHeight}px`; + } movieElemLocal.style.zIndex = '3'; movieElemLocal.style.left = '16px'; movieElemLocal.style.top = `${top + 16}px`; @@ -1545,12 +1613,24 @@ class PlexMeetsHomeAssistant extends HTMLElement { } else { container.style.height = `${CSS_STYLE.height + 30}px`; } + if (!_.isNil(data.channelCallSign)) { + container.style.marginBottom = '50px'; + } const movieElem = document.createElement('div'); movieElem.className = 'movieElem'; movieElem.style.width = `${CSS_STYLE.width}px`; movieElem.style.height = `${CSS_STYLE.height}px`; + + if (!_.isNil(data.channelCallSign)) { + movieElem.style.backgroundSize = '80%'; + movieElem.style.backgroundColor = 'rgba(0,0,0,0.2)'; + movieElem.style.backgroundPosition = 'center'; + container.style.height = container.style.width; + movieElem.style.height = `${CSS_STYLE.width}px`; + } + movieElem.style.backgroundImage = `url('${thumbURL}')`; if (this.playController && !this.playController.isPlaySupported(data)) { movieElem.style.cursor = 'pointer'; @@ -1609,17 +1689,25 @@ class PlexMeetsHomeAssistant extends HTMLElement { const titleElem = document.createElement('div'); if (_.isEqual(data.type, 'episode')) { titleElem.innerHTML = escapeHtml(data.grandparentTitle); + } else if (!_.isNil(data.channelCallSign)) { + titleElem.innerHTML = escapeHtml(data.channelCallSign); } else { titleElem.innerHTML = escapeHtml(data.title); } titleElem.className = 'titleElem'; - titleElem.style.marginTop = `${CSS_STYLE.height}px`; + if (!_.isNil(data.channelCallSign)) { + titleElem.style.marginTop = `${CSS_STYLE.width}px`; + } else { + titleElem.style.marginTop = `${CSS_STYLE.height}px`; + } const yearElem = document.createElement('div'); if (_.isEqual(data.type, 'episode')) { yearElem.innerHTML = escapeHtml(data.title); - } else { + } else if (!_.isNil(data.year)) { yearElem.innerHTML = escapeHtml(data.year); + } else if (!_.isNil(data.epg)) { + yearElem.innerHTML = escapeHtml(data.epg.title); } yearElem.className = 'yearElem';