diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index 90e4e0b..4b81ec2 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -18672,6 +18672,7 @@ class Plex { this.serverInfo = {}; this.clients = []; this.requestTimeout = 5000; + this.sections = []; this.init = async () => { await this.getClients(); /* @@ -18707,10 +18708,14 @@ class Plex { return this.serverInfo; }; this.getSections = async () => { - const url = this.authorizeURL(`${this.getBasicURL()}/library/sections`); - return (await axios.get(url, { - timeout: this.requestTimeout - })).data.MediaContainer.Directory; + if (lodash.isEmpty(this.sections)) { + const url = this.authorizeURL(`${this.getBasicURL()}/library/sections`); + const sectionsData = await axios.get(url, { + timeout: this.requestTimeout + }); + this.sections = sectionsData.data.MediaContainer.Directory; + } + return this.sections; }; this.getSectionsData = async () => { const sections = await this.getSections(); @@ -18724,6 +18729,13 @@ class Plex { }); return this.exportSectionsData(await Promise.all(sectionsRequests)); }; + this.getContinueWatching = async (sections) => { + const cleanedUpSections = sections.replace(' ', ''); + const url = this.authorizeURL(`${this.getBasicURL()}/hubs/continueWatching/items?contentDirectoryID=${cleanedUpSections}`); + return (await axios.get(url, { + timeout: this.requestTimeout + })).data; + }; this.getBasicURL = () => { return `${this.protocol}://${this.ip}:${this.port}`; }; @@ -19053,6 +19065,17 @@ const getOffset = (el) => { } return { top: y, left: x }; }; +const hasEpisodes = (media) => { + let result = false; + // eslint-disable-next-line consistent-return + lodash.forEach(media, data => { + if (lodash.isEqual(data.type, 'episode')) { + result = true; + return false; + } + }); + return result; +}; const isVideoFullScreen = (_this) => { const videoPlayer = _this.getElementsByClassName('videoPlayer')[0]; const video = videoPlayer.children[0]; @@ -19423,6 +19446,13 @@ style.textContent = css ` padding: 16px; display: none; } + .additionalElem { + color: hsla(0, 0%, 100%, 0.45); + position: relative; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } .ratingDetail { background: #ffffff24; padding: 5px 10px; @@ -19560,6 +19590,9 @@ style.textContent = css ` .yearElem { color: hsla(0, 0%, 100%, 0.45); position: relative; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } .toViewEpisode { position: relative; @@ -19824,12 +19857,14 @@ class PlexMeetsHomeAssistant extends HTMLElement { try { if (this.plex) { await this.plex.init(); + const continueWatching = await this.plex.getContinueWatching('3,5,6'); const [serverID, plexSections] = await Promise.all([this.plex.getServerID(), this.plex.getSectionsData()]); // eslint-disable-next-line @typescript-eslint/camelcase this.data.serverID = serverID; lodash.forEach(plexSections, section => { this.data[section.title1] = section.Metadata; }); + this.data.deck = continueWatching.MediaContainer.Metadata; if (this.data[this.config.libraryName] === undefined) { this.error = `Library name ${this.config.libraryName} does not exist.`; } @@ -19906,11 +19941,12 @@ class PlexMeetsHomeAssistant extends HTMLElement { // eslint-disable-next-line consistent-return let lastRowTop = 0; const loadAdditionalRowsCount = 2; // todo: make this configurable + const hasEpisodesResult = hasEpisodes(this.data[this.config.libraryName]); // eslint-disable-next-line consistent-return lodash.forEach(this.data[this.config.libraryName], (movieData) => { if ((!this.maxCount || this.renderedItems < this.maxCount) && (!this.maxRenderCount || this.renderedItems < this.maxRenderCount)) { - const movieElem = this.getMovieElement(movieData); + const movieElem = this.getMovieElement(movieData, hasEpisodesResult); let shouldRender = false; if (this.looseSearch) { let found = false; @@ -20695,12 +20731,24 @@ class PlexMeetsHomeAssistant extends HTMLElement { } return 0; }; - this.getMovieElement = (data) => { - const thumbURL = `${this.plexProtocol}://${this.config.ip}:${this.config.port}/photo/:/transcode?width=${CSS_STYLE.expandedWidth}&height=${CSS_STYLE.expandedHeight}&minSize=1&upscale=1&url=${data.thumb}&X-Plex-Token=${this.config.token}`; + this.getMovieElement = (data, hasAdditionalData = false) => { + console.log(data); + let thumbURL = ''; + if (lodash.isEqual(data.type, 'episode')) { + thumbURL = `${this.plexProtocol}://${this.config.ip}:${this.config.port}/photo/:/transcode?width=${CSS_STYLE.expandedWidth}&height=${CSS_STYLE.expandedHeight}&minSize=1&upscale=1&url=${data.grandparentThumb}&X-Plex-Token=${this.config.token}`; + } + else { + thumbURL = `${this.plexProtocol}://${this.config.ip}:${this.config.port}/photo/:/transcode?width=${CSS_STYLE.expandedWidth}&height=${CSS_STYLE.expandedHeight}&minSize=1&upscale=1&url=${data.thumb}&X-Plex-Token=${this.config.token}`; + } const container = document.createElement('div'); container.className = 'container'; container.style.width = `${CSS_STYLE.width}px`; - container.style.height = `${CSS_STYLE.height + 30}px`; + if (hasAdditionalData) { + container.style.height = `${CSS_STYLE.height + 50}px`; + } + else { + container.style.height = `${CSS_STYLE.height + 30}px`; + } const movieElem = document.createElement('div'); movieElem.className = 'movieElem'; movieElem.style.width = `${CSS_STYLE.width}px`; @@ -20738,15 +20786,31 @@ class PlexMeetsHomeAssistant extends HTMLElement { } }); const titleElem = document.createElement('div'); - titleElem.innerHTML = escapeHtml(data.title); + if (lodash.isEqual(data.type, 'episode')) { + titleElem.innerHTML = escapeHtml(data.grandparentTitle); + } + else { + titleElem.innerHTML = escapeHtml(data.title); + } titleElem.className = 'titleElem'; titleElem.style.marginTop = `${CSS_STYLE.height}px`; const yearElem = document.createElement('div'); - yearElem.innerHTML = escapeHtml(data.year); + if (lodash.isEqual(data.type, 'episode')) { + yearElem.innerHTML = escapeHtml(data.title); + } + else { + yearElem.innerHTML = escapeHtml(data.year); + } yearElem.className = 'yearElem'; + const additionalElem = document.createElement('div'); + if (lodash.isEqual(data.type, 'episode')) { + additionalElem.innerHTML = escapeHtml(`S${data.parentIndex} E${data.index}`); + additionalElem.className = 'additionalElem'; + } container.appendChild(movieElem); container.appendChild(titleElem); container.appendChild(yearElem); + container.appendChild(additionalElem); return container; }; this.loadCustomStyles = () => { diff --git a/src/modules/Plex.ts b/src/modules/Plex.ts index 6c7bfb1..aa7db3c 100644 --- a/src/modules/Plex.ts +++ b/src/modules/Plex.ts @@ -19,6 +19,8 @@ class Plex { sort: string; + sections: Array> = []; + constructor(ip: string, port = 32400, token: string, protocol: 'http' | 'https' = 'http', sort = 'titleSort:asc') { this.ip = ip; this.port = port; @@ -67,12 +69,14 @@ class Plex { }; getSections = async (): Promise => { - const url = this.authorizeURL(`${this.getBasicURL()}/library/sections`); - return ( - await axios.get(url, { + if (_.isEmpty(this.sections)) { + const url = this.authorizeURL(`${this.getBasicURL()}/library/sections`); + const sectionsData = await axios.get(url, { timeout: this.requestTimeout - }) - ).data.MediaContainer.Directory; + }); + this.sections = sectionsData.data.MediaContainer.Directory; + } + return this.sections; }; getSectionsData = async (): Promise => { @@ -90,6 +94,18 @@ class Plex { return this.exportSectionsData(await Promise.all(sectionsRequests)); }; + getContinueWatching = async (sections: string): Promise => { + const cleanedUpSections = sections.replace(' ', ''); + const url = this.authorizeURL( + `${this.getBasicURL()}/hubs/continueWatching/items?contentDirectoryID=${cleanedUpSections}` + ); + return ( + await axios.get(url, { + timeout: this.requestTimeout + }) + ).data; + }; + getBasicURL = (): string => { return `${this.protocol}://${this.ip}:${this.port}`; }; diff --git a/src/modules/style.ts b/src/modules/style.ts index 55a7d20..859079f 100644 --- a/src/modules/style.ts +++ b/src/modules/style.ts @@ -40,6 +40,13 @@ style.textContent = css` padding: 16px; display: none; } + .additionalElem { + color: hsla(0, 0%, 100%, 0.45); + position: relative; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } .ratingDetail { background: #ffffff24; padding: 5px 10px; @@ -177,6 +184,9 @@ style.textContent = css` .yearElem { color: hsla(0, 0%, 100%, 0.45); position: relative; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } .toViewEpisode { position: relative; diff --git a/src/modules/utils.ts b/src/modules/utils.ts index 2836926..835c5f7 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -43,6 +43,18 @@ const getOffset = (el: Element): Record => { return { top: y, left: x }; }; +const hasEpisodes = (media: Array>): boolean => { + let result = false; + // eslint-disable-next-line consistent-return + _.forEach(media, data => { + if (_.isEqual(data.type, 'episode')) { + result = true; + return false; + } + }); + return result; +}; + const isVideoFullScreen = (_this: any): boolean => { const videoPlayer = _this.getElementsByClassName('videoPlayer')[0] as HTMLElement; const video = videoPlayer.children[0] as any; @@ -160,4 +172,13 @@ const isScrolledIntoView = (elem: HTMLElement): boolean => { }; // eslint-disable-next-line import/prefer-default-export -export { escapeHtml, getOffset, isScrolledIntoView, getHeight, createEpisodesView, findTrailerURL, isVideoFullScreen }; +export { + escapeHtml, + getOffset, + isScrolledIntoView, + getHeight, + createEpisodesView, + findTrailerURL, + isVideoFullScreen, + hasEpisodes +}; diff --git a/src/plex-meets-homeassistant.ts b/src/plex-meets-homeassistant.ts index e84f52e..869fa3c 100644 --- a/src/plex-meets-homeassistant.ts +++ b/src/plex-meets-homeassistant.ts @@ -12,7 +12,8 @@ import { getHeight, createEpisodesView, findTrailerURL, - isVideoFullScreen + isVideoFullScreen, + hasEpisodes } from './modules/utils'; import style from './modules/style'; @@ -142,6 +143,7 @@ class PlexMeetsHomeAssistant extends HTMLElement { try { if (this.plex) { await this.plex.init(); + const continueWatching = await this.plex.getContinueWatching('3,5,6'); const [serverID, plexSections] = await Promise.all([this.plex.getServerID(), this.plex.getSectionsData()]); // eslint-disable-next-line @typescript-eslint/camelcase this.data.serverID = serverID; @@ -149,6 +151,8 @@ class PlexMeetsHomeAssistant extends HTMLElement { this.data[section.title1] = section.Metadata; }); + this.data.deck = continueWatching.MediaContainer.Metadata; + if (this.data[this.config.libraryName] === undefined) { this.error = `Library name ${this.config.libraryName} does not exist.`; } @@ -236,13 +240,14 @@ class PlexMeetsHomeAssistant extends HTMLElement { let lastRowTop = 0; const loadAdditionalRowsCount = 2; // todo: make this configurable + const hasEpisodesResult = hasEpisodes(this.data[this.config.libraryName]); // eslint-disable-next-line consistent-return _.forEach(this.data[this.config.libraryName], (movieData: Record) => { if ( (!this.maxCount || this.renderedItems < this.maxCount) && (!this.maxRenderCount || this.renderedItems < this.maxRenderCount) ) { - const movieElem = this.getMovieElement(movieData); + const movieElem = this.getMovieElement(movieData, hasEpisodesResult); let shouldRender = false; if (this.looseSearch) { let found = false; @@ -1117,13 +1122,23 @@ class PlexMeetsHomeAssistant extends HTMLElement { return 0; }; - getMovieElement = (data: any): HTMLDivElement => { - const thumbURL = `${this.plexProtocol}://${this.config.ip}:${this.config.port}/photo/:/transcode?width=${CSS_STYLE.expandedWidth}&height=${CSS_STYLE.expandedHeight}&minSize=1&upscale=1&url=${data.thumb}&X-Plex-Token=${this.config.token}`; + getMovieElement = (data: any, hasAdditionalData = false): HTMLDivElement => { + console.log(data); + let thumbURL = ''; + if (_.isEqual(data.type, 'episode')) { + thumbURL = `${this.plexProtocol}://${this.config.ip}:${this.config.port}/photo/:/transcode?width=${CSS_STYLE.expandedWidth}&height=${CSS_STYLE.expandedHeight}&minSize=1&upscale=1&url=${data.grandparentThumb}&X-Plex-Token=${this.config.token}`; + } else { + thumbURL = `${this.plexProtocol}://${this.config.ip}:${this.config.port}/photo/:/transcode?width=${CSS_STYLE.expandedWidth}&height=${CSS_STYLE.expandedHeight}&minSize=1&upscale=1&url=${data.thumb}&X-Plex-Token=${this.config.token}`; + } const container = document.createElement('div'); container.className = 'container'; container.style.width = `${CSS_STYLE.width}px`; - container.style.height = `${CSS_STYLE.height + 30}px`; + if (hasAdditionalData) { + container.style.height = `${CSS_STYLE.height + 50}px`; + } else { + container.style.height = `${CSS_STYLE.height + 30}px`; + } const movieElem = document.createElement('div'); movieElem.className = 'movieElem'; @@ -1172,17 +1187,32 @@ class PlexMeetsHomeAssistant extends HTMLElement { }); const titleElem = document.createElement('div'); - titleElem.innerHTML = escapeHtml(data.title); + if (_.isEqual(data.type, 'episode')) { + titleElem.innerHTML = escapeHtml(data.grandparentTitle); + } else { + titleElem.innerHTML = escapeHtml(data.title); + } titleElem.className = 'titleElem'; titleElem.style.marginTop = `${CSS_STYLE.height}px`; const yearElem = document.createElement('div'); - yearElem.innerHTML = escapeHtml(data.year); + if (_.isEqual(data.type, 'episode')) { + yearElem.innerHTML = escapeHtml(data.title); + } else { + yearElem.innerHTML = escapeHtml(data.year); + } yearElem.className = 'yearElem'; + const additionalElem = document.createElement('div'); + if (_.isEqual(data.type, 'episode')) { + additionalElem.innerHTML = escapeHtml(`S${data.parentIndex} E${data.index}`); + additionalElem.className = 'additionalElem'; + } + container.appendChild(movieElem); container.appendChild(titleElem); container.appendChild(yearElem); + container.appendChild(additionalElem); return container; };