From 2edeedd66fb04f1459576349e50525baff3aaab5 Mon Sep 17 00:00:00 2001 From: Chris Ham Date: Tue, 18 Sep 2018 11:54:08 -0700 Subject: [PATCH] restructuring --- config.json | 173 +++++++++++++- fgfm.TODO | 5 +- fgfm.js | 636 ++++++++++++++++++++++++++++++---------------------- 3 files changed, 535 insertions(+), 279 deletions(-) diff --git a/config.json b/config.json index 0f4c53f..9022adc 100755 --- a/config.json +++ b/config.json @@ -30,21 +30,25 @@ { "sceneItem": "ttas-segments", "activity": "TTAS Segments !ttas", + "name": "TTAS Segments", "chatName": "TTAS Segments" }, { "sceneItem": "room-grind", "activity": "TTAS Room Grind !ttas", + "name": "TTAS Room Grind", "chatName": "TTAS Room Grind" }, { "sceneItem": "gold-segments", "activity": "Any% NMG Gold Segments", + "name": "NMG Golds", "chatName": "NMG Golds" }, { "sceneItem": "personal-bests", "activity": "Past Personal Bests", + "name": "Personal Bests", "chatName": "Personal Bests" } ], @@ -52,13 +56,16 @@ }, "initialQueueSize": 3, "videoSceneName": "fgfm", + "commercialSceneName": "commercial", "videoPollSize": 5, "currentActivitySceneItemName": "now-showing-txt", + "commercialInterval": 60, "vods": [ { "id": "ot-seg-escape", "category": "Optimal TTAS Segment", "label": false, + "name": "Escape (OTTAS Seg)", "chatName": "Escape (OTTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\01-escape.mp4", "sceneItemProperties": { @@ -73,6 +80,7 @@ "id": "ot-seg-eastern", "category": "Optimal TTAS Segment", "label": false, + "name": "Eastern (OTTAS Seg)", "chatName": "Eastern (OTTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\02-eastern.mp4", "sceneItemProperties": { @@ -87,6 +95,7 @@ "id": "ot-seg-desert", "category": "Optimal TTAS Segment", "label": false, + "name": "Desert (OTTAS Seg)", "chatName": "Desert (OTTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\03-desert.mp4", "sceneItemProperties": { @@ -101,6 +110,7 @@ "id": "ot-seg-hera", "category": "Optimal TTAS Segment", "label": false, + "name": "Hera (OTTAS Seg)", "chatName": "Hera (OTTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\04-hera.mp4", "sceneItemProperties": { @@ -115,6 +125,7 @@ "id": "ot-seg-atower", "category": "Optimal TTAS Segment", "label": false, + "name": "ATower (OTTAS Seg)", "chatName": "ATower (OTTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\05-atower.mp4", "sceneItemProperties": { @@ -129,6 +140,7 @@ "id": "ot-seg-pod", "category": "Optimal TTAS Segment", "label": false, + "name": "PoD (OTTAS Seg)", "chatName": "PoD (OTTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\06-pod.mp4", "sceneItemProperties": { @@ -143,6 +155,7 @@ "id": "ot-seg-thieves", "category": "Optimal TTAS Segment", "label": false, + "name": "Thieves (OTTAS Seg)", "chatName": "Thieves (OTTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\07-thieves.mp4", "sceneItemProperties": { @@ -157,6 +170,7 @@ "id": "ot-seg-skull", "category": "Optimal TTAS Segment", "label": false, + "name": "Skull (OTTAS Seg)", "chatName": "Skull (OTTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\08-skull.mp4", "sceneItemProperties": { @@ -171,6 +185,7 @@ "id": "ot-seg-ice", "category": "Optimal TTAS Segment", "label": false, + "name": "Ice (OTTAS Seg)", "chatName": "Ice (OTTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\09-ice.mp4", "sceneItemProperties": { @@ -185,6 +200,7 @@ "id": "ot-seg-swamp", "category": "Optimal TTAS Segment", "label": false, + "name": "Swamp (OTTAS Seg)", "chatName": "Swamp (OTTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\10-swamp.mp4", "sceneItemProperties": { @@ -199,6 +215,7 @@ "id": "ot-seg-mire", "category": "Optimal TTAS Segment", "label": false, + "name": "Mire (OTTAS Seg)", "chatName": "Mire (OTTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\11-mire.mp4", "sceneItemProperties": { @@ -213,6 +230,7 @@ "id": "ot-seg-trock", "category": "Optimal TTAS Segment", "label": false, + "name": "TRock (OTTAS Seg)", "chatName": "TRock (OTTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\12-trock.mp4", "sceneItemProperties": { @@ -227,6 +245,7 @@ "id": "ot-seg-gtower", "category": "Optimal TTAS Segment", "label": false, + "name": "GTower (OTTAS Seg)", "chatName": "GTower (OTTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\13-gtower.mp4", "sceneItemProperties": { @@ -241,6 +260,7 @@ "id": "ot-seg-ganon", "category": "Optimal TTAS Segment", "label": false, + "name": "Ganon (TTAS Seg)", "chatName": "Ganon (TTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\14-ganon.mp4", "sceneItemProperties": { @@ -255,6 +275,7 @@ "id": "st-seg-escape", "category": "Safe TTAS Segment", "label": false, + "name": "Escape (STTAS Seg)", "chatName": "Escape (STTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\01-escape.mp4", "sceneItemProperties": { @@ -269,6 +290,7 @@ "id": "st-seg-eastern", "category": "Safe TTAS Segment", "label": false, + "name": "Eastern (STTAS Seg)", "chatName": "Eastern (STTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\02-eastern.mp4", "sceneItemProperties": { @@ -283,6 +305,7 @@ "id": "st-seg-desert", "category": "Safe TTAS Segment", "label": false, + "name": "Desert (STTAS Seg)", "chatName": "Desert (STTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\03-desert.mp4", "sceneItemProperties": { @@ -297,6 +320,7 @@ "id": "st-seg-hera", "category": "Safe TTAS Segment", "label": false, + "name": "Hera (STTAS Seg)", "chatName": "Hera (STTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\04-hera.mp4", "sceneItemProperties": { @@ -311,6 +335,7 @@ "id": "st-seg-atower", "category": "Safe TTAS Segment", "label": false, + "name": "Agah Tower (STTAS Seg)", "chatName": "Agah Tower (STTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\05-atower.mp4", "sceneItemProperties": { @@ -325,6 +350,7 @@ "id": "st-seg-pod", "category": "Safe TTAS Segment", "label": false, + "name": "PoD (STTAS Seg)", "chatName": "PoD (STTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\06-pod.mp4", "sceneItemProperties": { @@ -339,6 +365,7 @@ "id": "st-seg-thieves", "category": "Safe TTAS Segment", "label": false, + "name": "Thieves (STTAS Seg)", "chatName": "Thieves (STTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\07-thieves.mp4", "sceneItemProperties": { @@ -353,6 +380,7 @@ "id": "st-seg-skull", "category": "Safe TTAS Segment", "label": false, + "name": "Skull (STTAS Seg)", "chatName": "Skull (STTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\08-skull.mp4", "sceneItemProperties": { @@ -367,6 +395,7 @@ "id": "st-seg-ice", "category": "Safe TTAS Segment", "label": false, + "name": "Ice (STTAS Seg)", "chatName": "Ice (STTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\09-ice.mp4", "sceneItemProperties": { @@ -381,6 +410,7 @@ "id": "st-seg-swamp", "category": "Safe TTAS Segment", "label": false, + "name": "Swamp (STTAS Seg)", "chatName": "Swamp (STTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\10-swamp.mp4", "sceneItemProperties": { @@ -395,6 +425,7 @@ "id": "st-seg-mire", "category": "Safe TTAS Segment", "label": false, + "name": "Mire (STTAS Seg)", "chatName": "Mire (STTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\11-mire.mp4", "sceneItemProperties": { @@ -409,6 +440,7 @@ "id": "st-seg-trock", "category": "Safe TTAS Segment", "label": false, + "name": "TRock (STTAS Seg)", "chatName": "TRock (STTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\12-trock.mp4", "sceneItemProperties": { @@ -423,6 +455,7 @@ "id": "st-seg-gtower", "category": "Safe TTAS Segment", "label": false, + "name": "GTower (STTAS Seg)", "chatName": "GTower (STTAS Seg)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\13-gtower.mp4", "sceneItemProperties": { @@ -437,6 +470,7 @@ "id": "nmg-gold-escape", "category": "Any% NMG Gold Segment", "label": "Any% NMG Gold Segment: Escape (5:53.2) [2018-02-24]", + "name": "Escape (NMG Gold)", "chatName": "Escape (NMG Gold)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\01-[553.2]-2018-02-24-escape.mp4", "sceneItemProperties": { @@ -451,6 +485,7 @@ "id": "nmg-gold-eastern", "category": "Any% NMG Gold Segment", "label": "Any% NMG Gold Segment: Eastern (5:00.12) [2018-06-25]", + "name": "Eastern (NMG Gold)", "chatName": "Eastern (NMG Gold)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\02-[500.12]-2018-06-25-eastern.mp4", "sceneItemProperties": { @@ -465,6 +500,7 @@ "id": "nmg-gold-desert", "category": "Any% NMG Gold Segment", "label": "Any% NMG Gold Segment: Desert (6:10.32) [2018-06-02]", + "name": "Desert (NMG Gold)", "chatName": "Desert (NMG Gold)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\03-[610.32]-2018-06-02-desert.mp4", "sceneItemProperties": { @@ -479,6 +515,7 @@ "id": "nmg-gold-hera", "category": "Any% NMG Gold Segment", "label": "Any% NMG Gold Segment: Hera (5:28.83) [2018-06-01]", + "name": "Hera (NMG Gold)", "chatName": "Hera (NMG Gold)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\04-[528.83]-2018-06-01-hera.mp4", "sceneItemProperties": { @@ -493,20 +530,22 @@ "id": "nmg-gold-atower", "category": "Any% NMG Gold Segment", "label": "Any% NMG Gold Segment: Agah Tower (5:13.45) [2018-09-07]", + "name": "Agah Tower (NMG Gold)", "chatName": "Agah Tower (NMG Gold)", - "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\05a-[513.45]-2018-09-07-atower.mp4", + "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\05a-[513.45]-2018-09-07-atower.mp4", "sceneItemProperties": { "crop.left": 0, "position.x": 320, "scale.x": 960 }, "sceneItem": "4x3ph", - "length": 315 + "length": 314 }, { "id": "nmg-gold-pod", "category": "Any% NMG Gold Segment", "label": "Any% NMG Gold Segment: Palace of Darkness (6:11.15) [2018-05-27]", + "name": "PoD (NMG Gold)", "chatName": "PoD (NMG Gold)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\06-[611.15]-2018-05-27-pod.mp4", "sceneItemProperties": { @@ -521,6 +560,7 @@ "id": "nmg-gold-thieves", "category": "Any% NMG Gold Segment", "label": "Any% NMG Gold Segment: Thieves Town (7:08.37) [2018-07-01]", + "name": "Thieves (NMG Gold)", "chatName": "Thieves (NMG Gold)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\07-[708.37]-2018-07-01-thieves.mp4", "sceneItemProperties": { @@ -535,6 +575,7 @@ "id": "nmg-gold-skull", "category": "Any% NMG Gold Segment", "label": "Any% NMG Gold Segment: Skull Woods (5:24.28) [2018-06-02]", + "name": "Skull (NMG Gold)", "chatName": "Skull (NMG Gold)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\08-[524.28]-2018-06-02-skull.mp4", "sceneItemProperties": { @@ -549,20 +590,22 @@ "id": "nmg-gold-ice", "category": "Any% NMG Gold Segment", "label": "Any% NMG Gold Segment: Ice Palace (6:18.13) [2018-09-08]", + "name": "Ice (NMG Gold)", "chatName": "Ice (NMG Gold)", - "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\09-[618.13]-2018-09-08-ice.mp4", + "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\09-[618.13]-2018-09-08-ice.mp4", "sceneItemProperties": { "crop.left": 0, "position.x": 320, "scale.x": 960 }, "sceneItem": "4x3ph", - "length": 382 + "length": 379 }, { "id": "nmg-gold-swamp", "category": "Any% NMG Gold Segment", "label": "Any% NMG Gold Segment: Swamp Palace (6:51.87) [2018-06-10]", + "name": "Swamp (NMG Gold)", "chatName": "Swamp (NMG Gold)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\10-[651.87]-2018-06-10-swamp.mp4", "sceneItemProperties": { @@ -577,6 +620,7 @@ "id": "nmg-gold-mire", "category": "Any% NMG Gold Segment", "label": "Any% NMG Gold Segment: Misery Mire (7:07.17) [2018-06-10]", + "name": "Mire (NMG Gold)", "chatName": "Mire (NMG Gold)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\11-[707.17]-2018-06-10-mire.mp4", "sceneItemProperties": { @@ -591,6 +635,7 @@ "id": "nmg-gold-trock", "category": "Any% NMG Gold Segment", "label": "Any% NMG Gold Segment: Turtle Rock (7:07.34) [2018-06-25]", + "name": "TRock (NMG Gold)", "chatName": "TRock (NMG Gold)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\12-[707.34]-2018-06-25-trock.mp4", "sceneItemProperties": { @@ -605,6 +650,7 @@ "id": "nmg-gold-gtower", "category": "Any% NMG Gold Segment", "label": "Any% NMG Gold Segment: Ganon's Tower (7:09.85) [2018-08-19]", + "name": "GTower (NMG Gold)", "chatName": "GTower (NMG Gold)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\13-[709.85]-2018-08-19-gtower.mp4", "sceneItemProperties": { @@ -619,6 +665,7 @@ "id": "nmg-gold-ganon", "category": "Any% NMG Gold Segment", "label": "Any% NMG Gold Segment: Ganon (1:43.06) [2018-05-30]", + "name": "Ganon (NMG Gold)", "chatName": "Ganon (NMG Gold)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\14-[143.06]-2018-05-30-ganon.mp4", "sceneItemProperties": { @@ -633,6 +680,7 @@ "id": "pb-100-ahp", "category": "Personal Best", "label": "Personal Best: 100% All Heart Pieces (1:19:12) [2017-12-22]", + "name": "100% MG AHP (PB)", "chatName": "100% MG AHP (PB)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\100%-mg-ahp\\2017-12-22-100mg-11912.mp4", "sceneItemProperties": { @@ -647,6 +695,7 @@ "id": "pb-ab", "category": "Personal Best", "label": "Personal Best: All Bosses No EG (1:09:23) [2017-11-20]", + "name": "All Bosses (PB)", "chatName": "All Bosses (PB)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\all-bosses\\2017-11-20-ab-10923.mp4", "sceneItemProperties": { @@ -661,6 +710,7 @@ "id": "pb-ad", "category": "Personal Best", "label": "Personal Best: All Dungeons No EG/DW/WW (1:14:59) [2017-11-19]", + "name": "All Dungeons (PB)", "chatName": "All Dungeons (PB)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\all-dungeons\\2017-11-19-ad-11459.mp4", "sceneItemProperties": { @@ -675,6 +725,7 @@ "id": "pb-any-nmg", "category": "Personal Best", "label": "Personal Best: Any% NMG No S+Q (1:26:24) [2018-05-27]", + "name": "Any% NMG (PB)", "chatName": "Any% NMG (PB)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\any%-nmg-nsq\\2018-05-27-nmg-12624.mp4", "sceneItemProperties": { @@ -689,6 +740,7 @@ "id": "pb-any-no-eg", "category": "Personal Best", "label": "Personal Best: Any% No EG (30:28) [2018-09-09]", + "name": "Any% No EG (PB)", "chatName": "Any% No EG (PB)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\any%-no-eg\\2018-09-09-no-eg-3028.mp4", "sceneItemProperties": { @@ -703,6 +755,7 @@ "id": "pb-master-sword", "category": "Personal Best", "label": "Personal Best: Master Sword NMG (22:23) [2018-08-23]", + "name": "Master Sword (PB)", "chatName": "Master Sword (PB)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\master-sword\\2018-08-23-master-sword-2223.mp4", "sceneItemProperties": { @@ -717,6 +770,7 @@ "id": "pb-ms", "category": "Personal Best", "label": "Personal Best: Mirror Shield NMG (50:32) [2017-06-20]", + "name": "Mirror Shield (PB)", "chatName": "Mirror Shield (PB)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\mirror-shield\\2017-06-20-mirror-shield-5032.mp4", "sceneItemProperties": { @@ -731,6 +785,7 @@ "id": "pb-ms-no-eg", "category": "Personal Best", "label": "Personal Best: Mirror Shield No EG (11:38) [2017-07-02]", + "name": "Mirror Shield No EG (PB)", "chatName": "Mirror Shield No EG (PB)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\mirror-shield-no-eg\\2017-07-02-mirror-shield-no-eg-1138.mp4", "sceneItemProperties": { @@ -745,6 +800,7 @@ "id": "pb-rbo", "category": "Personal Best", "label": "Personal Best: Reverse Boss Order (1:18:13) [2017-12-01]", + "name": "Reverse Boss Order (PB)", "chatName": "Reverse Boss Order (PB)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\rbo\\2017-12-01-rbo-11813.mp4", "sceneItemProperties": { @@ -759,6 +815,7 @@ "id": "pb-100-nmg", "category": "Personal Best", "label": "Personal Best: 100% NMG (1:49:30) [2017-01-31]", + "name": "100% NMG (PB)", "chatName": "100% NMG (PB)", "filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\100%\\2017-01-31-hundo-14930.mp4", "sceneItem": "legacyph", @@ -768,11 +825,119 @@ "id": "pb-dg", "category": "Personal Best", "label": "Personal Best: Defeat Ganon (13:41) [2017-02-20]", + "name": "Defeat Ganon (PB)", "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", "sceneItem": "legacyph", "length": 868 } ], + "memes": [ + { + "id": "archery", + "name": "archery", + "filePath": "Y:\\media\\videos\\ALttP\\memes\\archery-contest.mp4", + "sceneItem": "meme1", + "length": 27 + }, + { + "id": "69blazeit", + "name": "69blazeit", + "filePath": "Y:\\media\\videos\\ALttP\\memes\\69BlazeIt.mp4", + "sceneItem": "meme1", + "length": 92 + }, + { + "id": "rpgfarm", + "name": "rpgfarm", + "filePath": "Y:\\media\\videos\\ALttP\\memes\\2016-07-24-1424-06-rpg-race-sniped.mp4", + "sceneItem": "meme1", + "length": 144 + }, + { + "id": "emmapeg", + "name": "emmapeg", + "filePath": "Y:\\media\\videos\\ALttP\\memes\\emma-pegging.mp4", + "sceneItem": "meme1", + "length": 8 + }, + { + "id": "handy", + "name": "handy", + "filePath": "Y:\\media\\videos\\ALttP\\memes\\handy-in-the-mothhole.mp4", + "sceneItem": "meme1", + "length": 39 + }, + { + "id": "bodyguard", + "name": "bodyguard", + "filePath": "Y:\\media\\videos\\ALttP\\memes\\heroic-pop.mp4", + "sceneItem": "meme1", + "length": 14 + }, + { + "id": "whowillitbe", + "name": "whowillitbe", + "filePath": "Y:\\media\\videos\\ALttP\\memes\\its-gonna-be-may.mp4", + "sceneItem": "meme1", + "length": 30 + }, + { + "id": "mindblown", + "name": "mindblown", + "filePath": "Y:\\media\\videos\\ALttP\\memes\\mindblowing-and-lifechanging.mp4", + "sceneItem": "meme1", + "length": 55 + }, + { + "id": "nerd-nookie", + "name": "nerd-nookie", + "filePath": "Y:\\media\\videos\\ALttP\\memes\\nerd-bizkit.mp4", + "sceneItem": "meme1", + "length": 27 + }, + { + "id": "curling", + "name": "curling", + "filePath": "Y:\\media\\videos\\ALttP\\curling-bored-janitors.mp4", + "sceneItem": "meme1", + "length": 16 + }, + { + "id": "airplane", + "name": "airplane", + "filePath": "Y:\\media\\videos\\ALttP\\emetaPlane.mp4", + "sceneItem": "meme1", + "length": 5 + }, + { + "id": "hard-things", + "name": "hard-things", + "filePath": "Y:\\media\\videos\\ALttP\\questions-about-hard-things.mp4", + "sceneItem": "meme1", + "length": 39 + }, + { + "id": "18arrows", + "name": "18arrows", + "filePath": "Y:\\media\\videos\\ALttP\\screevo-18-arrows-fine.mp4", + "sceneItem": "meme1", + "length": 41 + }, + { + "id": "quake", + "name": "quake", + "filePath": "Y:\\media\\videos\\ALttP\\trock-indoor-quake.mp4", + "sceneItem": "meme1", + "length": 17 + }, + { + "id": "imout", + "name": "imout", + "filePath": "Y:\\media\\videos\\ALttP\\water-tektite-imout.mp4", + "sceneItem": "meme1", + "length": 7 + } + ], "debug": false } \ No newline at end of file diff --git a/fgfm.TODO b/fgfm.TODO index fd23efa..ae4a92a 100755 --- a/fgfm.TODO +++ b/fgfm.TODO @@ -15,4 +15,7 @@ TODO: ☐ 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 + ☐ Support viewer skip voting + + 960x540 + 896x504 \ No newline at end of file diff --git a/fgfm.js b/fgfm.js index ec6e3fb..c0c72dd 100755 --- a/fgfm.js +++ b/fgfm.js @@ -12,10 +12,12 @@ const util = require('./lib/util'); let config = require('./config.json'); const snesGames = require('./conf/snesgames.json'); const twitchChannel = config.twitch.channels[0].toLowerCase(); +const randSort = () => { return 0.5 - Math.random() }; let videoQueue = recentlyPlayed = []; let currentVideo; let videoTimer; +let lastCommercialShown; // Connect to OBS Websocket const obs = new OBSWebSocket(); @@ -25,8 +27,8 @@ obs.connect({ address: config.obs.websocket.address, password: config.obs.websoc console.log(`Success! We're connected to OBS!`); return twitchInit(config, obs); }) - .then(data => { - return streamInit(config, obs, data); + .then(twitch => { + return streamInit(config, obs, twitch); }) .catch(err => { console.log(err); @@ -37,7 +39,7 @@ obs.on('error', err => { console.error(`OBS socket error: ${JSON.stringify(err)}`); }); -// Initialize Twitch chat hooks +// Initialize Twitch chat const twitchInit = (config, obs) => { return new Promise((resolve, reject) => { console.log('Connecting to Twitch...'); @@ -61,169 +63,6 @@ const twitchInit = (config, obs) => { }); // Set up event listeners for Twitch - twitchChat.addListener('message', (from, to, message) => { - // Ignore everything from blacklisted users - if (config.twitch.blacklistedUsers.includes(from)) return; - - // Listen for commands that start with the designated prefix - if (message.startsWith(config.twitch.cmdPrefix)) { - let commandParts = message.slice(config.twitch.cmdPrefix.length).split(' '); - let commandNoPrefix = commandParts[0] || ''; - - // 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'); - let visibleTerm = (newVisibility ? 'visible' : 'hidden'); - - let target = commandParts[1] || false; - if (!target) { - twitchChat.say(to, `A scene item name is required!`); - return; - } - - let sceneItem = {"item": target}; - - let sceneOrGroup = commandParts[2] || false; - if (sceneOrGroup !== false) { - sceneItem["scene-name"] = sceneOrGroup; - } - - obs.getSceneItemProperties(sceneItem) - .then(data => { - if (data.visible === newVisibility) { - twitchChat.say(to, `This scene item is already ${visibleTerm}. DerpHam`); - } else { - sceneItem.visible = newVisibility; - obs.setSceneItemProperties(sceneItem) - .then(res => { - twitchChat.say(to, `${target} is now ${visibleTerm}.`); - }) - .catch(console.error); - } - }) - .catch(err => { - twitchChat.say(to, JSON.stringify(err)); - }); - // TOGGLE SOURCE VISIBILITY - } else if (commandNoPrefix === 't') { - let target = commandParts[1] || false; - if (!target) { - twitchChat.say(to, `A scene item name is required!`); - return; - } - - let sceneItem = {"item": target}; - - obs.getSceneItemProperties(sceneItem) - .then(data => { - let newVisibility = !data.visible; - let visibleTerm = (newVisibility ? 'visible' : 'hidden'); - - sceneItem.visible = newVisibility; - obs.setSceneItemProperties(sceneItem) - .then(res => { - twitchChat.say(to, `${target} is now ${visibleTerm}.`); - }) - .catch(console.error); - }) - .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; - let targetToShow = commandParts[2] || false; - if (targetToHide === false || targetToShow == false) { - twitchChat.say(to, `Format: ${config.twitch.cmdPrefix}swap `); - return - } - - obs.setSceneItemProperties({"item": targetToHide, "visible": false}) - .then(res => { - obs.setSceneItemProperties({"item": targetToShow, "visible": true}); - }) - .catch(console.error); - // Black Box "Everybody Wow" "commercial" - } else if (commandNoPrefix === 'auw') { - obs.setCurrentScene({"scene-name": "commercial"}) - .then(res => { - // show the video - return obs.setSceneItemProperties({"item": "everybody-wow", "scene-name": "commercial", "visible": true}); - }) - .then(res => { - // mute songrequest audio - editorChat.say(to, '!volume 0'); - // show owen - obs.setSceneItemProperties({"item": "owen", "scene-name": "commercial", "visible": true}); - // tell chat what's up - twitchChat.say(to, 'Everybody OwenWow'); - // swap back to fgfm scene after the video ends - setTimeout(() => { - // hide video - obs.setSceneItemProperties({"item": "everybody-wow", "scene-name": "commercial", "visible": false}) - // hide owen - obs.setSceneItemProperties({"item": "owen", "scene-name": "commercial", "visible": false}); - // unmute songrequest audio - editorChat.say(to, '!volume 50'); - // swap back to fgfm - obs.setCurrentScene({"scene-name": "fgfm"}); - }, 248000); - }) - .catch(console.error); - // SWITCH SCENES - } else if (commandNoPrefix === 'switch') { - - let target = commandParts[1] || false; - if (!target) { - twitchChat.say(to, `A scene name is required!`); - return; - } - - obs.getCurrentScene() - .then(data => { - if (data.name === target) { - twitchChat.say(to, `That scene is already active! DerpHam`); - } else { - obs.setCurrentScene({"scene-name": target}) - .then(() => {twitchChat.say(to, `${target} is now active`)}) - .catch(console.error); - } - }) - .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...'); - process.exit(0); - } - } - - // Listen for commands from everyone else - if (commandNoPrefix === 'rngames') { - twitchChat.say(to, snesGames.sort( () => { return 0.5 - Math.random() } ).slice(0, 10).join(' | ')); - } - } - }); - twitchChat.addListener('error', message => { if (message.command != 'err_unknowncommand') { console.error('error from Twitch IRC Server: ', message); @@ -252,7 +91,7 @@ const twitchInit = (config, obs) => { }); twitchChat.addListener('motd', motd => { - console.log(`Received MOTD: ${motd}`); + //console.log(`Received MOTD: ${motd}`); }); resolve({"botChat": twitchChat, "editorChat": editorChat}); @@ -263,20 +102,88 @@ const twitchInit = (config, obs) => { const streamInit = (config, obs, twitch) => { return new Promise((resolve, reject) => { console.log(`Setting up initial video queue...`); - videoQueue = config.vods.sort( () => { return 0.5 - Math.random() } ).slice(0, config.initialQueueSize); + videoQueue = config.vods.sort(randSort).slice(0, config.initialQueueSize); console.log(`Initial queue: ${videoQueue.map((c, i) => `[${i+1}] ${c.chatName}`).join(' | ')}`); - currentVideo = videoQueue.shift(); - // Pick the next video in the queue (or shuffle if queue is empty) + // Shows a video in the given scene and triggers a callback when it's finished + const playVideoInScene = (video, scene, callback) => { + return new Promise((resolve, reject) => { + // set the file path + obs.setSourceSettings({"sourceName": video.sceneItem, "sourceSettings": {"local_file": video.filePath}}) + // show the video + .then(data => obs.setSceneItemProperties({"item": video.sceneItem, "scene-name": scene, "visible": true})) + // trigger callback when the video is over, but resolve promise immediately with the timer + .then(data => { + resolve(setTimeout(() => {callback(data)}, video.length*1000)); + }) + .catch(reject); + }); + }; + + // Show a gameplay vod + const showVideo = video => { + console.log(`Showing video: ${video.chatName}`); + + let handleVideoFinish = () => { + obs.setSceneItemProperties({"item": video.sceneItem, "scene-name": config.videoSceneName, "visible": false}) + .then(data => {nextVideo()}); + .catch(console.error); + }; + + obs.setCurrentScene({"scene": config.videoSceneName}) + .then(res => { + playVideoInScene(video, config.videoSceneName, handleVideoFinish) + .then(timer => { + // track timer so we can cancel callback later on if necessary + videoTimer = timer; + + // update activity label and show/hide appropriately + if (video.hasOwnProperty('label') && video.label !== false) { + obs.setTextGDIPlusProperties({"source": config.currentActivitySceneItemName, "scene-name": config.videoSceneName, "render": true, "text": video.label}); + } else { + obs.setSceneItemProperties({"item": config.currentActivitySceneItemName, "scene-name": config.videoSceneName, "visible": false}); + } + }); + }) + .catch(console.error); + }; + + // Picks the next video in the queue (shuffles if empty) + // Also handles "commercial breaks" const nextVideo = () => { - // add currentVideo.id to recentlyPlayed list, remove oldest video if cap is hit + // Show a "commercial break" if it's been long enough since the last one + let secondsSinceLastCommercial = (Date.now() - lastCommercialShown) / 1000; + console.log(`It has been ${secondsSinceLastCommercial} seconds since the last commercial`); + /* if (secondsSinceLastCommercial >= config.commercialInterval) { + console.log(`Showing commercial now...`); + // @TODO: Add a random chance here for it to be "everybody wow" + let commercial = config.memes.sort(randSort)[0]; + obs.setCurrentScene({"scene": config.commercialSceneName}) + .then(res => { + return playVideoInScene(commercial, config.commercialSceneName, () => { + // hide video + obs.setSceneItemProperties({"item": commercial.sceneItem, "scene-name": config.commercialSceneName, "visible": false}) + // unmute songrequest audio + editorChat.say(to, '!volume 50'); + // show next video in queue + lastCommercialShown = Date.now(); + nextVideo(); + }) + }) + .then(res => { + // mute songrequest audio + editorChat.say(to, '!volume 0'); + });; + .catch(console.error); + }*/ + + // Keep track of recently played videos 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) { @@ -286,130 +193,226 @@ const streamInit = (config, obs, twitch) => { let freshVods = config.vods.filter(e => { return !recentlyPlayed.includes(e.id); }); - currentVideo = freshVods.sort( () => { return 0.5 - Math.random() } ).slice(0, 1).shift(); + currentVideo = freshVods.sort(randSort).slice(0, 1).shift(); } showVideo(currentVideo); }; - // Show a video and hide it when finished - const showVideo = 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 - return obs.setSceneItemProperties({"item": video.sceneItem, "scene-name": config.videoSceneName, "visible": true}); - }) - .then(data => { - // 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}); - } else { - return obs.setSceneItemProperties({"item": config.currentActivitySceneItemName, "scene-name": config.videoSceneName, "visible": false}); - } - }) - .then(data => { - // 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 => { - nextVideo(); - }); - }, video.length*1000) - }) - .catch(console.error); - }; + lastCommercialShown = Date.now(); + // grab the first video in the queue and show it + currentVideo = videoQueue.shift(); showVideo(currentVideo); - console.log(`Initializing stream timers...`); - - let userVotes = currentChoices = []; - let rockTheVote = () => {}; - let rtvInterval = setInterval(() => {rockTheVote()}, 300000); - - 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) { - if (userVotes.length === 0) { - // choose a random element from currentChoices - winner = util.randElement(currentChoices); - console.log(`VIDEO CHOSEN RANDOMLY: ${winner.chatName}`); - twitch.botChat.say(twitchChannel, `No Votes Logged -- Next Video Chosen at Random: ${winner.chatName}`); - } else { - // tally and sort votes - let voteTallies = []; - util.asyncForEach(userVotes, vote => { - tallyIndex = voteTallies.findIndex(e => e.id === vote.vote); - if (tallyIndex !== -1) { - voteTallies[tallyIndex].count++; - } else { - voteTallies.push({id: vote.vote, count: 1}); - } - }); - voteTallies.sort((a, b) => { - if (a.count < b.count) { - return -1; - } - if (a.count > b.count) { - return 1; - } - // a must be equal to b - return 0; - }); - - console.log(`Voting Results: ${JSON.stringify(voteTallies)}`); - winner = currentChoices[voteTallies[0].id-1]; - console.log(`WINNER OF THE VOTE: ${winner.chatName}`); - twitch.botChat.say(twitchChannel, `Winner of the Video Vote: ${winner.chatName}`); - - // clear user votes - userVotes = []; - } - - videoQueue.push(winner); - } - - // choose more random videos from config.vods (that aren't already in the queue) - let vodsNotInQueue = config.vods.filter(e => { - let inQueue = videoQueue.findIndex(q => q.id === e.id) !== -1; - return !inQueue; - }); - currentChoices = vodsNotInQueue.sort( () => { return 0.5 - Math.random() } ).slice(0, config.videoPollSize); - - // Poll the chat - let chatChoices = currentChoices.map((c, i) => { - return `[${i+1}] ${c.chatName}`; - }); - - rockTheVote = () => { - twitch.botChat.say(twitchChannel, `Vote for which video you'd like to add to the queue using ${config.twitch.cmdPrefix}vote #: ${chatChoices.join(' | ')}`) - }; - clearInterval(rtvInterval); - rockTheVote(); - rtvInterval = setInterval(() => {rockTheVote()}, 300000); - }); - - // Twitch Chat Commands for Video Queue Control + // Twitch Chat Commands twitch.botChat.addListener('message', (from, to, message) => { + // Ignore everything from blacklisted users if (config.twitch.blacklistedUsers.includes(from)) return; // Listen for commands that start with the designated prefix if (message.startsWith(config.twitch.cmdPrefix)) { + let commandParts = message.slice(config.twitch.cmdPrefix.length).split(' '); - let commandNoPrefix = commandParts[0] || ''; + let commandNoPrefix = commandParts[0] || ''; // ADMIN COMMANDS if (config.twitch.admins.includes(from) || from === config.twitch.username.toLowerCase()) { + + // SHOW/HIDE SOURCE + if (commandNoPrefix === 'show' || commandNoPrefix === 'hide') { + + let newVisibility = (commandNoPrefix === 'show'); + let visibleTerm = (newVisibility ? 'visible' : 'hidden'); + + let target = commandParts[1] || false; + if (!target) { + twitch.botChat.say(to, `A scene item name is required!`); + return; + } + + let sceneItem = {"item": target}; + + let sceneOrGroup = commandParts[2] || false; + if (sceneOrGroup !== false) { + sceneItem["scene-name"] = sceneOrGroup; + } + + obs.getSceneItemProperties(sceneItem) + .then(data => { + if (data.visible === newVisibility) { + twitch.botChat.say(to, `This scene item is already ${visibleTerm}. DerpHam`); + } else { + sceneItem.visible = newVisibility; + obs.setSceneItemProperties(sceneItem) + .then(res => { + twitch.botChat.say(to, `${target} is now ${visibleTerm}.`); + }) + .catch(console.error); + } + }) + .catch(err => { + twitch.botChat.say(to, JSON.stringify(err)); + }); + + + + // TOGGLE SOURCE VISIBILITY + } else if (commandNoPrefix === 't') { + let target = commandParts[1] || false; + if (!target) { + twitch.botChat.say(to, `A scene item name is required!`); + return; + } + + let sceneItem = {"item": target}; + + obs.getSceneItemProperties(sceneItem) + .then(data => { + let newVisibility = !data.visible; + let visibleTerm = (newVisibility ? 'visible' : 'hidden'); + + sceneItem.visible = newVisibility; + obs.setSceneItemProperties(sceneItem) + .then(res => { + twitch.botChat.say(to, `${target} is now ${visibleTerm}.`); + }) + .catch(console.error); + }) + .catch(err => { + twitch.botChat.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; + let targetToShow = commandParts[2] || false; + if (targetToHide === false || targetToShow == false) { + twitch.botChat.say(to, `Format: ${config.twitch.cmdPrefix}swap `); + return + } + + obs.setSceneItemProperties({"item": targetToHide, "visible": false}) + .then(res => { + obs.setSceneItemProperties({"item": targetToShow, "visible": true}); + }) + .catch(console.error); + + + // Black Box "Everybody Wow" config.commercialSceneName + } else if (commandNoPrefix === 'auw') { + obs.setCurrentScene({"scene-name": config.commercialSceneName}) + .then(res => { + // show the video + return obs.setSceneItemProperties({"item": "everybody-wow", "scene-name": config.commercialSceneName, "visible": true}); + }) + .then(res => { + // mute songrequest audio + editorChat.say(to, '!volume 0'); + // show owen + obs.setSceneItemProperties({"item": "owen", "scene-name": config.commercialSceneName, "visible": true}); + // tell chat what's up + twitch.botChat.say(to, 'Everybody OwenWow'); + // swap back to fgfm scene after the video ends + setTimeout(() => { + // hide video + obs.setSceneItemProperties({"item": "everybody-wow", "scene-name": config.commercialSceneName, "visible": false}) + // hide owen + obs.setSceneItemProperties({"item": "owen", "scene-name": config.commercialSceneName, "visible": false}); + // unmute songrequest audio + editorChat.say(to, '!volume 50'); + // swap back to fgfm + obs.setCurrentScene({"scene-name": "fgfm"}); + }, 246000); + }) + .catch(console.error); + + + // memes on-demand + } else if (commandNoPrefix === 'meme') { + obs.setCurrentScene({"scene-name": config.commercialSceneName}) + /*.then(res => { + // show the video + return obs.setSceneItemProperties({"item": "everybody-wow", "scene-name": config.commercialSceneName, "visible": true}); + }) + .then(res => { + // mute songrequest audio + editorChat.say(to, '!volume 0'); + // show owen + obs.setSceneItemProperties({"item": "owen", "scene-name": config.commercialSceneName, "visible": true}); + // tell chat what's up + twitch.botChat.say(to, 'Everybody OwenWow'); + // swap back to fgfm scene after the video ends + setTimeout(() => { + // hide video + obs.setSceneItemProperties({"item": "everybody-wow", "scene-name": config.commercialSceneName, "visible": false}) + // hide owen + obs.setSceneItemProperties({"item": "owen", "scene-name": config.commercialSceneName, "visible": false}); + // unmute songrequest audio + editorChat.say(to, '!volume 50'); + // swap back to fgfm + obs.setCurrentScene({"scene-name": "fgfm"}); + }, 246000);*/ + }) + .catch(console.error); + + + // SWITCH SCENES + } else if (commandNoPrefix === 'switch') { + + let target = commandParts[1] || false; + if (!target) { + twitch.botChat.say(to, `A scene name is required!`); + return; + } + + obs.getCurrentScene() + .then(data => { + if (data.name === target) { + twitch.botChat.say(to, `That scene is already active! DerpHam`); + } else { + obs.setCurrentScene({"scene-name": target}) + .then(() => {twitch.botChat.say(to, `${target} is now active`)}) + .catch(console.error); + } + }) + .catch(console.error); + + + // SET ON-SCREEN ACTIVITY + } else if (commandNoPrefix === 'setactivity') { + let target = commandParts.slice(1).join(' '); + if (!target) { + twitch.botChat.say(to, `Please provide a new activity`); + return; + } + + obs.setTextGDIPlusProperties({"source": config.currentActivitySceneItemName, "scene-name": config.videoSceneName, "render": true, "text": target}) + .then(res => { + twitch.botChat.say(to, `Activity updated!`); + return; + }) + .catch(console.error); + + + // REBOOT + } else if (commandNoPrefix === 'reboot') { + console.log('Received request from admin to reboot...'); + twitch.botChat.say(to, 'Rebooting...'); + process.exit(0); + + // SKIP - if (commandNoPrefix === 'skip') { + } else if (commandNoPrefix === 'skip') { clearTimeout(videoTimer); obs.setSceneItemProperties({"item": currentVideo.sceneItem, "scene-name": config.videoSceneName, "visible": false}) .then(res => { nextVideo(); }); + + // ADD } else if (commandNoPrefix === 'add') { let requestedVideoId = commandParts[1] || false; @@ -435,10 +438,14 @@ const streamInit = (config, obs, twitch) => { 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); @@ -479,6 +486,8 @@ 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) { @@ -489,9 +498,13 @@ 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) { @@ -499,6 +512,8 @@ const streamInit = (config, obs, twitch) => { } else { twitch.botChat.say(to, `No videos currently in queue!`); } + + // VIDEO REQUEST } else if (commandNoPrefix === 'vr') { let requestedVideoId = commandParts[1] || false; @@ -524,12 +539,85 @@ const streamInit = (config, obs, twitch) => { videoQueue.push(config.vods[vodIndex]); twitch.botChat.say(to, `${config.vods[vodIndex].chatName} has been added to the queue [${videoQueue.length}]`); return; + + // RNGAMES + } else if (commandNoPrefix === 'rngames') { + twitch.botChat.say(to, snesGames.sort(randSort).slice(0, 10).join(' | ')); } //////////////// } }); - resolve(videoQueue); + console.log(`Initializing stream timers...`); + + let userVotes = currentChoices = []; + let rockTheVote = () => {}; + let rtvInterval = setInterval(() => {rockTheVote()}, 300000); + + 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) { + if (userVotes.length === 0) { + // choose a random element from currentChoices + winner = util.randElement(currentChoices); + console.log(`VIDEO CHOSEN RANDOMLY: ${winner.chatName}`); + twitch.botChat.say(twitchChannel, `No Votes Logged -- Next Video Chosen at Random: ${winner.chatName}`); + } else { + // tally and sort votes + let voteTallies = []; + await util.asyncForEach(userVotes, async (vote) => { + tallyIndex = voteTallies.findIndex(e => e.id === vote.vote); + if (tallyIndex !== -1) { + voteTallies[tallyIndex].count++; + } else { + voteTallies.push({id: vote.vote, count: 1}); + } + }); + voteTallies.sort((a, b) => { + if (a.count < b.count) { + return -1; + } + if (a.count > b.count) { + return 1; + } + // a must be equal to b + return 0; + }); + + console.log(`Voting Results: ${JSON.stringify(voteTallies)}`); + winner = currentChoices[voteTallies[0].id-1]; + console.log(`WINNER OF THE VOTE: ${winner.chatName}`); + twitch.botChat.say(twitchChannel, `Winner of the Video Vote: ${winner.chatName}`); + + // clear user votes + userVotes = []; + } + + videoQueue.push(winner); + } + + // choose more random videos from config.vods (that aren't already in the queue) + let vodsNotInQueue = config.vods.filter(e => { + let inQueue = videoQueue.findIndex(q => q.id === e.id) !== -1; + return !inQueue; + }); + currentChoices = vodsNotInQueue.sort(randSort).slice(0, config.videoPollSize); + + // Poll the chat + let chatChoices = currentChoices.map((c, i) => { + return `[${i+1}] ${c.chatName}`; + }); + + rockTheVote = () => { + twitch.botChat.say(twitchChannel, `Vote for which video you'd like to add to the queue using ${config.twitch.cmdPrefix}vote #: ${chatChoices.join(' | ')}`) + }; + clearInterval(rtvInterval); + rockTheVote(); + rtvInterval = setInterval(() => {rockTheVote()}, 300000); + }); + + resolve(); }); }