; AutoHotKey chat integration and auto-scene switcher for The Cichlid Show
#InstallMouseHook
#InstallKeybdHook
#Persistent
#SingleInstance force
#NoEnv
#UseHook On
#Include c:\seneye\json\AutoHotkey-JSON-master\JSON.ahk
Menu, Tray, Icon, shell32.dll, 110
SetTitleMatchMode, 2
DetectHiddenWindows, On
; global storm API call enabler
storm_enabled := "NO"
scenes_by_name := Object()
scenes_by_name.6 := "QUAD"
scenes_by_name.5 := "FRY"
scenes_by_name.4 := "SIDES"
scenes_by_name.3 := "FRONT"
scenes_by_name.2 := "LEFT"
scenes_by_name.1 := "RIGHT"
; scene ############################
; scene hotkey mappings
QUAD := 6
FRY := 5
SIDES := 4
FRONT := 3
LEFT := 2
RIGHT := 1
DAYTIME := [
, [FRONT,20]
, [FRY,10]
, [RIGHT,15]
, [FRONT,10]
, [LEFT,15] ]
NIGHTTIME := [
, [FRONT,30]
, [RIGHT,30]
, [LEFT,30] ]
STORM := [
, [LEFT,10]
, [FRONT,10]
, [RIGHT,10] ]
slog("mainloop", "starting up")
; YouTube Live Stream Chat API
api_url := <REMOVED>
oplog_messages := ""
message := Object()
; by default we show the oplog
oplog := "true"
; MAINLOOP ####################
Loop {
mode := GetRotationMode()
; GetBlueFishConditions()
; default to auto mode but look for commands every 1 second
For key, value in %mode%
{
GetViewersCount()
WriteOperatorStatus()
result := ProcessChat()
if result.storm
{
mode := GetRotationMode()
slog("debug","storm rotation enabled")
break
}
if result.newProfile
{
mode := GetRotationMode()
slog("debug","new scene requested: " . mode)
break
}
scene := value.1
duration := value.2
RotateAuto(scene, duration, mode)
}
}
; FUNCS ########################
GetBlueFishConditions()
{
; REMOVED
}
slog(log_type,message) {
file := FileOpen("c:\seneye\system_log.txt", "a")
FormatTime, TimeString, R
file.WriteLine(TimeString . " " . log_type . ": " message)
file.Close()
}
SetRotationMode(mode) {
if not mode
{
mode := "DAYTIME"
}
file := FileOpen("c:\seneye\current_mode.cfg", "w")
file.Write(mode)
file.Close()
}
GetRotationMode() {
FileRead, mode, c:\seneye\current_mode.cfg
Return mode
}
ProcessChat() {
global api_url, message, oplog, oplog_messages
user_duration := 60
command := Object()
; download "messages" api via IE com object
WinHttpReq := ComObjCreate("WinHttp.WinHttpRequest.5.1")
WinHttpReq.SetTimeouts("1000", "1000", "1000", "1000")
Try {
WinHttpReq.Open("GET", api_url, false)
WinHttpReq.Send()
WinHttpReq.WaitForResponse()
json_string := WinHttpReq.ResponseText
}
Catch e{
For Each, Line in StrSplit(e.Message, "`n", "") {
Results := InStr(Line, "Description:")
? StrReplace(Line, "Description:")
: ""
If (Results <> "")
Break
}
slog("messages", "youtube live api error")
}
; load JSON YouTube chat data (if any) and parse
parsed_json_string := JSON.Load(json_string)
for index, ele in % parsed_json_string.items
{
id := parsed_json_string.items[index].id
authorChannelId := parsed_json_string.items[index].snippet.authorChannelId
time := parsed_json_string.items[index].snippet.publishedAt
text := parsed_json_string.items[index].snippet.displayMessage
; parse this message only if it's new
If not message[id]
{
slog("messages", "new message found: " . text)
message[id] := id
; if #STORMSTART and #STORMSTOP
IfInString, text, `#STORMSTART
{
command.storm := "START"
slog("messages", "new message found: " . text)
dostorm("start")
}
IfInString, text, `#STORMSTOP
{
command.storm := "STOP"
slog("messages", "new message found: " . text)
dostorm("stop")
}
; if #LOG then show #LOG
IfInString, text, `#SHOWLOG
{
slog("messages", "new message found: " . text)
oplog := "true"
command.toggleoplog := 1
}
; if #HIDELOG then hide #LOG
IfInString, text, `#HIDELOG
{
slog("messages", "new message found: " . text)
oplog := "false"
command.toggleoplog := 1
}
; admin commands
IfInString, authorChannelId, <REMOVED>
{
slog("admin", "new admin command found: " . text)
IfInString, text, `#LOG
{
; remove "#LOG"
StringReplace, text_trimmed, text, `#LOG
; add time to beginning
parsed_time := DateParse(time)
FormatTime, formatted_parsed_time, parsed_time, hh:mm tt' '
oplog_messages = %oplog_messages%%formatted_parsed_time% %text_trimmed%`r`n
slog("admin", "new activity log found " . text_trimmed)
command.newoplog := 1
}
IfInString, text, `#CLEARLOG
{
oplog_messages := ""
slog("admin", "activity log cleared")
command.newoplog := 1
}
IfInString, text, `#DAYTIME
{
slog("messages", "new profile requested " . text)
command.newProfile := "DAYTIME"
SetRotationMode("DAYTIME")
}
IfInString, text, `#NIGHTTIME
{
slog("messages", "new profile requested " . text)
command.newProfile := "NIGHTTIME"
SetRotationMode("NIGHTTIME")
}
}
; write the current log string to disk
WriteOpLog()
; if scene command found in chat then rotate cam to selection
IfInString, text, `#FRONT
{
slog("messages", "new command found " . text)
command.userScene := 1
RotateUser(3,user_duration)
}
IfInString, text, `#LEFT
{
slog("messages", "new command found " . text)
command.userScene := 1
RotateUser(2,user_duration)
}
IfInString, text, `#RIGHT
{
slog("messages", "new command found " . text)
command.userScene := 1
RotateUser(1,user_duration)
}
IfInString, text, `#SIDES
{
slog("messages", "new command found " . text)
command.userScene := 1
RotateUser(4,user_duration)
}
IfInString, text, `#FRY
{
slog("messages", "new command found " . text)
command.userScene := 1
RotateUser(5,user_duration)
}
IfInString, text, `#QUAD
{
slog("messages", "new command found " . text)
command.userScene := 1
RotateUser(6,user_duration)
}
}
}
Return command
}
dostorm(state) {
/* implemented with help from:
* [USER=56035]@author[/USER] Spencer Shepard
* @copyright 2016
<REMOVED>
*/
}
StormTimerEnd(mode) {
SetRotationMode(mode)
slog("storm", "storm ended - switching profile back to " . mode)
Return
}
GetViewersCount() {
; get number of current viewers from YouTube
live_stats := "https://www.youtube.com/live_stats?v=yqqQDRy2zpE"
WinHttpReq := ComObjCreate("WinHttp.WinHttpRequest.5.1")
WinHttpReq.SetTimeouts("1000", "1000", "1000", "1000")
Try {
WinHttpReq.Open("GET", live_stats, true)
WinHttpReq.Send()
WinHttpReq.WaitForResponse()
live_stats_string := WinHttpReq.ResponseText
}
Catch e{
For Each, Line in StrSplit(e.Message, "`n", "") {
Results := InStr(Line, "Description:")
? StrReplace(Line, "Description:")
: ""
If (Results <> "")
Break
}
live_stats_string := "-"
slog("viewerscount", "youtube live api error")
}
file := FileOpen("c:\seneye\youtube_viewers.txt", "w")
file.WriteLine(live_stats_string . " watching")
file.Close()
Return live_stats_string
}
RotateAuto(scene,duration,mode) {
global scenes_by_name, message, oplog
WinActivate OBS
Send "!%scene%"
while duration
{
WriteSceneState(duration,scenes_by_name[scene],mode)
Sleep, 1000
duration := duration - 1
}
Return
}
RotateUser(scene,duration) {
global scenes_by_name, message, oplog
WinActivate OBS
Send "!%scene%"
while duration
{
WriteOpLog()
WriteSceneState(duration,scenes_by_name[scene],"YouTube Live Chat")
Sleep, 1000
duration := duration - 1
}
Return
}
WriteOpLog() {
global oplog_messages, oplog
; write #LOG to disk if #SHOWLOG enabled
If (oplog == "true")
{
if (oplog_messages)
{
file := FileOpen("c:\seneye\operator_messages.txt", "w")
file.WriteLine("(type #HIDELOG in chat to hide this activity feed)")
file.Write(oplog_messages)
file.Close()
}
else
{
file := FileOpen("c:\seneye\operator_messages.txt", "w")
file.WriteLine("no recent activity")
file.Close()
}
}
else
{
; otherwise write an blank file
file := FileOpen("c:\seneye\operator_messages.txt", "w")
file.WriteLine("")
file.Close()
}
}
WriteOperatorStatus() {
; if afk for more than 2 minutes then write a message
if (A_TimeIdlePhysical > 120000)
{
file := FileOpen("c:\seneye\operator_status.txt", "w")
file.WriteLine("admin AFK for " . GetAFKduration(A_TimeIdlePhysical))
file.Close()
}
else
{
file := FileOpen("c:\seneye\operator_status.txt", "w")
file.WriteLine("admin online")
file.Close()
}
}
GetAFKduration(milliseconds) { ; converts milli-seconds to hrs mins and secs
time:=milliseconds/1000
timestring:=ceil((time//3600)) . "h " . ceil((time//60)) . "m"
return timestring
}
WriteSceneState(duration,scene,source) {
file := FileOpen("c:\seneye\scene_state.txt", "w")
; DAYTIME #FRONT (next scene: 12)
file.WriteLine(source . " #" . scene . " (next scene:" . duration . ")")
file.Close()
Return
}
+Space::
Suspend
file := FileOpen("c:\seneye\scene_state.txt", "w")
file.WriteLine("SCENE ROTATION PAUSED")
file.Close()
slog("mainloop", "paused from keyboard")
Pause ,, 1
Return
DateParse(str) {
static e2 = "i)(?:(\d{1,2}+)[\s\.\-\/,]+)?(\d{1,2}|(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w*)[\s\.\-\/,]+(\d{2,4})"
str := RegExReplace(str, "((?:" . SubStr(e2, 42, 47) . ")\w*)(\s*)(\d{1,2})\b", "$3$2$1", "", 1)
If RegExMatch(str, "i)^\s*(?:(\d{4})([\s\-:\/])(\d{1,2})\2(\d{1,2}))?"
. "(?:\s*[T\s](\d{1,2})([\s\-:\/])(\d{1,2})(?:\6(\d{1,2})\s*(?:(Z)|(\+|\-)?"
. "(\d{1,2})\6(\d{1,2})(?:\6(\d{1,2}))?)?)?)?\s*$", i)
d3 := i1, d2 := i3, d1 := i4, t1 := i5, t2 := i7, t3 := i8
Else If !RegExMatch(str, "^\W*(\d{1,2}+)(\d{2})\W*$", t)
RegExMatch(str, "i)(\d{1,2})\s*:\s*(\d{1,2})(?:\s*(\d{1,2}))?(?:\s*([ap]m))?", t)
, RegExMatch(str, e2, d)
f = %A_FormatFloat%
SetFormat, Float, 02.0
d := (d3 ? (StrLen(d3) = 2 ? 20 : "") . d3 : A_YYYY)
. ((d2 := d2 + 0 ? d2 : (InStr(e2, SubStr(d2, 1, 3)) - 40) // 4 + 1.0) > 0
? d2 + 0.0 : A_MM) . ((d1 += 0.0) ? d1 : A_DD) . t1
+ (t1 = 12 ? t4 = "am" ? -12.0 : 0.0 : t4 = "am" ? 0.0 : 12.0) . t2 + 0.0 . t3 + 0.0
SetFormat, Float, %f%
Return, d
}