From ac18481ad0470741a82c132c7a47d254ae9f0192 Mon Sep 17 00:00:00 2001 From: Chris Ham Date: Mon, 17 Sep 2018 15:01:12 -0700 Subject: [PATCH] track recently played videos, setactivity --- config.json | 31 ++++++++---------- fgfm.TODO | 18 +++++++++++ fgfm.js | 90 +++++++++++++++++++++++++++++++---------------------- 3 files changed, 84 insertions(+), 55 deletions(-) create mode 100755 fgfm.TODO diff --git a/config.json b/config.json index ddfc2b7..0f4c53f 100755 --- a/config.json +++ b/config.json @@ -641,7 +641,7 @@ "scale.x": 1280 }, "sceneItem": "16x9ph", - "length": 5322 + "length": 4786 }, { "id": "pb-ab", @@ -655,7 +655,7 @@ "scale.x": 1280 }, "sceneItem": "16x9ph", - "length": 4730 + "length": 4200 }, { "id": "pb-ad", @@ -669,7 +669,7 @@ "scale.x": 1280 }, "sceneItem": "16x9ph", - "length": 5084 + "length": 4555 }, { "id": "pb-any-nmg", @@ -683,7 +683,7 @@ "scale.x": 1280 }, "sceneItem": "16x9ph", - "length": 5715 + "length": 5190 }, { "id": "pb-any-no-eg", @@ -697,7 +697,7 @@ "scale.x": 1280 }, "sceneItem": "16x9ph", - "length": 2352 + "length": 1833 }, { "id": "pb-master-sword", @@ -753,30 +753,25 @@ "scale.x": 1280 }, "sceneItem": "16x9ph", - "length": 5250 - } - ], - "vodsToCrop": [ + "length": 4725 + }, { "id": "pb-100-nmg", "category": "Personal Best", "label": "Personal Best: 100% NMG (1:49:30) [2017-01-31]", "chatName": "100% NMG (PB)", - "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\100%\\2017-01-31-hundo-14930.mp4" + "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\100%\\2017-01-31-hundo-14930.mp4", + "sceneItem": "legacyph", + "length": 6598 }, { "id": "pb-dg", "category": "Personal Best", "label": "Personal Best: Defeat Ganon (13:41) [2017-02-20]", "chatName": "Defeat Ganon (PB)", - "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\defeat-ganon\\2017-02-20_12-55-40-defeat-ganon-1341.mp4" - }, - { - "id": "pb-low-nmg", - "category": "Personal Best", - "label": "Personal Best: Low% NMG (1:32:48) [2017-05-11]", - "chatName": "Low% NMG (PB)", - "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\low%-nmg-nsq\\2017-05-11 17-01-52-low%-13248.mkv" + "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\defeat-ganon\\2017-02-20_12-55-40-defeat-ganon-1341.mp4", + "sceneItem": "legacyph", + "length": 868 } ], "debug": false diff --git a/fgfm.TODO b/fgfm.TODO new file mode 100755 index 0000000..09b89f9 --- /dev/null +++ b/fgfm.TODO @@ -0,0 +1,18 @@ +TODO: + ☐ Room vid requests / import + ☐ Add random chance for room grind playlist to show for certain amount of time + ☐ modularize OBS and Twitch code + ☐ Rotating background images (leftside) + ☐ Stream alerts for chat + ☐ add memes to commercial scene + ☐ show commercials after a video length cap is hit -- show at conclusion of video + ☐ add $setcurrent support (to update text label through obs websocket instead of chat) + ☐ support for $pause + ☐ remove currently playing video from vote choices + ☐ restrict # of requests a user can have in the queue at once + ☐ Add cooldowns + ✔ remember the last X vids played, remove these from shuffle choices @done (18-09-17 14:34) + ☐ Start/stop stream automation + ☐ Move vods to their own config + ☐ Tool to output list of video ID's / descriptions + ☐ Support viewer skip voting \ No newline at end of file diff --git a/fgfm.js b/fgfm.js index 7e3b9c6..ec6e3fb 100755 --- a/fgfm.js +++ b/fgfm.js @@ -2,20 +2,6 @@ * FG.fm Automation */ -// @TODO: Room vid requests / import -// @TODO: modularize OBS and Twitch code -// @TODO: Rotating background images (leftside) -// @TODO: Stream alerts for chat -// @TODO: Add random chance for room grind playlist to show for certain amount of time -// @TODO: add memes to commercial scene -// @TODO: show commercials after a video length cap is hit -- show at conclusion of video -// @TODO: add $setcurrent support (to update text label through obs websocket instead of chat) -// @TODO: update PB vod lengths to cut off before credits -// @TODO: support for $pause -// @TODO: remove currently playing video from vote choices -// @TODO: restrict # of requests a user can have in the queue at once -// @TODO: add cooldowns - // Import modules const irc = require('irc'); const OBSWebSocket = require('obs-websocket-js'); @@ -24,9 +10,12 @@ const util = require('./lib/util'); // Read internal configuration let config = require('./config.json'); -let currentPlaylist = config.obs.defaultPlaylist; -let twitchChannel = config.twitch.channels[0].toLowerCase(); const snesGames = require('./conf/snesgames.json'); +const twitchChannel = config.twitch.channels[0].toLowerCase(); + +let videoQueue = recentlyPlayed = []; +let currentVideo; +let videoTimer; // Connect to OBS Websocket const obs = new OBSWebSocket(); @@ -45,7 +34,7 @@ obs.connect({ address: config.obs.websocket.address, password: config.obs.websoc // Listen for errors from OBS obs.on('error', err => { - console.error('OBS socket error:', err); + console.error(`OBS socket error: ${JSON.stringify(err)}`); }); // Initialize Twitch chat hooks @@ -84,6 +73,7 @@ const twitchInit = (config, obs) => { // Listen for specific commands from admins if (config.twitch.admins.includes(from) || from === config.twitch.username.toLowerCase()) { + // SHOW/HIDE SOURCE if (commandNoPrefix === 'show' || commandNoPrefix === 'hide') { let newVisibility = (commandNoPrefix === 'show'); @@ -118,7 +108,7 @@ const twitchInit = (config, obs) => { .catch(err => { twitchChat.say(to, JSON.stringify(err)); }); - + // TOGGLE SOURCE VISIBILITY } else if (commandNoPrefix === 't') { let target = commandParts[1] || false; if (!target) { @@ -143,6 +133,7 @@ const twitchInit = (config, obs) => { .catch(err => { twitchChat.say(to, JSON.stringify(err)); }); + // SWAP -- Hide one source, show another } else if (commandNoPrefix === 'swap') { // hide first argument, show second argument let targetToHide = commandParts[1] || false; @@ -157,9 +148,8 @@ const twitchInit = (config, obs) => { obs.setSceneItemProperties({"item": targetToShow, "visible": true}); }) .catch(console.error); + // Black Box "Everybody Wow" "commercial" } else if (commandNoPrefix === 'auw') { - // @TODO: switch to 'commercial' scene and show appropriate items, then switch back - // this way, playing a commercial doesn't have to know what's playing in the other scene obs.setCurrentScene({"scene-name": "commercial"}) .then(res => { // show the video @@ -185,7 +175,7 @@ const twitchInit = (config, obs) => { }, 248000); }) .catch(console.error); - + // SWITCH SCENES } else if (commandNoPrefix === 'switch') { let target = commandParts[1] || false; @@ -205,6 +195,21 @@ const twitchInit = (config, obs) => { } }) .catch(console.error); + // SET ON-SCREEN ACTIVITY + } else if (commandNoPrefix === 'setactivity') { + let target = commandParts.slice(1).join(' '); + if (!target) { + twitchChat.say(to, `Please provide a new activity`); + return; + } + + obs.setTextGDIPlusProperties({"source": config.currentActivitySceneItemName, "scene-name": config.videoSceneName, "render": true, "text": target}) + .then(res => { + twitchChat.say(to, `Activity updated!`); + return; + }) + .catch(console.error); + // REBOOT } else if (commandNoPrefix === 'reboot') { console.log('Received request from admin to reboot...'); twitchChat.say(to, 'Rebooting...'); @@ -258,34 +263,44 @@ const twitchInit = (config, obs) => { const streamInit = (config, obs, twitch) => { return new Promise((resolve, reject) => { console.log(`Setting up initial video queue...`); - let videoQueue = config.vods.sort( () => { return 0.5 - Math.random() } ).slice(0, config.initialQueueSize); + videoQueue = config.vods.sort( () => { return 0.5 - Math.random() } ).slice(0, config.initialQueueSize); console.log(`Initial queue: ${videoQueue.map((c, i) => `[${i+1}] ${c.chatName}`).join(' | ')}`); - let currentVideo = videoQueue.shift(); - let videoTimer; + currentVideo = videoQueue.shift(); + // Pick the next video in the queue (or shuffle if queue is empty) const nextVideo = () => { + // add currentVideo.id to recentlyPlayed list, remove oldest video if cap is hit + if (recentlyPlayed.length === 3) { + recentlyPlayed.shift(); + } + recentlyPlayed.push(currentVideo.id); + + // @TODO: Add a random chance here for room grind to be played for an amount of time + + // play the next video in the queue, or pick one at random if the queue is empty if (videoQueue.length > 0) { currentVideo = videoQueue.shift(); - //console.log(`Playing next video in queue: ${JSON.stringify(currentVideo)}`); } else { - currentVideo = config.vods.sort( () => { return 0.5 - Math.random() } ).slice(0, 1).shift(); - //console.log(`Queue is empty, vod chosen at random for shuffle: ${JSON.stringify(currentVideo)}`); + // filter recently played from shuffle + let freshVods = config.vods.filter(e => { + return !recentlyPlayed.includes(e.id); + }); + currentVideo = freshVods.sort( () => { return 0.5 - Math.random() } ).slice(0, 1).shift(); } showVideo(currentVideo); }; + // Show a video and hide it when finished const showVideo = video => { - //console.log(`Showing video: ${JSON.stringify(video)}`); + console.log(`Showing video: ${video.chatName}`); // set the file path obs.setSourceSettings({"sourceName": video.sceneItem, "sourceSettings": {"local_file": video.filePath}}) .then(data => { // show the video - //console.log('local_file updated'); return obs.setSceneItemProperties({"item": video.sceneItem, "scene-name": config.videoSceneName, "visible": true}); }) .then(data => { - //console.log('scene item shown'); // update activity label and show/hide appropriately if (video.label !== false) { return obs.setTextGDIPlusProperties({"source": config.currentActivitySceneItemName, "scene-name": config.videoSceneName, "render": true, "text": video.label}); @@ -294,8 +309,7 @@ const streamInit = (config, obs, twitch) => { } }) .then(data => { - //console.log('activity label updated'); - // Set a timeout for hiding this at the end of the video and play the next video + // hide this video when it's finished and play the next video videoTimer = setTimeout(() => { obs.setSceneItemProperties({"item": video.sceneItem, "scene-name": config.videoSceneName, "visible": false}) .then(data => { @@ -306,7 +320,6 @@ const streamInit = (config, obs, twitch) => { .catch(console.error); }; - console.log(`Showing first video: ${currentVideo.chatName}`); showVideo(currentVideo); console.log(`Initializing stream timers...`); @@ -378,6 +391,7 @@ const streamInit = (config, obs, twitch) => { rtvInterval = setInterval(() => {rockTheVote()}, 300000); }); + // Twitch Chat Commands for Video Queue Control twitch.botChat.addListener('message', (from, to, message) => { // Ignore everything from blacklisted users if (config.twitch.blacklistedUsers.includes(from)) return; @@ -391,7 +405,6 @@ const streamInit = (config, obs, twitch) => { if (config.twitch.admins.includes(from) || from === config.twitch.username.toLowerCase()) { // SKIP if (commandNoPrefix === 'skip') { - //console.log(`admin is skipping video: ${JSON.stringify(currentVideo)}`); clearTimeout(videoTimer); obs.setSceneItemProperties({"item": currentVideo.sceneItem, "scene-name": config.videoSceneName, "visible": false}) .then(res => { @@ -406,13 +419,13 @@ const streamInit = (config, obs, twitch) => { } // make sure request vid isn't in the queue already - if (videoQueue.findIndex(e => e.id === requestedVideoId) !== -1) { + if (videoQueue.findIndex(e => e.id == requestedVideoId) !== -1) { twitch.botChat.say(to, `That video is in the queue already!`); return; } // search for req'd vid by id in config.vods - let vodIndex = config.vods.findIndex(e => e.id === requestedVideoId); + let vodIndex = config.vods.findIndex(e => e.id == requestedVideoId); if (vodIndex === -1) { twitch.botChat.say(to, `A video with that ID does not exist!`); return; @@ -433,8 +446,10 @@ const streamInit = (config, obs, twitch) => { twitch.botChat.say(to, `Voting has been paused.`); } } + //////////////// - // ALL USER COMMANDS + // USER COMMANDS + // // VOTE FOR VIDEO if (commandNoPrefix === 'vote') { let userVote = commandParts[1] || false; @@ -510,6 +525,7 @@ const streamInit = (config, obs, twitch) => { twitch.botChat.say(to, `${config.vods[vodIndex].chatName} has been added to the queue [${videoQueue.length}]`); return; } + //////////////// } });