diff --git a/config.json b/config.json index d128c70..ddfc2b7 100755 --- a/config.json +++ b/config.json @@ -50,13 +50,13 @@ ], "defaultPlaylist": "room-grind" }, - "initialQueueSize": 10, + "initialQueueSize": 3, "videoSceneName": "fgfm", "videoPollSize": 5, "currentActivitySceneItemName": "now-showing-txt", "vods": [ { - "id": "ottas-seg-escape", + "id": "ot-seg-escape", "category": "Optimal TTAS Segment", "label": false, "chatName": "Escape (OTTAS Seg)", @@ -70,7 +70,7 @@ "length": 352 }, { - "id": "ottas-seg-eastern", + "id": "ot-seg-eastern", "category": "Optimal TTAS Segment", "label": false, "chatName": "Eastern (OTTAS Seg)", @@ -84,7 +84,7 @@ "length": 277 }, { - "id": "ottas-seg-desert", + "id": "ot-seg-desert", "category": "Optimal TTAS Segment", "label": false, "chatName": "Desert (OTTAS Seg)", @@ -98,7 +98,7 @@ "length": 345 }, { - "id": "ottas-seg-hera", + "id": "ot-seg-hera", "category": "Optimal TTAS Segment", "label": false, "chatName": "Hera (OTTAS Seg)", @@ -112,7 +112,7 @@ "length": 299 }, { - "id": "ottas-seg-atower", + "id": "ot-seg-atower", "category": "Optimal TTAS Segment", "label": false, "chatName": "ATower (OTTAS Seg)", @@ -126,7 +126,7 @@ "length": 352 }, { - "id": "ottas-seg-pod", + "id": "ot-seg-pod", "category": "Optimal TTAS Segment", "label": false, "chatName": "PoD (OTTAS Seg)", @@ -140,7 +140,7 @@ "length": 309 }, { - "id": "ottas-seg-thieves", + "id": "ot-seg-thieves", "category": "Optimal TTAS Segment", "label": false, "chatName": "Thieves (OTTAS Seg)", @@ -154,7 +154,7 @@ "length": 377 }, { - "id": "ottas-seg-skull", + "id": "ot-seg-skull", "category": "Optimal TTAS Segment", "label": false, "chatName": "Skull (OTTAS Seg)", @@ -168,7 +168,7 @@ "length": 267 }, { - "id": "ottas-seg-ice", + "id": "ot-seg-ice", "category": "Optimal TTAS Segment", "label": false, "chatName": "Ice (OTTAS Seg)", @@ -182,7 +182,7 @@ "length": 318 }, { - "id": "ottas-seg-swamp", + "id": "ot-seg-swamp", "category": "Optimal TTAS Segment", "label": false, "chatName": "Swamp (OTTAS Seg)", @@ -196,7 +196,7 @@ "length": 345 }, { - "id": "ottas-seg-mire", + "id": "ot-seg-mire", "category": "Optimal TTAS Segment", "label": false, "chatName": "Mire (OTTAS Seg)", @@ -210,7 +210,7 @@ "length": 365 }, { - "id": "ottas-seg-trock", + "id": "ot-seg-trock", "category": "Optimal TTAS Segment", "label": false, "chatName": "TRock (OTTAS Seg)", @@ -224,7 +224,7 @@ "length": 361 }, { - "id": "ottas-seg-gtower", + "id": "ot-seg-gtower", "category": "Optimal TTAS Segment", "label": false, "chatName": "GTower (OTTAS Seg)", @@ -238,7 +238,7 @@ "length": 396 }, { - "id": "ttas-seg-ganon", + "id": "ot-seg-ganon", "category": "Optimal TTAS Segment", "label": false, "chatName": "Ganon (TTAS Seg)", @@ -252,7 +252,7 @@ "length": 108 }, { - "id": "sttas-seg-escape", + "id": "st-seg-escape", "category": "Safe TTAS Segment", "label": false, "chatName": "Escape (STTAS Seg)", @@ -266,7 +266,7 @@ "length": 354 }, { - "id": "sttas-seg-eastern", + "id": "st-seg-eastern", "category": "Safe TTAS Segment", "label": false, "chatName": "Eastern (STTAS Seg)", @@ -280,7 +280,7 @@ "length": 281 }, { - "id": "sttas-seg-desert", + "id": "st-seg-desert", "category": "Safe TTAS Segment", "label": false, "chatName": "Desert (STTAS Seg)", @@ -294,7 +294,7 @@ "length": 347 }, { - "id": "sttas-seg-hera", + "id": "st-seg-hera", "category": "Safe TTAS Segment", "label": false, "chatName": "Hera (STTAS Seg)", @@ -308,7 +308,7 @@ "length": 303 }, { - "id": "sttas-seg-atower", + "id": "st-seg-atower", "category": "Safe TTAS Segment", "label": false, "chatName": "Agah Tower (STTAS Seg)", @@ -322,7 +322,7 @@ "length": 354 }, { - "id": "sttas-seg-pod", + "id": "st-seg-pod", "category": "Safe TTAS Segment", "label": false, "chatName": "PoD (STTAS Seg)", @@ -336,7 +336,7 @@ "length": 310 }, { - "id": "sttas-seg-thieves", + "id": "st-seg-thieves", "category": "Safe TTAS Segment", "label": false, "chatName": "Thieves (STTAS Seg)", @@ -350,7 +350,7 @@ "length": 379 }, { - "id": "sttas-seg-skull", + "id": "st-seg-skull", "category": "Safe TTAS Segment", "label": false, "chatName": "Skull (STTAS Seg)", @@ -364,7 +364,7 @@ "length": 270 }, { - "id": "sttas-seg-ice", + "id": "st-seg-ice", "category": "Safe TTAS Segment", "label": false, "chatName": "Ice (STTAS Seg)", @@ -378,7 +378,7 @@ "length": 321 }, { - "id": "sttas-seg-swamp", + "id": "st-seg-swamp", "category": "Safe TTAS Segment", "label": false, "chatName": "Swamp (STTAS Seg)", @@ -392,7 +392,7 @@ "length": 351 }, { - "id": "sttas-seg-mire", + "id": "st-seg-mire", "category": "Safe TTAS Segment", "label": false, "chatName": "Mire (STTAS Seg)", @@ -406,7 +406,7 @@ "length": 370 }, { - "id": "sttas-seg-trock", + "id": "st-seg-trock", "category": "Safe TTAS Segment", "label": false, "chatName": "TRock (STTAS Seg)", @@ -420,7 +420,7 @@ "length": 364 }, { - "id": "sttas-seg-gtower", + "id": "st-seg-gtower", "category": "Safe TTAS Segment", "label": false, "chatName": "GTower (STTAS Seg)", @@ -711,7 +711,7 @@ "scale.x": 1280 }, "sceneItem": "16x9ph", - "length": 1349 + "length": 1409 }, { "id": "pb-ms", diff --git a/twitch.js b/twitch.js index 3c4c3ad..fcbd28e 100755 --- a/twitch.js +++ b/twitch.js @@ -3,19 +3,17 @@ */ // @TODO: modularize OBS and Twitch code -// @TODO: Make the bot aware of what video is current active -// @TODO: Change video playlist source on an interval // @TODO: Rotating background images (leftside) // @TODO: Stream alerts for chat -// @TODO: Instead of playlists being chosen, have a database of files that can be played -// dynamically load this into a single media source that hides itself after playback ends -// listen for the event of the video ending and switch the source to the next one in the queue -// -// We can have the video queue work a lot like the songrequest queue. -// Instead of a new playlist being chosen every 2 hours, votes will be held for which *individual* video should be added to the queue next. -// A list of 10 or so will be chosen at random to be voted on. The top (x) will be added to the video queue. If nothing gets voted on, something random gets added. -// This will actually be easier on OBS too -- instead of having multiple playlists I have to manage within there, I can use a single media source and dynamically load whatever video gets chosen into that. -// I can also store whatever metadata about the video I want so I can update the labels automatically too. OpieOP +// @TODO: Room vid requests / import +// @TODO: Add random chance for room grind playlist to show for certain amount of time +// ☐ 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) +// ☐ update PB vod lengths to cut off before credits +// ☐ support for $pause +// ☐ remove currently playing video from vote choices + // Import modules const irc = require('irc'); @@ -43,6 +41,7 @@ obs.connect({ address: config.obs.websocket.address, password: config.obs.websoc .catch(err => { console.log(err); }); + // Listen for errors from OBS obs.on('error', err => { console.error('OBS socket error:', err); @@ -214,7 +213,7 @@ const twitchInit = (config, obs) => { // Listen for commands from everyone else if (commandNoPrefix === 'rngames') { - twitchChat.say(to, snesGames.sort( function() { return 0.5 - Math.random() } ).slice(0, 10).join(' | ')); + twitchChat.say(to, snesGames.sort( () => { return 0.5 - Math.random() } ).slice(0, 10).join(' | ')); } } }); @@ -258,19 +257,34 @@ 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( function() { return 0.5 - Math.random() } ).slice(0, config.initialQueueSize); + let 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; + const nextVideo = () => { + // 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)}`); + } + showVideo(currentVideo); + }; + const showVideo = video => { + //console.log(`Showing video: ${JSON.stringify(video)}`); // 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}); @@ -279,12 +293,12 @@ 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 videoTimer = setTimeout(() => { obs.setSceneItemProperties({"item": video.sceneItem, "scene-name": config.videoSceneName, "visible": false}) .then(data => { - currentVideo = videoQueue.shift(); - showVideo(currentVideo); + nextVideo(); }); }, video.length*1000) }) @@ -300,7 +314,7 @@ const streamInit = (config, obs, twitch) => { let rockTheVote = () => {}; let rtvInterval = setInterval(() => {rockTheVote()}, 300000); - let videoVoteJob = schedule.scheduleJob("*/15 * * * *", () => { + let videoVoteJob = new schedule.Job(() => { // Tally votes from previous election (if there was one), add the winner to the queue let winner; if (currentChoices.length > 0) { @@ -348,7 +362,7 @@ const streamInit = (config, obs, twitch) => { let inQueue = videoQueue.findIndex(q => q.id === e.id) !== -1; return !inQueue; }); - currentChoices = vodsNotInQueue.sort( function() { return 0.5 - Math.random() } ).slice(0, config.videoPollSize); + currentChoices = vodsNotInQueue.sort( () => { return 0.5 - Math.random() } ).slice(0, config.videoPollSize); // Poll the chat let chatChoices = currentChoices.map((c, i) => { @@ -363,7 +377,6 @@ const streamInit = (config, obs, twitch) => { rtvInterval = setInterval(() => {rockTheVote()}, 300000); }); - // Track user votes for video queue twitch.botChat.addListener('message', (from, to, message) => { // Ignore everything from blacklisted users if (config.twitch.blacklistedUsers.includes(from)) return; @@ -373,17 +386,55 @@ const streamInit = (config, obs, twitch) => { let commandParts = message.slice(config.twitch.cmdPrefix.length).split(' '); let commandNoPrefix = commandParts[0] || ''; + // ADMIN COMMANDS 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 => { - currentVideo = videoQueue.shift(); - showVideo(currentVideo); + nextVideo(); }); + // ADD + } else if (commandNoPrefix === 'add') { + let requestedVideoId = commandParts[1] || false; + if (requestedVideoId === false) { + twitch.botChat.say(to, `Missing video ID`); + return; + } + + // make sure request vid isn't in the queue already + 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); + if (vodIndex === -1) { + twitch.botChat.say(to, `A video with that ID does not exist!`); + return; + } + + // add to queue if it exists + videoQueue.push(config.vods[vodIndex]); + twitch.botChat.say(to, `${config.vods[vodIndex].chatName} has been added to the queue [${videoQueue.length}]`); + return; + // START VOTE + } else if (commandNoPrefix === 'startvote') { + videoVoteJob.reschedule("*/15 * * * *"); + twitch.botChat.say(to, `Voting has been started. Next run: ${videoVoteJob.nextInvocation()}`); + // PAUSE VOTE + } else if (commandNoPrefix === 'pausevote') { + clearInterval(rtvInterval); + videoVoteJob.cancel(); + twitch.botChat.say(to, `Voting has been paused.`); } } + // ALL USER COMMANDS + // VOTE FOR VIDEO if (commandNoPrefix === 'vote') { let userVote = commandParts[1] || false; @@ -412,6 +463,7 @@ const streamInit = (config, obs, twitch) => { userVotes.push({"from": from, "vote": userVote}); twitch.botChat.say(to, `@${from}, your vote has been logged!`); } + // QUEUE STATUS } else if (commandNoPrefix === 'queue') { if (videoQueue.length > 0) { let chatQueue = videoQueue.map((c, i) => { @@ -421,8 +473,41 @@ const streamInit = (config, obs, twitch) => { } else { twitch.botChat.say(to, `No videos currently in queue!`); } + // CURRENT VIDEO } else if (commandNoPrefix === 'current') { twitch.botChat.say(to, `Now Playing: ${currentVideo.chatName}`); + // NEXT VIDEO + } else if (commandNoPrefix === 'next') { + if (videoQueue.length > 0) { + twitch.botChat.say(to, `Next Video: ${videoQueue[0].chatName}`); + } else { + twitch.botChat.say(to, `No videos currently in queue!`); + } + // VIDEO REQUEST + } else if (commandNoPrefix === 'vr') { + let requestedVideoId = commandParts[1] || false; + if (requestedVideoId === false) { + twitch.botChat.say(to, `Useage: ${config.twitch.cmdPrefix}vr | Videos: https://pastebin.com/qv0wDkvB`); + return; + } + + // make sure request vid isn't in the queue already + 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); + if (vodIndex === -1) { + twitch.botChat.say(to, `A video with that ID does not exist!`); + return; + } + + // add to queue if it exists + videoQueue.push(config.vods[vodIndex]); + twitch.botChat.say(to, `${config.vods[vodIndex].chatName} has been added to the queue [${videoQueue.length}]`); + return; } } }); diff --git a/vid-id-list b/vid-id-list new file mode 100755 index 0000000..c5a8923 --- /dev/null +++ b/vid-id-list @@ -0,0 +1,66 @@ +Request videos using $vr where video-id is one of the following: + +e.g. $vr st-seg-gtower + +Optimal TTAS Segments +--------------------- +ot-seg-escape +ot-seg-eastern +ot-seg-desert +ot-seg-hera +ot-seg-atower +ot-seg-pod +ot-seg-thieves +ot-seg-skull +ot-seg-ice +ot-seg-swamp +ot-seg-mire +ot-seg-trock +ot-seg-gtower +ot-seg-ganon + +Safe/RTA Theory TAS Segments +---------------------------- +st-seg-escape +st-seg-eastern +st-seg-desert +st-seg-hera +st-seg-atower +st-seg-pod +st-seg-thieves +st-seg-skull +st-seg-ice +st-seg-swamp +st-seg-mire +st-seg-trock +st-seg-gtower + +NMG Golds +--------- +nmg-gold-escape +nmg-gold-eastern +nmg-gold-desert +nmg-gold-hera +nmg-gold-atower +nmg-gold-pod +nmg-gold-thieves +nmg-gold-skull +nmg-gold-ice +nmg-gold-swamp +nmg-gold-mire +nmg-gold-trock +nmg-gold-gtower +nmg-gold-ganon + + +Personal Bests +-------------- +pb-100-ahp +pb-ab +pb-ad +pb-any-nmg +pb-any-no-eg +pb-master-sword +pb-ms +pb-ms-no-eg +pb-rbo \ No newline at end of file