├── views ├── partials │ └── footer.ejs └── pages │ └── index.ejs ├── package.json ├── config ├── config.json └── sample config.json ├── app.js ├── README.md └── services └── plex.js /views/partials/footer.ejs: -------------------------------------------------------------------------------- 1 |

©Test Footer

-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plex-overlays", 3 | "version": "0.1.0", 4 | "dependencies": { 5 | "ejs": "^3.1.8", 6 | "express": "^4.18.1", 7 | "fs": "^0.0.1-security", 8 | "http": "^0.0.1-security", 9 | "multer": "^1.4.5-lts.1", 10 | "nodemon": "^2.0.20", 11 | "request": "^2.88.2", 12 | "xml2js": "^0.4.23" 13 | }, 14 | "scripts": { 15 | "start": "nodemon ./app.js" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plexToken": " in any of this>", 3 | "tvIp": "", 4 | "tvPort": , 5 | "plexServerIp": "", 6 | "webhookPort": , 7 | "tvdbToken": "" 8 | } -------------------------------------------------------------------------------- /config/sample config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plexToken": "xxipxusYqxxxxxxxxxxxx", //also referred to as X-Plex-Token - https://www.plexopedia.com/plex-media-server/general/plex-token/ 3 | "tvIp": "192.168.1.27", //open PiPup on your Android device, put that ip here, without the port or http prefix 4 | "tvPort": 7979, //open PiPup on your Android device, put the port here, without the ip or prefix 5 | "plexServerIp": "192.168.1.209", //this is the ip where you're running the node app, so whatever computer you're editing this on, not the TV ip. This has to match the webhook ip you use in Plex settings 6 | "webhookPort": 8400, //pick an open port on your computer, this has to match the webhook port you setup in Plex settings 7 | "tvdbToken": "xxe4979fe9421549e6exxxxxxxxxxxxxx" //get it here https://www.themoviedb.org/settings/api, this is the shorter v3 key, NOT the long v4 key 8 | } -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'), 2 | express = require('express'), 3 | multer = require('multer'), 4 | plex = require('./services/plex'), 5 | app = express(), 6 | http = require('http'), 7 | request = require('request'); 8 | 9 | app.use(express.static(__dirname+'/storage')); 10 | app.set('view engine', 'ejs'); 11 | config = JSON.parse(fs.readFileSync('./config/config.json')); 12 | upload = multer({ dest: './storage/multer/' }); 13 | 14 | let plexData; 15 | let nowPlaying = ''; 16 | 17 | 18 | 19 | /*Known issues and todos: 20 | - PiPup server seems to crash randomly, and needs to be manually restarted - we might be able to kick off the service and/or check status with ADB - do disable power/battery saving 21 | - config needs user/player info for TV - right now, if someone else is using the PMS on a different device (eg. mobile phone), their webhooks will trigger the overlays on the TV 22 | -- need to test with shared users vs guest vs local users to see where we need to detect...or maybe IP based 23 | - need to test with local auth/secure connection enforcement on/off - ideally should be able to run with 'secure connection (always)' enabled - need to add PMS ip to "List of IP addresses and networks that are allowed without auth" in Settings > Network 24 | - video (face-api.js, in progress) 25 | - last.fm + nowplaying notification (small) 26 | - not sure if we can do anything about 'plex pass only' 27 | - need a way to remove the overlay when it gets 'stuck' showing for whatever reason (pipup service crash, or plex webhook misfires/delays) 28 | */ 29 | 30 | 31 | function startRouter(){ 32 | 33 | app.get('/', (req, res) =>{ 34 | updatePage(res); 35 | }); 36 | 37 | 38 | app.post(['/plex', '/tasker'], upload.single('thumb'), async function (req, res, next) { 39 | switch(req.url){ 40 | case "/plex": 41 | console.log(timeStamp(),'=========================================================\n'); 42 | plexData = await plex.processPayload(config, req); 43 | //console.log('got back w/ :', plexData); 44 | plexData.tvdbToken = config.tvdbToken; 45 | //if we're on a new media, update overlay with new content 46 | if(plexData.title != nowPlaying){ //need to check with a movie 47 | nowPlaying = plexData.title; 48 | //setPageInfo 49 | console.log('Now Playing Change: ', nowPlaying); 50 | updatePage(res); 51 | } 52 | 53 | ///move all this somewhere else 54 | if(plexData.event == 'media.pause'){ 55 | postOverlay(9999); 56 | } 57 | else{ //might need to change to capture other events and filter...we don't want to send bunch of requests if user is ffwd'ing, etc. 58 | //remove overlay, contents don't matter, just want duration 0 59 | postOverlay(0); 60 | } 61 | break; 62 | } 63 | }); 64 | app.listen(config.webhookPort, config.plexServerIp); 65 | console.log('listening on '+config.plexServerIp+':'+config.webhookPort+'...'); 66 | 67 | 68 | } 69 | startRouter(); 70 | 71 | 72 | 73 | 74 | function updatePage(res){ 75 | res.render('pages/index',{ 76 | plexData: plexData 77 | }); 78 | } 79 | 80 | 81 | function timeStamp(){ 82 | var today = new Date(); 83 | var date = today.getFullYear()+'-'+(today.getMonth()+1)+'-'+today.getDate(); 84 | var time = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds(); 85 | return date+' '+time; 86 | } 87 | 88 | 89 | 90 | function postOverlay(dur){ 91 | var clientServerOptions = { 92 | uri: 'http://'+config.tvIp+':'+config.tvPort+'/notify', 93 | body: '{"duration": '+dur+', "position": 0, "backgroundColor": "#00ffffff", "media": { "web": { "uri": "http://'+config.plexServerIp+':'+config.webhookPort+'", "width": 1920, "height": 1080}}}', 94 | method: 'POST', 95 | headers: { 96 | 'Content-Type': 'application/json' 97 | } 98 | } 99 | //comment out this request to test locally 100 | request(clientServerOptions, function (error, response) { 101 | //console.log(error,response); 102 | return; 103 | }); 104 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebsUpTV 2 | 3 | ![](https://i.ibb.co/q5Hy7x4/foto-no-exif-1.jpg) 4 | ![](https://i.ibb.co/crkwTnF/foto-no-exif.jpg) 5 | 6 | [Reddit Community](https://www.reddit.com/r/WebsUpTV) 7 | 8 | A Node.js application that displays various web widgets over any Android TV OS application. 9 | 10 | State of development - the application is in early development and is not yet suggested for daily use, currently working through PlexUp integration 11 | 12 | ## Requirements 13 | - Windows PC - Tested with Windows 10, should work on other OSs running PMS, but not yet tested 14 | - Install [Node.js](https://nodejs.org/en/download/) 15 | - Install Node NPM 16 | - Download and configure WebsUpTV 17 | - Android TV - Tested with NVIDIA Shield TV (v2015) 18 | - Mouse higly recommended to access all features 19 | - Install [PiPupTransparent](https://github.com/my-ugly-code/PiPupTransparent/releases) - allow permission to 'Display over other apps'. 20 | 21 | ## Features 22 | - [PlexUp](https://github.com/my-ugly-code/WebsUpTV/blob/main/README.md#plexup) - shows actors/roles from your now-playing [Plex](https://play.google.com/store/apps/details?id=com.plexapp.android&hl=en_US&gl=US) media, either on-demand, or automatically when you pause. 23 | - Coming Next - Song Id, Sports, Weather, News, Security Cameras...what else should we add? 24 | 25 | ## Feature Requirements 26 | - PlexUp - PlexPass ($$), Plex Media Server, TMDB api key (free) 27 | 28 | ## Setup 29 | I'll try to get a better guide here soon, but here's the quick setup guide. If you get stuck, post in the subreddit and I'll try to help get you going. 30 | - Configure Plex 31 | - Plex Settings > Settings > Network - Check 'Enable Webhooks' (bottom of page), and set Secure connections to "Preferred" (top of page) - hoping we can find a way around this, but it's a must for now. You can add your TV (and any PCs you are dev'ing on) to the "List of IP addresses and networks that are allowed without auth" whitelist on the same page. I also disabled "Allow media deletion" when I set this, but those are optional. 32 | - Plex Settings > Webhooks - create a webhook in the format http://:8400/plex. Eg. http://192.168.1.209:8400/plex 33 | 34 | - Configure WebsUpTV 35 | - Pull this repo, or download and unzip. Open the config/config.json file in Notepad or another editor, complete the config file and save. There is a sample config file in the config folder, this includes some additional notes on how to set this up and what the end result should look like. 36 | - From the root directory of WebsUpTV, Open a PowerShell window (shift + right click), and run "npm install". If everything installs correctly, run "npm start" next. If this doesn't work, be sure you have NPM installed correctly. 37 | 38 | ## FAQ 39 | There's a list of FAQs [here](https://github.com/my-ugly-code/WebsUpTV/wiki/FAQ). This covers general questions and some setup troubleshooting. Don't see your answer here? Just ask! 40 | 41 | 42 | ### Misc Notes - odd bugs, how to use, troubleshoot, etc. 43 | - Please check back for updates often, there is no auto update feature currently in place. To update, you can save your config.json file for reference, but note the changelog as the config.json file will expand in future releases. 44 | - The overlay is currently triggered when you're playing media from YOUR library. This will not work with 'Plex Movies & TV', trailers, Live TV, etc. 45 | - The overlay will also trigger on your config'd TV if someone else is playing media on your server - you can stop the application to prevent this. It's a simple fix, but I wanted to get the working version in place for now. Player IP (your TV IP in the config file) is sent in the plex payload. 46 | - When actors are visible, use your mouse to hover over a card, click the button on the card back to display movie/TV credits for that actor. 47 | - If the overlay is 'stuck' on your TV, move the cursor to the top-left corner of your screen and click the red triange that appears. 48 | - If the overlay stops displaying, 1) you can stop the application in PowerShell by typing Ctrl+C a few times - run 'npm start' to restart. 49 | - It might be helpful to disable any power/battery saving on the PiPup Android application - done in Android settings. 50 | - You can view the overlay data on any other local browser - the urls are displayed in PowerShell when the application starts or restarts. 51 | - You can manually trigger the overlay by sending a http request to PiPup directly, totally optional - this can be done in apps like Button Mapper or Tasker. You can set the duration value to whatever you'd like, if you need to remove the overlay before the duration timer expires, use the red triangle (top-left corner) or send another request with duration set to 0. The first step of displaying the overlay is to clear any existing overlays. 52 | - The app uses no highly complex code - feel free to play around, make it look different, add more info. Show me what you've done and we can merge in anything cool that maintains existing functionality!! I wrote the application in Sublime Text, you don't need any fancy IDEs to play with these files. If you're trying to do something and can't figure it out, drop me a line and I can try to help or learn with you. 53 | - Bug reporting - if you have a reoccuring issue, and want to collect helpful information for debugging, please copy/paste your PowerShell output to a .txt file, then find/replace Plex and TMDB tokens and include that file with your report. Please do not share your Plex or other tokens, it's putting yourself at risk. 54 | - Want to help? https://github.com/my-ugly-code/WebsUpTV/issues there's stuff in there for all skill levels, even if you've never coded before :) 55 | 56 | -------------------------------------------------------------------------------- /services/plex.js: -------------------------------------------------------------------------------- 1 | parseString = require('xml2js').parseString; 2 | request = require('request'); //there's probably a way to not have this duplicated here and app.js...besides passing request into processPayload() 3 | 4 | 5 | var Plex = module.exports = { 6 | processPayload: async function(config, req) { 7 | //console.log('in with....'); 8 | //console.log(req.body); 9 | let usefulData = await cleanData(config, req); 10 | console.log('usefulData.type:', usefulData.type); 11 | if(usefulData.type == 'episode'){ //so we call that twice? need to sort out promises 12 | let gParent = await getParentMedia(usefulData.topKey); 13 | console.log('got parent: ', JSON.stringify(gParent)); 14 | //console.log(gParent.MediaContainer.$.librarySectionTitle) 15 | usefulData.parent = gParent; 16 | try{ 17 | usefulData.parentRec = gParent.MediaContainer.Directory[0].$; 18 | } 19 | catch(e){ 20 | console.log('did not get episode parent, ok if not episode'); 21 | } 22 | usefulData.roles = await getRolesFromParent(gParent); //want these to fit in like movie roles 23 | } 24 | console.log('returning: ', usefulData); 25 | return usefulData; 26 | }/*, 27 | somethingelse: function() { 28 | console.log('something else'); 29 | }*/ 30 | } 31 | 32 | 33 | 34 | function getRolesFromParent(parent){ 35 | let roles = []; 36 | return new Promise(resolve => { 37 | if(parent && parent.MediaContainer){ 38 | parent.MediaContainer.Directory[0].Role.forEach(function(role){ 39 | var pRole = role.$; 40 | pRole.randColor = colorGen() 41 | //console.log('ROLE:', role); 42 | roles.push(pRole); 43 | }); 44 | } 45 | 46 | //console.log('GOT PARENT ROLES: ', roles); 47 | resolve(roles); 48 | }); 49 | } 50 | 51 | 52 | function cleanData(config, req){ 53 | return new Promise(resolve => { 54 | let payload = JSON.parse(req.body.payload); 55 | console.log('PLEX WEBHOOK PAYLOAD::::: ',JSON.stringify(payload)); 56 | 57 | //for episodes, some things are pulled from parent/grandparent - eg. roles, art 58 | if(payload.Metadata.grandparentThumb == undefined){ 59 | payload.Metadata.grandparentThumb = payload.Metadata.thumb; 60 | } 61 | var usefulData = { 62 | roles: payload.Metadata.Role, //empty if type = episode 63 | title: payload.Metadata.title, 64 | rating: payload.Metadata.contentRating, 65 | summary: payload.Metadata.summary, 66 | posterurl: 'http://'+config.plexServerIp+':32400'+payload.Metadata.grandparentThumb+'?X-Plex-Token='+config.plexToken, 67 | type: payload.Metadata.type, //episode, clip (trailer), movie 68 | show: payload.Metadata.grandparentTitle, 69 | topKey: payload.Metadata.grandparentKey, 70 | event: payload.event, 71 | releaseDate: payload.Metadata.originallyAvailableAt 72 | }; 73 | 74 | 75 | resolve(usefulData); 76 | }); 77 | } 78 | 79 | 80 | 81 | 82 | function colorGen(){ 83 | return 'rgb('+getRandomInt(110,175)+' '+getRandomInt(110,175)+' '+getRandomInt(110,175)+')'; 84 | } 85 | function getRandomInt(min, max) { 86 | min = Math.ceil(min); 87 | max = Math.floor(max); 88 | return Math.floor(Math.random() * (max - min) + min); // The maximum is exclusive and the minimum is inclusive 89 | } 90 | 91 | 92 | 93 | //for type episode, need to look at grandparent, sample for media below 94 | // http://config.plexServerIp:32400/library/metadata/7744?X-Plex-Token=config.plexToken 95 | function getParentMedia(key){ 96 | return new Promise(resolve => { 97 | 98 | 99 | console.log('KEY???', key); ///should be in format /library/metadata/12345 100 | 101 | var clientServerOptions = { 102 | uri: 'http://'+config.plexServerIp+':32400'+key+'?X-Plex-Token='+config.plexToken, 103 | method: 'GET' 104 | } 105 | request(clientServerOptions, function (error, response, body) { 106 | parseString(body, function (err, result) { 107 | resolve(result); 108 | }); 109 | }); 110 | }); 111 | 112 | 113 | //console.log('test test test'); 114 | } 115 | 116 | 117 | 118 | 119 | 120 | 121 | ////SAMPLE EPISODE JSON///// 122 | /* 123 | { 124 | "event": "media.resume", 125 | "user": true, 126 | "owner": true, 127 | "Account": { 128 | "id": 924920, 129 | "thumb": "https://plex.tv/users/80xxxxxxxxxxxxxxad/avatar?c=16xxxxxxxxx54", 130 | "title": "Allison_Wonderland" 131 | }, 132 | "Server": { 133 | "title": "Plex Main", 134 | "uuid": "e90xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxe14" 135 | }, 136 | "Player": { 137 | "local": true, 138 | "publicAddress": "XXX.XXX.XXX.179", 139 | "title": "Chrome", 140 | "uuid": "lbxxxxxxxxxxxxxxxxxxxxxe5" 141 | }, 142 | "Metadata": { 143 | "librarySectionType": "show", 144 | "ratingKey": "8150", 145 | "key": "/library/metadata/8150", 146 | "parentRatingKey": "7751", 147 | "grandparentRatingKey": "7744", 148 | "guid": "plex://episode/60341406ae8595002c187924", 149 | "parentGuid": "plex://season/602e774fc96042002d099b4d", 150 | "grandparentGuid": "plex://show/5d9f40086fc551001ef8e688", 151 | "type": "episode", 152 | "title": "Palm Beach County, FL 11", 153 | "grandparentKey": "/library/metadata/7744", 154 | "parentKey": "/library/metadata/7751", 155 | "librarySectionTitle": "TV Shows", 156 | "librarySectionID": 3, 157 | "librarySectionKey": "/library/sections/3", 158 | "grandparentTitle": "Cops", 159 | "parentTitle": "Season 14", 160 | "contentRating": "TV-MA", 161 | "summary": "", 162 | "index": 1, 163 | "parentIndex": 14, 164 | "viewOffset": 336000, 165 | "viewCount": 1, 166 | "lastViewedAt": 1665187453, 167 | "thumb": "/library/metadata/8150/thumb/1633852677", 168 | "art": "/library/metadata/7744/art/1652409778", 169 | "parentThumb": "/library/metadata/7751/thumb/1633852676", 170 | "grandparentThumb": "/library/metadata/7744/thumb/1652409778", 171 | "grandparentArt": "/library/metadata/7744/art/1652409778", 172 | "grandparentTheme": "/library/metadata/7744/theme/1652409778", 173 | "duration": 1500000, 174 | "originallyAvailableAt": "2001-09-01", 175 | "addedAt": 1469122046, 176 | "updatedAt": 1633852677, 177 | "Guid": [ 178 | { 179 | "id": "tvdb://445575" 180 | } 181 | ] 182 | } 183 | } 184 | */ -------------------------------------------------------------------------------- /views/pages/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 298 | 299 | 300 | 301 |
302 |
303 | <% if (plexData && plexData.roles) { %> 304 | <% for(var i=0; i < plexData.roles.length && i < 51; i++) {%> 305 |
306 | 307 |
308 |
309 |
310 | 311 | 312 | <%= plexData.roles[i].tag.substring(0,1) %> 313 | 314 | 315 | 316 |
317 | 318 | <%= plexData.roles[i].tag %>
319 | 320 | <%= plexData.roles[i].role %> 321 | 322 |
323 |
324 |
325 | <%= plexData.roles[i].tag %>
326 |
View Details
327 |
328 | 329 | 330 |
331 |
332 |
333 | <% } %> 334 | <% } %> 335 |
336 |
337 |
338 | <% if (plexData) { %> 339 |
340 | 341 |
342 |
343 |
344 | <%= plexData.title %> 345 |
346 |
347 | <%= plexData.show %> 348 |
349 |
350 | 351 | 352 | <%= plexData.releaseDate %> 353 | 354 | 355 |
356 |
357 | <%= plexData.summary %> 358 | <% if(plexData.summary == '' && plexData.parentRec != undefined){ %> 359 | <%= plexData.parentRec.summary %> 360 | <% if(plexData.parentRec.summary == ''){ %> 361 | There is no summary provided for this episode or series. 362 | <% } %> 363 | <% } %> 364 |
365 |
366 | <% } %> 367 |
368 | 369 | 370 | 371 | 372 | 373 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | --------------------------------------------------------------------------------