Compare commits
19 Commits
develop
...
spotify-in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b6bb38192 | ||
|
|
11655fac86 | ||
|
|
67c677f61c | ||
|
|
1603f26aeb | ||
|
|
6683d7f68a | ||
|
|
51f417b916 | ||
|
|
6e36e06ce9 | ||
|
|
fd75d11eec | ||
|
|
d01439cf15 | ||
|
|
b86e2f102f | ||
|
|
f3a4159823 | ||
|
|
886a5f8b27 | ||
|
|
8e92d37338 | ||
|
|
159d5a328c | ||
|
|
2476e6a721 | ||
|
|
c3da69bf48 | ||
|
|
0f030992bd | ||
|
|
eba9e7ef2f | ||
|
|
617f176f5f |
@@ -12223,5 +12223,65 @@
|
|||||||
"fps": 60,
|
"fps": 60,
|
||||||
"length": 48.933984
|
"length": 48.933984
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 817,
|
||||||
|
"shortPath": "/comparisons/desert-exit.mp4",
|
||||||
|
"winPath": "\\comparisons\\desert-exit.mp4",
|
||||||
|
"dungeonId": "04",
|
||||||
|
"dungeonName": "hera",
|
||||||
|
"roomId": "01",
|
||||||
|
"roomName": "desert-exit-comparisons",
|
||||||
|
"videoData": {
|
||||||
|
"width": 960,
|
||||||
|
"height": 720,
|
||||||
|
"fps": 60,
|
||||||
|
"length": 14.183
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 818,
|
||||||
|
"shortPath": "/comparisons/bnc.mp4",
|
||||||
|
"winPath": "\\comparisons\\bnc.mp4",
|
||||||
|
"dungeonId": "01",
|
||||||
|
"dungeonName": "escape",
|
||||||
|
"roomId": "20",
|
||||||
|
"roomName": "bnc-comparisons",
|
||||||
|
"videoData": {
|
||||||
|
"width": 960,
|
||||||
|
"height": 720,
|
||||||
|
"fps": 60,
|
||||||
|
"length": 12.27
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 819,
|
||||||
|
"shortPath": "/comparisons/escape-1st-keyguard.mp4",
|
||||||
|
"winPath": "\\comparisons\\escape-1st-keyguard.mp4",
|
||||||
|
"dungeonId": "01",
|
||||||
|
"dungeonName": "escape",
|
||||||
|
"roomId": "11",
|
||||||
|
"roomName": "1st-keyguard-comparisons",
|
||||||
|
"videoData": {
|
||||||
|
"width": 960,
|
||||||
|
"height": 720,
|
||||||
|
"fps": 60,
|
||||||
|
"length": 5.78
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 820,
|
||||||
|
"shortPath": "/comparisons/pod-arrow-statue-dash-vs-walk.mp4",
|
||||||
|
"winPath": "\\comparisons\\pod-arrow-statue-dash-vs-walk.mp4",
|
||||||
|
"dungeonId": "06",
|
||||||
|
"dungeonName": "pod",
|
||||||
|
"roomId": "30",
|
||||||
|
"roomName": "pod-arrow-statue-comparisons",
|
||||||
|
"videoData": {
|
||||||
|
"width": 960,
|
||||||
|
"height": 720,
|
||||||
|
"fps": 60,
|
||||||
|
"length": 24.52
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
7
conf/timers.json
Executable file
7
conf/timers.json
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "vr",
|
||||||
|
"interval": 1800,
|
||||||
|
"value": "Video and room requests are on! Use $vr <video-id> to request a video from this list [https://pastebin.com/qv0wDkvB] or $room <room-id> to request a specific room (looped for a few minutes) from this list [https://goo.gl/qoNmuH]"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,5 +1,16 @@
|
|||||||
{
|
{
|
||||||
"alttp": [
|
"alttp": [
|
||||||
|
{
|
||||||
|
"id": "ttas",
|
||||||
|
"category": "Optimal TTAS",
|
||||||
|
"label": "Any% NMG Optimal Theory TAS [1:20:38.72]",
|
||||||
|
"name": "OTTAS Full",
|
||||||
|
"chatName": "OTTAS Full",
|
||||||
|
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\full-risky-raw.mp4",
|
||||||
|
"sceneItem": "16x9ph",
|
||||||
|
"length": 4843,
|
||||||
|
"includeInShuffle": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "ot-seg-escape",
|
"id": "ot-seg-escape",
|
||||||
"category": "Optimal TTAS Segment",
|
"category": "Optimal TTAS Segment",
|
||||||
@@ -154,6 +165,17 @@
|
|||||||
"length": 108,
|
"length": 108,
|
||||||
"includeInShuffle": true
|
"includeInShuffle": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "rta-ttas",
|
||||||
|
"category": "Safe TTAS",
|
||||||
|
"label": "Any% NMG Safe/RTA Theory TAS [1:21:26.52]",
|
||||||
|
"name": "STTAS Full",
|
||||||
|
"chatName": "STTAS Full",
|
||||||
|
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\full-rta-raw.mp4",
|
||||||
|
"sceneItem": "16x9ph",
|
||||||
|
"length": 4892,
|
||||||
|
"includeInShuffle": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "st-seg-escape",
|
"id": "st-seg-escape",
|
||||||
"category": "Safe TTAS Segment",
|
"category": "Safe TTAS Segment",
|
||||||
@@ -410,12 +432,12 @@
|
|||||||
{
|
{
|
||||||
"id": "nmg-gold-mire",
|
"id": "nmg-gold-mire",
|
||||||
"category": "Any% NMG Gold Segment",
|
"category": "Any% NMG Gold Segment",
|
||||||
"label": "Any% NMG Gold Segment: Misery Mire (7:07.17) [2018-06-10]",
|
"label": "Any% NMG Gold Segment: Misery Mire (7:06.58) [2018-10-21]",
|
||||||
"name": "Mire (NMG Gold)",
|
"name": "Mire (NMG Gold)",
|
||||||
"chatName": "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",
|
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\11-[706.58]-2018-10-21-mire.mp4",
|
||||||
"sceneItem": "4x3ph",
|
"sceneItem": "4x3ph",
|
||||||
"length": 432,
|
"length": 430,
|
||||||
"includeInShuffle": true
|
"includeInShuffle": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -460,7 +482,7 @@
|
|||||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\100%-mg-ahp\\2017-12-22-100mg-11912.mp4",
|
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\100%-mg-ahp\\2017-12-22-100mg-11912.mp4",
|
||||||
"sceneItem": "16x9ph",
|
"sceneItem": "16x9ph",
|
||||||
"length": 4786,
|
"length": 4786,
|
||||||
"includeInShuffle": false
|
"includeInShuffle": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "pb-ab",
|
"id": "pb-ab",
|
||||||
@@ -471,18 +493,18 @@
|
|||||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\all-bosses\\2017-11-20-ab-10923.mp4",
|
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\all-bosses\\2017-11-20-ab-10923.mp4",
|
||||||
"sceneItem": "16x9ph",
|
"sceneItem": "16x9ph",
|
||||||
"length": 4200,
|
"length": 4200,
|
||||||
"includeInShuffle": false
|
"includeInShuffle": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "pb-ad",
|
"id": "pb-ad",
|
||||||
"category": "Personal Best",
|
"category": "Personal Best",
|
||||||
"label": "Personal Best: All Dungeons No EG/DW/WW (1:14:59) [2017-11-19]",
|
"label": "Personal Best: All Dungeons No EG/DG/WW (1:14:59) [2017-11-19]",
|
||||||
"name": "All Dungeons (PB)",
|
"name": "All Dungeons (PB)",
|
||||||
"chatName": "All Dungeons (PB)",
|
"chatName": "All Dungeons (PB)",
|
||||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\all-dungeons\\2017-11-19-ad-11459.mp4",
|
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\all-dungeons\\2017-11-19-ad-11459.mp4",
|
||||||
"sceneItem": "16x9ph",
|
"sceneItem": "16x9ph",
|
||||||
"length": 4555,
|
"length": 4555,
|
||||||
"includeInShuffle": false
|
"includeInShuffle": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "pb-any-nmg",
|
"id": "pb-any-nmg",
|
||||||
@@ -493,17 +515,17 @@
|
|||||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\any%-nmg-nsq\\2018-05-27-nmg-12624.mp4",
|
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\any%-nmg-nsq\\2018-05-27-nmg-12624.mp4",
|
||||||
"sceneItem": "16x9ph",
|
"sceneItem": "16x9ph",
|
||||||
"length": 5190,
|
"length": 5190,
|
||||||
"includeInShuffle": false
|
"includeInShuffle": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "pb-any-no-eg",
|
"id": "pb-any-no-eg",
|
||||||
"category": "Personal Best",
|
"category": "Personal Best",
|
||||||
"label": "Personal Best: Any% No EG (30:28) [2018-09-09]",
|
"label": "Personal Best: Any% No EG (29:05) [2018-10-14]",
|
||||||
"name": "Any% No EG (PB)",
|
"name": "Any% No EG (PB)",
|
||||||
"chatName": "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",
|
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\any%-no-eg\\2018-10-14-no-eg-2905.mp4",
|
||||||
"sceneItem": "16x9ph",
|
"sceneItem": "16x9ph",
|
||||||
"length": 1833,
|
"length": 1777,
|
||||||
"includeInShuffle": true
|
"includeInShuffle": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -526,7 +548,7 @@
|
|||||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\mirror-shield\\2017-06-20-mirror-shield-5032.mp4",
|
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\mirror-shield\\2017-06-20-mirror-shield-5032.mp4",
|
||||||
"sceneItem": "16x9ph",
|
"sceneItem": "16x9ph",
|
||||||
"length": 3068,
|
"length": 3068,
|
||||||
"includeInShuffle": false
|
"includeInShuffle": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "pb-ms-no-eg",
|
"id": "pb-ms-no-eg",
|
||||||
@@ -534,8 +556,8 @@
|
|||||||
"label": "Personal Best: Mirror Shield No EG (11:38) [2017-07-02]",
|
"label": "Personal Best: Mirror Shield No EG (11:38) [2017-07-02]",
|
||||||
"name": "Mirror Shield No EG (PB)",
|
"name": "Mirror Shield No EG (PB)",
|
||||||
"chatName": "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",
|
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\mirror-shield-no-eg\\2017-07-02-mirror-shield-no-eg-1138-resized.mp4",
|
||||||
"sceneItem": "16x9ph",
|
"sceneItem": "4x3ph",
|
||||||
"length": 738,
|
"length": 738,
|
||||||
"includeInShuffle": true
|
"includeInShuffle": true
|
||||||
},
|
},
|
||||||
@@ -548,7 +570,7 @@
|
|||||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\rbo\\2017-12-01-rbo-11813.mp4",
|
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\rbo\\2017-12-01-rbo-11813.mp4",
|
||||||
"sceneItem": "16x9ph",
|
"sceneItem": "16x9ph",
|
||||||
"length": 4725,
|
"length": 4725,
|
||||||
"includeInShuffle": false
|
"includeInShuffle": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "pb-100-nmg",
|
"id": "pb-100-nmg",
|
||||||
|
|||||||
83
fgfm.TODO
83
fgfm.TODO
@@ -1,35 +1,86 @@
|
|||||||
TODO:
|
TODO:
|
||||||
✔ Add cooldowns @done (18-10-02 10:16)
|
☐ Spotify integration
|
||||||
|
✔ song -- display current song + link @done (18-12-04 11:24)
|
||||||
|
✔ playlist -- display current context / album @done (18-12-04 11:24)
|
||||||
|
✔ skip -- mods auto-skip @done (18-12-04 11:47)
|
||||||
|
✔ volume -- volume adjustment @done (18-12-04 19:49)
|
||||||
|
✔ pause / resume @done (18-12-04 11:49)
|
||||||
|
✔ ability to change the playlist (setplaylist <spotify-uri>) @done (18-12-11 10:52)
|
||||||
|
✔ shuffle on/off @done (18-12-11 11:22)
|
||||||
|
✔ repeat mode control @done (18-12-11 11:22)
|
||||||
|
☐ Web interface for viewers to issue commands
|
||||||
|
☐ Organized room / video list, one-click add-to-queue
|
||||||
|
☐ Admin panel on website for control
|
||||||
|
☐ Add support for a command to mute/unmute audio sources
|
||||||
|
☐ Don't re-create queue on start if it already exists
|
||||||
|
☐ Handle socket disconnect
|
||||||
|
☐ Decouple twitch chat from GHOBS
|
||||||
☐ Move anything that calls director.state from app into fgfm lib
|
☐ Move anything that calls director.state from app into fgfm lib
|
||||||
☐ Restrict # of requests a user can have in the queue at once
|
☐ Restrict # of requests a user can have in the queue at once
|
||||||
☐ Move vrmode timer to this bot, delete from SLCB
|
|
||||||
☐ Support a CLI flag to delay showing queue until command is issued
|
|
||||||
☐ Room vid requests / import
|
☐ Room vid requests / import
|
||||||
☐ Improved interface for viewer requests
|
☐ Improved interface for viewer requests
|
||||||
☐ Web interface? Twitch extension?
|
☐ Web interface? Twitch extension?
|
||||||
☐ Improvements
|
☐ Improvements
|
||||||
☐ When playing a room back, loop it at slower speeds for a few iterations
|
☐ When playing a room back, loop it at slower speeds for a few iterations
|
||||||
☐ Support viewer skip voting
|
☐ Support adding sets of videos to the queue at once (like the entire ttas or all gold segments)
|
||||||
☐ Remove currently playing video from vote choices
|
|
||||||
☐ Command to add sets of videos to the queue at once (like the entire ttas or all gold segments)
|
|
||||||
☐ Command to stop video rotation / timers (shutdown)
|
|
||||||
☐ Start/stop stream automation
|
|
||||||
☐ Start
|
|
||||||
- Starting Soon is shown until countdown is triggered
|
|
||||||
- Countdown for X minutes is triggered and shown
|
|
||||||
- Once countdown finishes, switch to intro scene and play
|
|
||||||
- Switch to fgfm once intro finishes
|
|
||||||
☐ Support for $pause -- not sure how to pull this off
|
|
||||||
☐ Tool to output list of video ID's / descriptions
|
☐ Tool to output list of video ID's / descriptions
|
||||||
☐ Stream alerts for chat
|
☐ Stream alerts for chat
|
||||||
☐ Rotating background images (leftside)
|
☐ Rotating background images (leftside)
|
||||||
|
☐ Support gif's via command through gifph
|
||||||
|
- !slowdance
|
||||||
|
- !bender
|
||||||
|
- !carlton
|
||||||
|
- !weeb
|
||||||
|
☐ Fix commercial playing issue (switches back to scene early)
|
||||||
|
☐ Change $auw and $meme to queue up the videos just like the others (don't switch scenes)
|
||||||
|
☐ Stats tracking for games
|
||||||
|
☐ Most won/lost gambling
|
||||||
|
☐ Most trivia answered
|
||||||
|
☐ Allow %'s for !gamble
|
||||||
|
|
||||||
Ideas:
|
Ideas:
|
||||||
☐ Web interface for viewers to issue commands -- twitch extension?!
|
|
||||||
☐ Support songrequests -- play through discord?
|
StreamWebRemote:
|
||||||
|
- OBS Websocket Connection Mgmt
|
||||||
|
- Stream Start/Stop/Restart
|
||||||
|
- Video Queue Mgmt
|
||||||
|
- Video DB Mgmt
|
||||||
|
Users:
|
||||||
|
- use twitch login
|
||||||
|
- video requests
|
||||||
|
- room requests
|
||||||
|
|
||||||
___________________
|
___________________
|
||||||
Archive:
|
Archive:
|
||||||
|
✔ Stop @done (18-11-06 21:54) @project(TODO)
|
||||||
|
✔ video starting/ending/skipped @done (18-11-06 21:53) @project(TODO)
|
||||||
|
✔ show status changing @done (18-11-06 21:53) @project(TODO)
|
||||||
|
✔ Have director emit events for bots to listen to @done (18-11-06 21:53) @project(TODO)
|
||||||
|
✔ Auto-enable vrmode timer when show starts (listen for event) @done (18-11-06 21:53) @project(TODO)
|
||||||
|
✔ hide sources before switching to credits @done (18-11-06 21:41) @project(TODO)
|
||||||
|
✔ Support a delay for Start Streaming @done (18-11-06 21:41) @project(TODO)
|
||||||
|
✔ Add a $rooms alias for $room @done (18-11-06 15:05) @project(TODO)
|
||||||
|
✔ Remove currently playing video from vote choices @done (18-10-30 11:51) @project(TODO)
|
||||||
|
✔ track votes for the current playing video @done (18-10-30 11:48) @project(TODO)
|
||||||
|
✔ if threshold is met, skip @done (18-10-30 11:48) @project(TODO)
|
||||||
|
✔ config item for how many votes are required to skip @done (18-10-30 11:48) @project(TODO)
|
||||||
|
✔ clear votes after video finishes @done (18-10-30 11:48) @project(TODO)
|
||||||
|
✔ Support viewer $skip voting @done (18-10-30 11:48) @project(TODO)
|
||||||
|
✔ update director showStatus to 'PAUSED' @done (18-10-30 11:33) @project(TODO)
|
||||||
|
✔ have nextVideo check for PAUSED @done (18-10-30 11:33) @project(TODO)
|
||||||
|
✔ Support for $pause (pauses queue after current video finishes) @done (18-10-30 11:33) @project(TODO)
|
||||||
|
✔ Move vrmode timer to this bot, delete from SLCB @done (18-10-30 11:20) @project(TODO)
|
||||||
|
✔ Don't auto-init GHOBS or FGFM, make them on-demand @done (18-10-30 08:26) @project(TODO)
|
||||||
|
✔ Switch to credits with 1 minute remaining @done (18-10-30 08:15) @project(TODO)
|
||||||
|
✔ Stop Stream @done (18-10-30 08:15) @project(TODO)
|
||||||
|
✔ Stop @done (18-10-30 08:15) @project(TODO)
|
||||||
|
✔ Parameter for how long until the stream should end @done (18-10-30 08:15) @project(TODO)
|
||||||
|
✔ Fade out audio sources with 5 seconds left @done (18-10-30 08:15) @project(TODO)
|
||||||
|
✔ Add parameter for countdown @done (18-10-30 08:15) @project(TODO)
|
||||||
|
✔ Set up the queue upon init so it can be managed during startup @done (18-10-30 08:14) @project(TODO)
|
||||||
|
✔ Starting Soon is shown until countdown is triggered @done (18-10-26 09:44) @project(TODO)
|
||||||
|
✔ Start stream @done (18-10-26 09:44) @project(TODO)
|
||||||
|
✔ Add cooldowns @done (18-10-02 10:16) @project(TODO)
|
||||||
✔ video length @done (18-09-28 09:33) @project(TODO)
|
✔ video length @done (18-09-28 09:33) @project(TODO)
|
||||||
✔ root folder name @done (18-09-28 09:33) @project(TODO)
|
✔ root folder name @done (18-09-28 09:33) @project(TODO)
|
||||||
✔ room ID @done (18-09-28 09:33) @project(TODO)
|
✔ room ID @done (18-09-28 09:33) @project(TODO)
|
||||||
|
|||||||
261
fgfm.js
261
fgfm.js
@@ -6,18 +6,24 @@
|
|||||||
const irc = require('irc');
|
const irc = require('irc');
|
||||||
const schedule = require('node-schedule');
|
const schedule = require('node-schedule');
|
||||||
const md5 = require('md5');
|
const md5 = require('md5');
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
// Import local packages
|
// Import local packages
|
||||||
const GHOBS = require('./lib/ghobs');
|
const GHOBS = require('./lib/ghobs');
|
||||||
const FGFM = require('./lib/fgfm');
|
const FGFM = require('./lib/fgfm');
|
||||||
const cooldowns = require('./lib/cooldowns');
|
const cooldowns = require('./lib/cooldowns');
|
||||||
const util = require('./lib/util');
|
const util = require('./lib/util');
|
||||||
|
const Spotify = require('./lib/spotify');
|
||||||
|
|
||||||
// Read internal configuration
|
// Read internal configuration
|
||||||
let config = require('./config.json');
|
let config = require('./config.json');
|
||||||
config.vods = require(config.vodConfigFile);
|
config.vods = require(config.vodConfigFile);
|
||||||
config.rooms = require(config.roomConfigFile);
|
config.rooms = require(config.roomConfigFile);
|
||||||
let snesGames = require('./conf/snesgames.json');
|
let snesGames = require('./conf/snesgames.json');
|
||||||
|
let timersList = require('./conf/timers.json');
|
||||||
|
|
||||||
|
let activeTimers = [];
|
||||||
|
let skipVote = {target: null, count: 0};
|
||||||
|
|
||||||
// Main screen turn on
|
// Main screen turn on
|
||||||
const obs = new GHOBS(config);
|
const obs = new GHOBS(config);
|
||||||
@@ -73,12 +79,57 @@ const streamInit = (config, twitch) => {
|
|||||||
|
|
||||||
// All your comfy are belong to us
|
// All your comfy are belong to us
|
||||||
const director = new FGFM({config: config, obs: obs});
|
const director = new FGFM({config: config, obs: obs});
|
||||||
director.init();
|
|
||||||
|
// Handle show events from the director
|
||||||
|
director.on('SHOW_STARTED', () => {
|
||||||
|
manageTimer('vr', 'on');
|
||||||
|
});
|
||||||
|
director.on('SHOW_PAUSED', () => {
|
||||||
|
manageTimer('vr', 'off');
|
||||||
|
});
|
||||||
|
director.on('SHOW_RESUMED', () => {
|
||||||
|
manageTimer('vr', 'on');
|
||||||
|
});
|
||||||
|
|
||||||
|
director.on('SHOW_ENDING', (secondsUntilCredits) => {
|
||||||
|
manageTimer('vr', 'off');
|
||||||
|
|
||||||
|
// Let the chat know the stream is ending soon
|
||||||
|
twitch.botChat.say(config.twitch.channel, `The stream will be ending in ${parseFloat(secondsUntilCredits/60).toFixed(0)} minutes!`);
|
||||||
|
});
|
||||||
|
|
||||||
|
director.on('CREDITS_SHOWN', (secondsUntilEnd) => {
|
||||||
|
twitch.editorChat.say(config.twitch.channel, `Thanks to everyone for watching and lurking! Have a wonderful night and stay comfy. greenhComfy`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spotify integration
|
||||||
|
const spotify = new Spotify(config.spotify);
|
||||||
|
spotify.init();
|
||||||
|
|
||||||
// Chat commands
|
// Chat commands
|
||||||
const commands = {
|
const commands = {
|
||||||
admin: {
|
admin: {
|
||||||
|
|
||||||
|
init: (cmd) => {
|
||||||
|
let streamStartDelaySeconds = cmd.args[1] || 1;
|
||||||
|
let showStartDelaySeconds = cmd.args[2] || 300;
|
||||||
|
|
||||||
|
director.startingSoon(streamStartDelaySeconds, showStartDelaySeconds);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
start: (cmd) => {
|
||||||
|
director.startTheShow();
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
end: (cmd) => {
|
||||||
|
let creditsDelay = cmd.args[1] || 1;
|
||||||
|
let endDelay = cmd.args[2] || 60;
|
||||||
|
director.endTheShow(creditsDelay, endDelay);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
changevis: (cmd, newVisibility) => {
|
changevis: (cmd, newVisibility) => {
|
||||||
let sceneItem = command.args[1] || false;
|
let sceneItem = command.args[1] || false;
|
||||||
if (!sceneItem) {
|
if (!sceneItem) {
|
||||||
@@ -112,6 +163,23 @@ const streamInit = (config, twitch) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
timer: (cmd) => {
|
||||||
|
let timerName = cmd.args[1] || false;
|
||||||
|
if (!timerName) {
|
||||||
|
twitch.botChat.say(cmd.to, `A timer name is required!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let timerStatus = cmd.args[2] || false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
manageTimer(timerName, timerStatus);
|
||||||
|
} catch (e) {
|
||||||
|
twitch.botChat.say(cmd.to, e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
auw: (cmd) => {
|
auw: (cmd) => {
|
||||||
director.showMeme('auw');
|
director.showMeme('auw');
|
||||||
},
|
},
|
||||||
@@ -156,6 +224,16 @@ const streamInit = (config, twitch) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
showact: (cmd) => {
|
||||||
|
obs.showActivity().catch(console.error);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
hideact: (cmd) => {
|
||||||
|
obs.hideActivity().catch(console.error);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
add: (cmd) => {
|
add: (cmd) => {
|
||||||
// @TODO: DRY this out with the checks in vr
|
// @TODO: DRY this out with the checks in vr
|
||||||
let requestedVideoId = cmd.args[1] || false;
|
let requestedVideoId = cmd.args[1] || false;
|
||||||
@@ -193,6 +271,16 @@ const streamInit = (config, twitch) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
pause: (cmd) => {
|
||||||
|
director.pause();
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
resume: (cmd) => {
|
||||||
|
director.resume();
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
clear: (cmd) => {
|
clear: (cmd) => {
|
||||||
director.clearQueue();
|
director.clearQueue();
|
||||||
},
|
},
|
||||||
@@ -211,6 +299,95 @@ const streamInit = (config, twitch) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
songskip: (cmd) => {
|
||||||
|
spotify.skip();
|
||||||
|
},
|
||||||
|
|
||||||
|
songpause: (cmd) => {
|
||||||
|
spotify.pause();
|
||||||
|
},
|
||||||
|
|
||||||
|
songresume: (cmd) => {
|
||||||
|
spotify.resume();
|
||||||
|
},
|
||||||
|
|
||||||
|
songvol: (cmd) => {
|
||||||
|
let volume = parseInt(cmd.args[1]) || 100;
|
||||||
|
spotify.setVolume(volume)
|
||||||
|
.then(res => twitch.botChat.say(cmd.to, `Volume set to ${volume}`))
|
||||||
|
.catch(err => twitch.botChat.say(cmd.to, `Error setting spotify volume: ${JSON.stringify(err)}`));
|
||||||
|
},
|
||||||
|
|
||||||
|
songplay: (cmd) => {
|
||||||
|
let url = cmd.args[1] || false;
|
||||||
|
if (url === false) {
|
||||||
|
return twitch.botChat.say(cmd.to, `You must provide a link to a spotify playlist or album!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse+validate url
|
||||||
|
let spotifyUri = false;
|
||||||
|
|
||||||
|
// check for native spotify URI first
|
||||||
|
if (url.includes('spotify:')) {
|
||||||
|
let parsedUrl = url.match(/spotify:(playlist|album):([A-Za-z0-9]{22})/);
|
||||||
|
if (parsedUrl !== null) {
|
||||||
|
spotifyUri = parsedUrl[0];
|
||||||
|
}
|
||||||
|
} else if (url.includes('spotify.com')) {
|
||||||
|
// determine if it's an album or playlist
|
||||||
|
if (!url.includes('/playlist/') && !url.includes('/album/')) {
|
||||||
|
return twitch.botChat.say(cmd.to, `Spotify URL must be a playlist or album!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the URL to get the resource type and ID
|
||||||
|
let parsedUrl = url.match(/(playlist|album)\/([A-Za-z0-9]{22})/);
|
||||||
|
if (parsedUrl !== null) {
|
||||||
|
spotifyUri = `spotify:${parsedUrl[1]}:${parsedUrl[2]}`;
|
||||||
|
} else {
|
||||||
|
return twitch.botChat.say(cmd.to, `Unable to parse spotify URL!`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return twitch.botChat.say(cmd.to, `Invalid spotify URL!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spotifyUri !== false) {
|
||||||
|
spotify.playContext(spotifyUri)
|
||||||
|
.then(res => twitch.botChat.say(cmd.to, `Changed playlist!`))
|
||||||
|
.catch(err => twitch.botChat.say(cmd.to, `Error changing playlist: ${JSON.stringify(err)}`));
|
||||||
|
} else {
|
||||||
|
return twitch.botChat.say(cmd.to, `Unable to parse Spotify URL!`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
songshuffle: (cmd) => {
|
||||||
|
let state = cmd.args[1] || true;
|
||||||
|
|
||||||
|
if (state === 'off' || state === 'false') {
|
||||||
|
state = false;
|
||||||
|
} else {
|
||||||
|
state = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
spotify.shuffle(state)
|
||||||
|
.then(res => twitch.botChat.say(cmd.to, `Updated shuffle state!`))
|
||||||
|
.catch(err => twitch.botChat.say(cmd.to, `Error changing shuffle state: ${JSON.stringify(err)}`))
|
||||||
|
},
|
||||||
|
|
||||||
|
songrepeat: (cmd) => {
|
||||||
|
let state = cmd.args[1] || false;
|
||||||
|
if (state === false) {
|
||||||
|
return twitch.botChat.say(cmd.to, `You must provide a repeat mode (track, context, or off)!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['track', 'context', 'off'].includes(state)) {
|
||||||
|
return twitch.botChat.say(cmd.to, `You must provide a valid repeat mode (track, context, or off)!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
spotify.repeat(state)
|
||||||
|
.then(res => twitch.botChat.say(cmd.to, `Updated repeat mode!`))
|
||||||
|
.catch(err => twitch.botChat.say(cmd.to, `Error changing repeat mode: ${JSON.stringify(err)}`))
|
||||||
|
},
|
||||||
|
|
||||||
reboot: (cmd) => {
|
reboot: (cmd) => {
|
||||||
console.log('Received request from admin to reboot...');
|
console.log('Received request from admin to reboot...');
|
||||||
twitch.botChat.say(cmd.to, 'Rebooting...');
|
twitch.botChat.say(cmd.to, 'Rebooting...');
|
||||||
@@ -347,10 +524,48 @@ const streamInit = (config, twitch) => {
|
|||||||
|
|
||||||
rngames: (cmd) => {
|
rngames: (cmd) => {
|
||||||
twitch.botChat.say(cmd.to, snesGames.sort(util.randSort).slice(0, 10).join(' | '));
|
twitch.botChat.say(cmd.to, snesGames.sort(util.randSort).slice(0, 10).join(' | '));
|
||||||
|
},
|
||||||
|
|
||||||
|
// voting to skip current video
|
||||||
|
skip: (cmd) => {
|
||||||
|
// check if there is an existing vote to skip for the director.state.currentVideo
|
||||||
|
if (skipVote.target === director.state.currentVideo.id) {
|
||||||
|
// if yes, add the vote, check if threshold is met, skip if necessary
|
||||||
|
skipVote.count++;
|
||||||
|
} else {
|
||||||
|
skipVote.target = director.state.currentVideo.id;
|
||||||
|
skipVote.count = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipVote.count >= config.skipVoteThreshold) {
|
||||||
|
director.skip();
|
||||||
|
skipVote.target = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
song: async (cmd) => {
|
||||||
|
spotify.getCurrentSong()
|
||||||
|
.then(async song => {
|
||||||
|
let artists = [];
|
||||||
|
await util.asyncForEach(song.artists, async (artist) => artists.push(artist.name));
|
||||||
|
twitch.botChat.say(cmd.to, `Current Song: ${artists.join(',')} - ${song.name} | ${song.url}`);
|
||||||
|
})
|
||||||
|
.catch(err => twitch.botChat.say(cmd.to, `Error retrieving current song: ${JSON.stringify(err)}`));
|
||||||
|
},
|
||||||
|
|
||||||
|
playlist: (cmd) => {
|
||||||
|
spotify.getCurrentPlaylist()
|
||||||
|
.then(playlist => twitch.botChat.say(cmd.to, `Current Playlist: ${playlist}`))
|
||||||
|
.catch(err => twitch.botChat.say(cmd.to, `Error retrieving current playlist: ${JSON.stringify(err)}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Aliases for chat commands
|
||||||
|
const aliases = {
|
||||||
|
"rooms": "room"
|
||||||
|
};
|
||||||
|
|
||||||
// Listen for the above commands
|
// Listen for the above commands
|
||||||
twitch.botChat.addListener('message', (from, to, message) => {
|
twitch.botChat.addListener('message', (from, to, message) => {
|
||||||
// Ignore everything from blacklisted users
|
// Ignore everything from blacklisted users
|
||||||
@@ -372,6 +587,12 @@ const streamInit = (config, twitch) => {
|
|||||||
// Ignore messages without a command
|
// Ignore messages without a command
|
||||||
if (!key || key.length === 0) return;
|
if (!key || key.length === 0) return;
|
||||||
|
|
||||||
|
// Case-insensitive
|
||||||
|
key.toLowerCase();
|
||||||
|
|
||||||
|
// Check for aliased commands
|
||||||
|
if (aliases.hasOwnProperty(key)) key = aliases[key];
|
||||||
|
|
||||||
// Ignore unrecognized commands
|
// Ignore unrecognized commands
|
||||||
if (!commands.admin.hasOwnProperty(key) && !commands.user.hasOwnProperty(key)) return;
|
if (!commands.admin.hasOwnProperty(key) && !commands.user.hasOwnProperty(key)) return;
|
||||||
|
|
||||||
@@ -398,6 +619,42 @@ const streamInit = (config, twitch) => {
|
|||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const manageTimer = (timerName, timerStatus) => {
|
||||||
|
// search timers for matching name
|
||||||
|
let theTimerIndex = timersList.findIndex(e => e.name === timerName);
|
||||||
|
if (theTimerIndex === -1) {
|
||||||
|
throw("Invalid timer name!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let theTimer = timersList[theTimerIndex];
|
||||||
|
|
||||||
|
// look in activeTimers for current status
|
||||||
|
let currentTimerIndex = activeTimers.findIndex(e => e.name === timerName);
|
||||||
|
|
||||||
|
if (!timerStatus || timerStatus !== 'on' || timerStatus !== 'off') {
|
||||||
|
// toggle by default
|
||||||
|
if (currentTimerIndex === -1) {
|
||||||
|
timerStatus = 'on';
|
||||||
|
} else {
|
||||||
|
timerStatus = 'off';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTimerIndex === -1 && timerStatus === 'on') {
|
||||||
|
let timerFunc = () => {
|
||||||
|
twitch.botChat.say(config.twitch.channel, theTimer.value);
|
||||||
|
};
|
||||||
|
let timerInterval = setInterval(timerFunc, theTimer.interval*1000);
|
||||||
|
activeTimers.push({name: theTimer.name, timer: timerInterval});
|
||||||
|
timerFunc();
|
||||||
|
} else if (timerStatus === 'off') {
|
||||||
|
clearInterval(activeTimers[currentTimerIndex].timer);
|
||||||
|
activeTimers.splice(currentTimerIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// @TODO: Modularize timed events
|
// @TODO: Modularize timed events
|
||||||
//console.log(`Initializing stream timers...`);
|
//console.log(`Initializing stream timers...`);
|
||||||
let userVotes = currentChoices = [];
|
let userVotes = currentChoices = [];
|
||||||
@@ -450,7 +707,7 @@ const streamInit = (config, twitch) => {
|
|||||||
// choose more random videos from config.vods.alttp (that aren't already in the queue)
|
// choose more random videos from config.vods.alttp (that aren't already in the queue)
|
||||||
// @TODO: Move into FGFM
|
// @TODO: Move into FGFM
|
||||||
let vodsNotInQueue = config.vods.alttp.filter(e => {
|
let vodsNotInQueue = config.vods.alttp.filter(e => {
|
||||||
let inQueue = director.state.videoQueue.findIndex(q => q.id === e.id) !== -1;
|
let inQueue = (director.state.videoQueue.findIndex(q => q.id === e.id) !== -1) && (director.state.currentVideo.id !== e.id);
|
||||||
return !inQueue;
|
return !inQueue;
|
||||||
});
|
});
|
||||||
currentChoices = vodsNotInQueue.sort(util.randSort).slice(0, config.videoPollSize);
|
currentChoices = vodsNotInQueue.sort(util.randSort).slice(0, config.videoPollSize);
|
||||||
|
|||||||
133
lib/fgfm.js
133
lib/fgfm.js
@@ -1,10 +1,13 @@
|
|||||||
const util = require('./util');
|
const util = require('./util'),
|
||||||
|
emitter = require('events').EventEmitter,
|
||||||
|
sysutil = require('util');
|
||||||
|
|
||||||
function FGFM(config) {
|
function FGFM(config) {
|
||||||
// Set up initial state
|
// Set up initial state
|
||||||
this.config = config.config;
|
this.config = config.config;
|
||||||
this.obs = config.obs;
|
this.obs = config.obs;
|
||||||
this.state = {
|
this.state = {
|
||||||
|
showStatus: 'IDLE',
|
||||||
videoQueue: [],
|
videoQueue: [],
|
||||||
recentlyPlayed: [],
|
recentlyPlayed: [],
|
||||||
currentVideo: null,
|
currentVideo: null,
|
||||||
@@ -13,14 +16,118 @@ function FGFM(config) {
|
|||||||
commercialPlaying: false
|
commercialPlaying: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
emitter.call(this);
|
||||||
|
|
||||||
|
this.startingSoon = (streamStartDelaySeconds, showStartDelaySeconds) => {
|
||||||
|
// @TODO: Move these defaults to config
|
||||||
|
if (typeof streamStartDelaySeconds === 'undefined') {
|
||||||
|
streamStartDelaySeconds = 1;
|
||||||
|
} else {
|
||||||
|
streamStartDelaySeconds = parseInt(streamStartDelaySeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof showStartDelaySeconds === 'undefined') {
|
||||||
|
showStartDelaySeconds = 300;
|
||||||
|
} else {
|
||||||
|
showStartDelaySeconds = parseInt(showStartDelaySeconds);
|
||||||
|
showStartDelaySeconds += streamStartDelaySeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.showStatus = 'STARTING_SOON';
|
||||||
|
|
||||||
|
// Set up the initial queue by randomly choosing the configured amount of vods included in shuffling
|
||||||
|
this.state.videoQueue = this.config.vods.alttp.filter(e => e.includeInShuffle === true).sort(util.randSort).slice(0, this.config.initialQueueSize);
|
||||||
|
|
||||||
|
// Show the starting-soon scene
|
||||||
|
this.obs.switchToScene('starting-soon');
|
||||||
|
|
||||||
|
// Restore volume
|
||||||
|
this.obs.setVolume('headphones', 1.0);
|
||||||
|
|
||||||
|
// Start the stream after delay
|
||||||
|
console.log(`The stream will start in ${streamStartDelaySeconds} seconds!`);
|
||||||
|
setTimeout(() => {this.obs.startStream().then(() => {this.emit('STREAM_STARTED')})}, streamStartDelaySeconds*1000);
|
||||||
|
|
||||||
|
// Start the "show" after stream+show delay
|
||||||
|
// @TODO: Actually show the countdown in the scene
|
||||||
|
console.log(`The show will start in ${showStartDelaySeconds} seconds!`);
|
||||||
|
setTimeout(this.startTheShow, showStartDelaySeconds*1000);
|
||||||
|
};
|
||||||
|
|
||||||
// Set up initial queue + start playback
|
// Set up initial queue + start playback
|
||||||
this.init = () => {
|
this.startTheShow = () => {
|
||||||
// Set up the initial queue by randomly choosing the configured amount of vods included in shuffling
|
// Set up the initial queue by randomly choosing the configured amount of vods included in shuffling
|
||||||
this.state.videoQueue = this.config.vods.alttp.filter(e => e.includeInShuffle === true).sort(util.randSort).slice(0, this.config.initialQueueSize);
|
this.state.videoQueue = this.config.vods.alttp.filter(e => e.includeInShuffle === true).sort(util.randSort).slice(0, this.config.initialQueueSize);
|
||||||
|
|
||||||
// Start queue playback
|
// Start queue playback
|
||||||
this.state.currentVideo = this.state.videoQueue.shift();
|
this.state.currentVideo = this.state.videoQueue.shift();
|
||||||
this.showVideo(this.state.currentVideo);
|
this.showVideo(this.state.currentVideo);
|
||||||
|
|
||||||
|
// restore volume
|
||||||
|
this.obs.setVolume('headphones', 1.0);
|
||||||
|
|
||||||
|
this.state.showStatus = 'RUNNING';
|
||||||
|
|
||||||
|
this.emit('SHOW_STARTED');
|
||||||
|
};
|
||||||
|
|
||||||
|
this.endTheShow = (creditsDelaySeconds, endDelaySeconds) => {
|
||||||
|
if (typeof creditsDelaySeconds === 'undefined' || creditsDelaySeconds === false) {
|
||||||
|
creditsDelaySeconds = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof endDelaySeconds === 'undefined' || endDelaySeconds === false) {
|
||||||
|
endDelaySeconds = 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Credits will be shown in ${creditsDelaySeconds} seconds!`);
|
||||||
|
this.emit('SHOW_ENDING', creditsDelaySeconds);
|
||||||
|
|
||||||
|
let end = () => {
|
||||||
|
this.state.showStatus = 'ENDING';
|
||||||
|
|
||||||
|
// Hide current video, don't play next video
|
||||||
|
this.obs.hide(this.state.currentVideo.sceneItem, this.config.defaultSceneName)
|
||||||
|
clearTimeout(this.state.videoTimer);
|
||||||
|
|
||||||
|
this.obs.switchToScene('credits')
|
||||||
|
.then(() => {
|
||||||
|
this.emit('CREDITS_SHOWN', endDelaySeconds);
|
||||||
|
|
||||||
|
if (endDelaySeconds < 5) endDelaySeconds = 5;
|
||||||
|
console.log(`Stream will stop in ${endDelaySeconds} seconds`);
|
||||||
|
let fadeOutDelay = endDelaySeconds - 5;
|
||||||
|
|
||||||
|
// Fade out volume with 5 seconds left
|
||||||
|
setTimeout(() => {
|
||||||
|
this.obs.getVolume('headphones')
|
||||||
|
.then(currentVolume => {
|
||||||
|
console.log(`current volume of headphones: ${currentVolume}`);
|
||||||
|
let step = 0.1;
|
||||||
|
while (currentVolume > 0.1) {
|
||||||
|
currentVolume -= step;
|
||||||
|
console.log(`setting volume to: ${currentVolume}`);
|
||||||
|
this.obs.setVolume('headphones', currentVolume);
|
||||||
|
util.sleep(250);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}, fadeOutDelay*1000);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.obs.stopStream();
|
||||||
|
this.state.showStatus = 'ENDED';
|
||||||
|
this.emit('SHOW_ENDED');
|
||||||
|
}, endDelaySeconds*1000);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (creditsDelaySeconds > 0) {
|
||||||
|
setTimeout(end, creditsDelaySeconds*1000);
|
||||||
|
} else {
|
||||||
|
end();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Shows.. a... video
|
// Shows.. a... video
|
||||||
@@ -71,6 +178,12 @@ function FGFM(config) {
|
|||||||
// Picks the next video in the queue (shuffles if empty)
|
// Picks the next video in the queue (shuffles if empty)
|
||||||
// Also handles "commercial breaks" if enabled
|
// Also handles "commercial breaks" if enabled
|
||||||
this.nextVideo = () => {
|
this.nextVideo = () => {
|
||||||
|
// @TODO: Validate this.state.showStatus -- make sure the "show" hasn't been paused or stopped
|
||||||
|
let ignoreStates = ['ENDING', 'ENDED', 'PAUSED'];
|
||||||
|
if (ignoreStates.includes(this.state.showStatus)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Show a "commercial break" if it's been long enough since the last one
|
// Show a "commercial break" if it's been long enough since the last one
|
||||||
let secondsSinceLastCommercial = (Date.now() - this.state.lastCommercialShownAt) / 1000;
|
let secondsSinceLastCommercial = (Date.now() - this.state.lastCommercialShownAt) / 1000;
|
||||||
if (this.config.commercialsEnabled === true && secondsSinceLastCommercial >= this.config.commercialInterval) {
|
if (this.config.commercialsEnabled === true && secondsSinceLastCommercial >= this.config.commercialInterval) {
|
||||||
@@ -142,6 +255,7 @@ function FGFM(config) {
|
|||||||
this.showCommercial = (video, callback) => {
|
this.showCommercial = (video, callback) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let handleFinish = () => {
|
let handleFinish = () => {
|
||||||
|
console.log('commercial is finished playing...');
|
||||||
this.state.commercialPlaying = false;
|
this.state.commercialPlaying = false;
|
||||||
if (typeof callback !== 'undefined') callback();
|
if (typeof callback !== 'undefined') callback();
|
||||||
}
|
}
|
||||||
@@ -191,7 +305,20 @@ function FGFM(config) {
|
|||||||
// Clears.. the... queue
|
// Clears.. the... queue
|
||||||
this.clearQueue = () => {
|
this.clearQueue = () => {
|
||||||
this.state.videoQueue = [];
|
this.state.videoQueue = [];
|
||||||
}
|
};
|
||||||
|
|
||||||
|
this.pause = () => {
|
||||||
|
this.state.showStatus = 'PAUSED';
|
||||||
|
this.emit('SHOW_PAUSED');
|
||||||
|
};
|
||||||
|
|
||||||
|
this.resume = () => {
|
||||||
|
this.state.showStatus = 'RUNNING';
|
||||||
|
this.nextVideo();
|
||||||
|
this.emit('SHOW_RESUMED');
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sysutil.inherits(FGFM, emitter);
|
||||||
|
|
||||||
module.exports = FGFM;
|
module.exports = FGFM;
|
||||||
|
|||||||
124
lib/ghobs.js
124
lib/ghobs.js
@@ -10,76 +10,100 @@ function GHOBS(config) {
|
|||||||
this.websocket.connect({ address: this.config.obs.websocket.address, password: this.config.obs.websocket.password })
|
this.websocket.connect({ address: this.config.obs.websocket.address, password: this.config.obs.websocket.password })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log(`Success! We're connected to OBS!`);
|
console.log(`Success! We're connected to OBS!`);
|
||||||
this.websocket.getCurrentScene().then(res => this.currentScene = res.name);
|
this.websocket.getCurrentScene().then(currentScene => this.currentScene = currentScene.name);
|
||||||
this.websocket.onSwitchScenes(data => {
|
this.websocket.onSwitchScenes(newScene => this.currentScene = newScene.sceneName);
|
||||||
//console.log(`New Active Scene: ${data.sceneName}`);
|
|
||||||
this.currentScene = data.sceneName;
|
|
||||||
});
|
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
|
|
||||||
// Listen for errors from OBS
|
// Listen for errors from OBS
|
||||||
|
// @TODO: Handle socket disconnect gracefully
|
||||||
|
/** { status: 'error',
|
||||||
|
description: 'There is no Socket connection available.',
|
||||||
|
code: 'NOT_CONNECTED',
|
||||||
|
error: 'There is no Socket connection available.' }*/
|
||||||
this.websocket.on('error', err => {
|
this.websocket.on('error', err => {
|
||||||
console.error(`OBS websocket error: ${JSON.stringify(err)}`);
|
console.error(`OBS websocket error: ${JSON.stringify(err)}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// @TODO: pass any unrecognized commands to the websocket
|
this.startStream = () => {
|
||||||
|
return this.websocket.startStreaming();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.stopStream = () => {
|
||||||
|
return this.websocket.stopStreaming();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setVolume = (source, volume) => {
|
||||||
|
return this.websocket.setVolume({source: source, volume: volume});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getVolume = (source) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.websocket.getVolume({source: source})
|
||||||
|
.then(res => {
|
||||||
|
resolve(res.volume);
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plays a video in the current scene and hides when finished
|
||||||
|
this.playVideo = (video, callback) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// @TODO Validation of video
|
||||||
|
|
||||||
|
// set the file path on the source
|
||||||
|
let sourceSettings = {
|
||||||
|
local_file: video.filePath,
|
||||||
|
looping: (typeof video.loops !== 'undefined' && video.loops > 1)
|
||||||
|
};
|
||||||
|
sourceSettings.loop = sourceSettings.looping;
|
||||||
|
|
||||||
|
this.websocket.setSourceSettings({"sourceName": video.sceneItem, "sourceSettings": sourceSettings})
|
||||||
|
// show the video scene item
|
||||||
|
.then(() => this.websocket.setSceneItemProperties({"item": video.sceneItem, "visible": true}))
|
||||||
|
// when the video is over, hide it and trigger the user callback, but resolve promise immediately with the timer
|
||||||
|
.then(() => {
|
||||||
|
// if this video is being looped, adjust timeout length to allow the requested number of loops to complete
|
||||||
|
if (sourceSettings.loop === true) {
|
||||||
|
video.length *= video.loops;
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve Promise with a timer of when the video will finish playback
|
||||||
|
// trigger user callback when the video finishes
|
||||||
|
let timer = setTimeout(() => {
|
||||||
|
this.websocket.setSceneItemProperties({"item": video.sceneItem, "visible": false});
|
||||||
|
if (typeof callback !== 'undefined') {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}, parseInt(video.length*1000));
|
||||||
|
|
||||||
|
resolve(timer);
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Shows a video in the given scene/item and then hides it and switches back to the original scene when finished
|
// Shows a video in the given scene/item and then hides it and switches back to the original scene when finished
|
||||||
this.playVideoInScene = (video, scene, callback) => {
|
this.playVideoInScene = (video, scene, callback) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
video.scene = scene;
|
||||||
let originalScene = this.currentScene || false;
|
let originalScene = this.currentScene || false;
|
||||||
//console.log(`Changing scene from ${originalScene} to ${scene}`);
|
let handleVideoEnd = () => {
|
||||||
this.websocket.setCurrentScene({"scene-name": scene})
|
if (originalScene !== false) {
|
||||||
.then(res => {
|
|
||||||
// set the file path on the source
|
|
||||||
//console.log(`Setting file path to: ${video.filePath}`);
|
|
||||||
let sourceSettings = {
|
|
||||||
"local_file": video.filePath,
|
|
||||||
"looping": (typeof video.loops !== 'undefined' && video.loops > 1)
|
|
||||||
};
|
|
||||||
sourceSettings.loop = sourceSettings.looping;
|
|
||||||
|
|
||||||
// @TODO loop room vids at a slower speed for a few iterations
|
|
||||||
// @TODO support any sourceSetting?
|
|
||||||
//
|
|
||||||
/*{ close_when_inactive: true,
|
|
||||||
local_file: 'Y:\\media\\videos\\ALttP\\my-vids\\room-vids\\11-mire\\38-wizzpot-rta-hook-610.mp4',
|
|
||||||
loop: true,
|
|
||||||
looping: false,
|
|
||||||
restart_on_activate: false,
|
|
||||||
speed_percent: 100 }*/
|
|
||||||
|
|
||||||
//this.websocket.getSourceSettings({"sourceName": video.sceneItem}).then(console.log);
|
|
||||||
|
|
||||||
this.websocket.setSourceSettings({"sourceName": video.sceneItem, "sourceSettings": sourceSettings})
|
|
||||||
// show the video scene item
|
|
||||||
.then(() => this.websocket.setSceneItemProperties({"item": video.sceneItem, "scene-name": scene, "visible": true}))
|
|
||||||
// when the video is over, hide it and trigger the user callback, but resolve promise immediately with the timer
|
|
||||||
.then(data => {
|
|
||||||
// adjust timeout length to allow the requested number of loops to complete
|
|
||||||
if (sourceSettings.loop === true) {
|
|
||||||
video.length *= video.loops;
|
|
||||||
console.log(`Video is set to loop, adjusted length to ${video.length}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(setTimeout(() => {
|
|
||||||
//console.log(`Hiding ${video.sceneItem}`);
|
|
||||||
this.websocket.setSceneItemProperties({"item": video.sceneItem, "scene-name": scene, "visible": false});
|
|
||||||
if (originalScene) {
|
|
||||||
//console.log(`Switching scene back to ${originalScene}`);
|
|
||||||
this.websocket.setCurrentScene({"scene-name": originalScene});
|
this.websocket.setCurrentScene({"scene-name": originalScene});
|
||||||
}
|
}
|
||||||
if (typeof callback !== 'undefined') {
|
if (typeof callback !== 'undefined') {
|
||||||
//console.log('Triggering user callback');
|
callback();
|
||||||
callback(data);
|
|
||||||
}
|
}
|
||||||
}, parseInt(video.length*1000)))
|
};
|
||||||
});
|
|
||||||
})
|
this.websocket.setCurrentScene({"scene-name": scene})
|
||||||
|
.then(() => this.playVideo(video, handleVideoEnd))
|
||||||
|
.then(timer => { resolve(timer) })
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
134
lib/spotify.js
Executable file
134
lib/spotify.js
Executable file
@@ -0,0 +1,134 @@
|
|||||||
|
var SpotifyWebApi = require('spotify-web-api-node');
|
||||||
|
|
||||||
|
function Spotify(config) {
|
||||||
|
// Set up initial state
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
this.credentials = {
|
||||||
|
clientId: this.config.clientId,
|
||||||
|
clientSecret: this.config.clientSecret,
|
||||||
|
redirectUri: this.config.redirectUri
|
||||||
|
};
|
||||||
|
|
||||||
|
const spotifyApi = new SpotifyWebApi(this.credentials);
|
||||||
|
|
||||||
|
// The code that's returned as a query parameter to the redirect URI
|
||||||
|
const code = this.config.userCode;
|
||||||
|
|
||||||
|
this.init = () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Retrieve an access token and a refresh token
|
||||||
|
spotifyApi.authorizationCodeGrant(code).then(
|
||||||
|
function(data) {
|
||||||
|
console.log('The token expires in ' + data.body['expires_in']);
|
||||||
|
console.log('The access token is ' + data.body['access_token']);
|
||||||
|
console.log('The refresh token is ' + data.body['refresh_token']);
|
||||||
|
|
||||||
|
// Set the access token on the API object to use it in later calls
|
||||||
|
spotifyApi.setAccessToken(data.body['access_token']);
|
||||||
|
spotifyApi.setRefreshToken(data.body['refresh_token']);
|
||||||
|
|
||||||
|
// clientId, clientSecret and refreshToken has been set on the api object previous to this call.
|
||||||
|
setInterval(() => {
|
||||||
|
spotifyApi.refreshAccessToken().then(
|
||||||
|
function(data) {
|
||||||
|
console.log('The access token has been refreshed!');
|
||||||
|
|
||||||
|
// Save the access token so that it's used in future calls
|
||||||
|
spotifyApi.setAccessToken(data.body['access_token']);
|
||||||
|
},
|
||||||
|
function(err) {
|
||||||
|
console.log('Could not refresh access token', err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, data.body['expires_in']*1000);
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
function(err) {
|
||||||
|
console.log('Something went wrong!', JSON.stringify(err));
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getMe = () => {
|
||||||
|
spotifyApi.getMe()
|
||||||
|
.then(function(data) {
|
||||||
|
console.log('Some information about the authenticated user', data.body);
|
||||||
|
}, function(err) {
|
||||||
|
console.log('Something went wrong!', err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getCurrentSong = () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
spotifyApi.getMyCurrentPlaybackState({}, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = data.body;
|
||||||
|
resolve({
|
||||||
|
artists: state.item.artists,
|
||||||
|
name: state.item.name,
|
||||||
|
album: state.item.album.name,
|
||||||
|
url: state.item.external_urls.spotify
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getCurrentPlaylist = () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
spotifyApi.getMyCurrentPlaybackState({}, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = data.body;
|
||||||
|
if (state.context) {
|
||||||
|
resolve(state.context.external_urls.spotify);
|
||||||
|
} else {
|
||||||
|
resolve(state.item.album.external_urls.spotify);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.playContext = (uri) => {
|
||||||
|
return spotifyApi.play({"context_uri": uri});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.skip = () => {
|
||||||
|
return spotifyApi.skipToNext();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.pause = () => {
|
||||||
|
return spotifyApi.pause();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.resume = () => {
|
||||||
|
return spotifyApi.play();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setVolume = (volume) => {
|
||||||
|
volume = parseInt(volume);
|
||||||
|
if (volume < 0) volume = 0;
|
||||||
|
if (volume > 100) volume = 100;
|
||||||
|
return spotifyApi.setVolume(volume);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.shuffle = (state) => {
|
||||||
|
return spotifyApi.setShuffle({"state": state});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.repeat = (state) => {
|
||||||
|
return spotifyApi.setRepeat({"state": state});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Spotify;
|
||||||
@@ -49,3 +49,12 @@ exports.average = function(e) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
exports.randSort = () => { return 0.5 - Math.random() };
|
exports.randSort = () => { return 0.5 - Math.random() };
|
||||||
|
|
||||||
|
exports.sleep = (milliseconds) => {
|
||||||
|
var start = new Date().getTime();
|
||||||
|
for (var i = 0; i < 1e7; i++) {
|
||||||
|
if ((new Date().getTime() - start) > milliseconds){
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
143
package-lock.json
generated
143
package-lock.json
generated
@@ -116,11 +116,29 @@
|
|||||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||||
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
|
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
|
||||||
},
|
},
|
||||||
|
"combined-stream": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
|
||||||
|
"requires": {
|
||||||
|
"delayed-stream": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"commander": {
|
"commander": {
|
||||||
"version": "2.11.0",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
|
||||||
"integrity": "sha1-FXFS/R56bI2YpbcVzzdt+SgARWM="
|
"integrity": "sha1-FXFS/R56bI2YpbcVzzdt+SgARWM="
|
||||||
},
|
},
|
||||||
|
"component-emitter": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
|
||||||
|
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
|
||||||
|
},
|
||||||
|
"cookiejar": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA=="
|
||||||
|
},
|
||||||
"core-js": {
|
"core-js": {
|
||||||
"version": "2.5.7",
|
"version": "2.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
|
||||||
@@ -203,6 +221,11 @@
|
|||||||
"safer-buffer": "2.1.2"
|
"safer-buffer": "2.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"extend": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||||
|
},
|
||||||
"extsprintf": {
|
"extsprintf": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
||||||
@@ -237,6 +260,21 @@
|
|||||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||||
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
|
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
|
||||||
},
|
},
|
||||||
|
"form-data": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
|
||||||
|
"requires": {
|
||||||
|
"asynckit": "0.4.0",
|
||||||
|
"combined-stream": "1.0.7",
|
||||||
|
"mime-types": "2.1.21"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formidable": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg=="
|
||||||
|
},
|
||||||
"getpass": {
|
"getpass": {
|
||||||
"version": "0.1.7",
|
"version": "0.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
||||||
@@ -316,6 +354,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||||
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
|
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
|
||||||
},
|
},
|
||||||
|
"isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||||
|
},
|
||||||
"isexe": {
|
"isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
@@ -388,6 +431,29 @@
|
|||||||
"resolved": "https://registry.npmjs.org/memcache/-/memcache-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/memcache/-/memcache-0.3.0.tgz",
|
||||||
"integrity": "sha1-vbuXjqS+4P3TFmmXsYg9KX4IWdw="
|
"integrity": "sha1-vbuXjqS+4P3TFmmXsYg9KX4IWdw="
|
||||||
},
|
},
|
||||||
|
"methods": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||||
|
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
|
||||||
|
},
|
||||||
|
"mime": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
|
||||||
|
},
|
||||||
|
"mime-db": {
|
||||||
|
"version": "1.37.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
|
||||||
|
"integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg=="
|
||||||
|
},
|
||||||
|
"mime-types": {
|
||||||
|
"version": "2.1.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
|
||||||
|
"integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==",
|
||||||
|
"requires": {
|
||||||
|
"mime-db": "1.37.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"moment": {
|
"moment": {
|
||||||
"version": "2.22.2",
|
"version": "2.22.2",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
|
||||||
@@ -508,6 +574,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-0.0.3.tgz",
|
||||||
"integrity": "sha512-c9KkNifSMU/iXT8FFTaBwBMr+rdVcN+H/uNv1o+CuFeTThNZNTOrQ+RgXA1yL/DeLk098duAeRPP3QNPNbhxYQ=="
|
"integrity": "sha512-c9KkNifSMU/iXT8FFTaBwBMr+rdVcN+H/uNv1o+CuFeTThNZNTOrQ+RgXA1yL/DeLk098duAeRPP3QNPNbhxYQ=="
|
||||||
},
|
},
|
||||||
|
"process-nextick-args": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
|
||||||
|
},
|
||||||
"psl": {
|
"psl": {
|
||||||
"version": "1.1.29",
|
"version": "1.1.29",
|
||||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz",
|
"resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz",
|
||||||
@@ -518,6 +589,25 @@
|
|||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||||
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
|
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
|
||||||
},
|
},
|
||||||
|
"qs": {
|
||||||
|
"version": "6.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||||
|
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
|
||||||
|
},
|
||||||
|
"readable-stream": {
|
||||||
|
"version": "2.3.6",
|
||||||
|
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||||
|
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||||
|
"requires": {
|
||||||
|
"core-util-is": "1.0.2",
|
||||||
|
"inherits": "2.0.3",
|
||||||
|
"isarray": "1.0.0",
|
||||||
|
"process-nextick-args": "2.0.0",
|
||||||
|
"safe-buffer": "5.1.2",
|
||||||
|
"string_decoder": "1.1.1",
|
||||||
|
"util-deprecate": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ref": {
|
"ref": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/ref/-/ref-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/ref/-/ref-1.3.5.tgz",
|
||||||
@@ -643,6 +733,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.2.0.tgz",
|
||||||
"integrity": "sha512-sWpjPhIZJtqO77GN+LD8dDsDKcWZ9GCOJNqKzi1tvtjGIzwfoyuRH8S0psunmc6Z5P+qfDqztSbwYR5X/e1UTg=="
|
"integrity": "sha512-sWpjPhIZJtqO77GN+LD8dDsDKcWZ9GCOJNqKzi1tvtjGIzwfoyuRH8S0psunmc6Z5P+qfDqztSbwYR5X/e1UTg=="
|
||||||
},
|
},
|
||||||
|
"spotify-web-api-node": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/spotify-web-api-node/-/spotify-web-api-node-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-FQAX4qiP9xfjmJpkSfF5PEVr7RVorUZiLvcdVTlhVFLYAmQ8VSsZlyb0yTK0GExKhAcgJy9GfWxqjSB2r9SrjA==",
|
||||||
|
"requires": {
|
||||||
|
"superagent": "3.8.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sshpk": {
|
"sshpk": {
|
||||||
"version": "1.14.2",
|
"version": "1.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz",
|
||||||
@@ -659,6 +757,46 @@
|
|||||||
"tweetnacl": "0.14.5"
|
"tweetnacl": "0.14.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
|
"requires": {
|
||||||
|
"safe-buffer": "5.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"superagent": {
|
||||||
|
"version": "3.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz",
|
||||||
|
"integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==",
|
||||||
|
"requires": {
|
||||||
|
"component-emitter": "1.2.1",
|
||||||
|
"cookiejar": "2.1.2",
|
||||||
|
"debug": "3.2.6",
|
||||||
|
"extend": "3.0.2",
|
||||||
|
"form-data": "2.3.3",
|
||||||
|
"formidable": "1.2.1",
|
||||||
|
"methods": "1.1.2",
|
||||||
|
"mime": "1.6.0",
|
||||||
|
"qs": "6.5.2",
|
||||||
|
"readable-stream": "2.3.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"debug": {
|
||||||
|
"version": "3.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||||
|
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||||
|
"requires": {
|
||||||
|
"ms": "2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"tough-cookie": {
|
"tough-cookie": {
|
||||||
"version": "2.4.3",
|
"version": "2.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
|
||||||
@@ -682,6 +820,11 @@
|
|||||||
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
|
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||||
|
},
|
||||||
"uuid": {
|
"uuid": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
"node-opus": "^0.2.9",
|
"node-opus": "^0.2.9",
|
||||||
"node-schedule": "^1.3.0",
|
"node-schedule": "^1.3.0",
|
||||||
"obs-websocket-js": "^1.2.0",
|
"obs-websocket-js": "^1.2.0",
|
||||||
"request": "^2.88.0"
|
"request": "^2.88.0",
|
||||||
|
"spotify-web-api-node": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {},
|
"devDependencies": {},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
BIN
sfx/mouthfeel.mp3
Executable file
BIN
sfx/mouthfeel.mp3
Executable file
Binary file not shown.
BIN
sfx/wobbuffet.mp3
Executable file
BIN
sfx/wobbuffet.mp3
Executable file
Binary file not shown.
19
spotify-auth-url.js
Executable file
19
spotify-auth-url.js
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
var SpotifyWebApi = require('spotify-web-api-node');
|
||||||
|
let config = require('./config.json');
|
||||||
|
|
||||||
|
var scopes = ['streaming', 'app-remote-control', 'user-read-currently-playing', 'user-read-playback-state', 'user-modify-playback-state', 'user-read-recently-played', 'playlist-read-collaborative', 'playlist-modify-private', 'playlist-modify-public', 'playlist-read-private'],
|
||||||
|
redirectUri = 'http://forevergrind.fm/spotify',
|
||||||
|
clientId = config.spotify.clientId,
|
||||||
|
state = 'some-state-of-my-choice';
|
||||||
|
|
||||||
|
// Setting credentials can be done in the wrapper's constructor, or using the API object's setters.
|
||||||
|
var spotifyApi = new SpotifyWebApi({
|
||||||
|
redirectUri: redirectUri,
|
||||||
|
clientId: clientId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the authorization URL
|
||||||
|
var authorizeURL = spotifyApi.createAuthorizeURL(scopes, state);
|
||||||
|
|
||||||
|
// https://accounts.spotify.com:443/authorize?client_id=5fe01282e44241328a84e7c5cc169165&response_type=code&redirect_uri=https://example.com/callback&scope=user-read-private%20user-read-email&state=some-state-of-my-choice
|
||||||
|
console.log(authorizeURL);
|
||||||
Reference in New Issue
Block a user