Ready for early adopter release
Signed-off-by: Martyn Ranyard <m@rtyn.berlin>
This commit is contained in:
parent
d13c00eff0
commit
ee0b2328b2
|
@ -1,14 +1,14 @@
|
||||||
irc:
|
irc:
|
||||||
password: ENC[AES256_GCM,data:bmVyAcxlCKaL8MkttQWipktXkoU637pSRuEZHETvkFBdRX4p,iv:NSZ6TtlXcxy7cUtKZ1MrB4hAX4FwNV5LuPNViFd0jh0=,tag:4Ex7p59w2FFkFkp4yEhZ0w==,type:str]
|
password: ENC[AES256_GCM,data:oxd4te1D31VaTea8a+fw8DgiPQBuaIAyYjKTRUL9ywR+DPGY,iv:ZV9agOpxGzu+wK/X8ThXHQgNjkytKpqf8I7kcMtVBcQ=,tag:1ntk+4gVQfj9g1CNYpuh0g==,type:str]
|
||||||
twitchapp:
|
twitchapp:
|
||||||
client_id: ENC[AES256_GCM,data:Iy1Wb5WevXQtDpDPTKKkpYez+WWOGOUDre89qf1Y,iv:z/ZbcgADO5vDQOjX/A6xU99lGiD2k3qxor31KnVFE4o=,tag:pgf67HdxOjUSWEktgPfolw==,type:str]
|
client_id: ENC[AES256_GCM,data:XlpiZQX6oY/frhlOg/sA85AuB1JmlLpHX2yBp84y,iv:SI/elJatih9J289rYXi+wzyYy38SAyt+DfHqWfQy56c=,tag:LIq3dYLAd/isLygBsh2gYw==,type:str]
|
||||||
client_secret: ENC[AES256_GCM,data:5fY77ccyfqZMh30hMdhXy1h7fio4763NLgYu9tgP,iv:kLyEULWcgKHwpPxoBX69IcHAaeAob9AN8gD5/K5nIyg=,tag:nxtZMuCZdGyVhBBvgRz2SA==,type:str]
|
client_secret: ENC[AES256_GCM,data:E5zgitQ0cq+MDQsc5gjAyShwgoRS4TwewzLRus7M,iv:9TKxA2sASAvwbUOVVB3UPgowyBjUmLJA/tJCEpt2XGc=,tag:CIZw2pf3mBxASQLO48zJtQ==,type:str]
|
||||||
sops:
|
sops:
|
||||||
kms: []
|
kms: []
|
||||||
gcp_kms: []
|
gcp_kms: []
|
||||||
azure_kv: []
|
azure_kv: []
|
||||||
lastmodified: '2020-07-14T18:03:02Z'
|
lastmodified: '2020-07-15T20:38:41Z'
|
||||||
mac: ENC[AES256_GCM,data:quUrWcjviC08daW0k+Y0a6oB+ZXT8NkF2z1cni2DeFUuSXjrxDQcMY3POj45DbzHrojXMkJKOk6JLOcj9oV1h8uCqyZ/rGWhgfly1YNLJEP8LcJbCQt2jUAhctxaQkvPtqIak7e/ABKHhMuKmBbNWzhTLBbyl2/YR2+pPsLxKQA=,iv:PbRPKBaLtns4+M0ydrVTecVHgtR3q7TkhKWN01cEhVE=,tag:nCK7gSQ+F97lJJhCOol7zw==,type:str]
|
mac: ENC[AES256_GCM,data:8zIsya5YjfW/R1xD8Q2B1yIoXEOXJsDCqAeicFyNL6zkxPSCZSv5hJfBDH0kmNuAfR9oG7+XCqQbhNM8bVrNWbhYILc8aJuexth67riuVibWvg0r2xdMnozJYvJ6u1rRJ9xwRtYvBzjqK6SMuq5mkD2uq7wcmJr0rNoWjnXhuVc=,iv:wMQ8HgfskVGlt+x5t+ah+Yo1b0gbV/6jE1APBrxhdS8=,tag:JZXTby+v8meFEwbSBOyWlg==,type:str]
|
||||||
pgp:
|
pgp:
|
||||||
- created_at: '2020-07-14T18:02:04Z'
|
- created_at: '2020-07-14T18:02:04Z'
|
||||||
enc: |
|
enc: |
|
||||||
|
|
|
@ -35,7 +35,7 @@ securityContext: {}
|
||||||
secretFiles: {}
|
secretFiles: {}
|
||||||
|
|
||||||
irc:
|
irc:
|
||||||
nick: twitchsingstools
|
nick: tstools
|
||||||
|
|
||||||
twitchapp: {}
|
twitchapp: {}
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ service:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
port: 80
|
port: 80
|
||||||
|
|
||||||
externalHostname: twitchsingstools.ing.martyn.berlin
|
externalHostname: twitchsingstools.martyn.berlin
|
||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -56,10 +56,16 @@ ingress:
|
||||||
- host: twitchsingstools.ing.martyn.berlin
|
- host: twitchsingstools.ing.martyn.berlin
|
||||||
paths:
|
paths:
|
||||||
- /
|
- /
|
||||||
|
- host: twitchsingstools.martyn.berlin
|
||||||
|
paths:
|
||||||
|
- /
|
||||||
tls:
|
tls:
|
||||||
- secretName: tstools-tls
|
- secretName: tstools-tls
|
||||||
hosts:
|
hosts:
|
||||||
- twitchsingstools.ing.martyn.berlin
|
- twitchsingstools.ing.martyn.berlin
|
||||||
|
- secretName: tstools-secondary-tls
|
||||||
|
hosts:
|
||||||
|
- twitchsingstools.martyn.berlin
|
||||||
|
|
||||||
resources: {}
|
resources: {}
|
||||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||||
|
|
|
@ -106,6 +106,7 @@ type ChannelData struct {
|
||||||
VideoCache []SingsVideoStruct `json:"videoCache"`
|
VideoCache []SingsVideoStruct `json:"videoCache"`
|
||||||
VideoCacheUpdated time.Time `json:"videoCacheUpdated"`
|
VideoCacheUpdated time.Time `json:"videoCacheUpdated"`
|
||||||
Bearer string `json:"bearer"`
|
Bearer string `json:"bearer"`
|
||||||
|
TwitchUserID string `json:"twitchUserID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connects the bot to the Twitch IRC server. The bot will continue to try to connect until it
|
// Connects the bot to the Twitch IRC server. The bot will continue to try to connect until it
|
||||||
|
@ -159,6 +160,13 @@ func (bb *KardBot) UpdateBearerToken(user string, token string) {
|
||||||
bb.ChannelData[user] = record
|
bb.ChannelData[user] = record
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bb *KardBot) UpdateTwitchUserID(user string, userid string) {
|
||||||
|
record := bb.ChannelData[user]
|
||||||
|
record.TwitchUserID = userid
|
||||||
|
bb.Database.Write("channelData", user, record)
|
||||||
|
bb.ChannelData[user] = record
|
||||||
|
}
|
||||||
|
|
||||||
// Listens for and logs messages from chat. Responds to commands from the channel owner. The bot
|
// Listens for and logs messages from chat. Responds to commands from the channel owner. The bot
|
||||||
// continues until it gets disconnected, told to shutdown, or forcefully shutdown.
|
// continues until it gets disconnected, told to shutdown, or forcefully shutdown.
|
||||||
func (bb *KardBot) HandleChat() error {
|
func (bb *KardBot) HandleChat() error {
|
||||||
|
|
|
@ -108,10 +108,6 @@ func TemplateHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
ClientID string
|
ClientID string
|
||||||
BaseURI string
|
BaseURI string
|
||||||
}
|
}
|
||||||
// tmpl, err := template.New("html"+request.URL.Path).Funcs(template.FuncMap{
|
|
||||||
// "ToUpper": strings.ToUpper,
|
|
||||||
// "ToLower": strings.ToLower,
|
|
||||||
// }).ParseFiles("html"+request.URL.Path)
|
|
||||||
_ = strings.ToLower("Hello")
|
_ = strings.ToLower("Hello")
|
||||||
if strings.Index(request.URL.Path, "/") < 0 {
|
if strings.Index(request.URL.Path, "/") < 0 {
|
||||||
http.Error(response, "No slashes wat - "+request.URL.Path, http.StatusInternalServerError)
|
http.Error(response, "No slashes wat - "+request.URL.Path, http.StatusInternalServerError)
|
||||||
|
@ -151,6 +147,73 @@ func humanTimeFromTimeString(s string) string {
|
||||||
// return humanize.Time(d)
|
// return humanize.Time(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AugmentedSingsVideoStruct struct {
|
||||||
|
Date time.Time
|
||||||
|
NiceDate string
|
||||||
|
FullTitle string
|
||||||
|
Duet bool
|
||||||
|
OtherSinger string
|
||||||
|
SongTitle string
|
||||||
|
LastSungSong time.Time
|
||||||
|
NiceLastSungSong string
|
||||||
|
LastSungSinger time.Time
|
||||||
|
NiceLastSungSinger string
|
||||||
|
}
|
||||||
|
|
||||||
|
func AugmentSingsVideoStructForCSV(input irc.SingsVideoStruct) AugmentedSingsVideoStruct {
|
||||||
|
var ret AugmentedSingsVideoStruct
|
||||||
|
ret.Date = input.Date
|
||||||
|
ret.NiceDate = input.Date.Format("2006-01-02 15:04:05")
|
||||||
|
ret.FullTitle = input.FullTitle
|
||||||
|
ret.Duet = input.Duet
|
||||||
|
ret.OtherSinger = input.OtherSinger
|
||||||
|
ret.SongTitle = input.SongTitle
|
||||||
|
ret.LastSungSong = input.LastSungSong
|
||||||
|
ret.NiceLastSungSong = input.LastSungSong.Format("2006-01-02 15:04:05")
|
||||||
|
ret.LastSungSinger = input.LastSungSinger
|
||||||
|
if !ret.Duet {
|
||||||
|
ret.NiceLastSungSinger = "Solo performance"
|
||||||
|
} else {
|
||||||
|
ret.NiceLastSungSinger = input.LastSungSinger.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func AugmentSingsVideoStructSliceForCSV(input []irc.SingsVideoStruct) []AugmentedSingsVideoStruct {
|
||||||
|
ret := make([]AugmentedSingsVideoStruct, 0)
|
||||||
|
for _, record := range input {
|
||||||
|
ret = append(ret, AugmentSingsVideoStructForCSV(record))
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func AugmentSingsVideoStruct(input irc.SingsVideoStruct) AugmentedSingsVideoStruct {
|
||||||
|
var ret AugmentedSingsVideoStruct
|
||||||
|
ret.Date = input.Date
|
||||||
|
ret.NiceDate = humanize.Time(input.Date)
|
||||||
|
ret.FullTitle = input.FullTitle
|
||||||
|
ret.Duet = input.Duet
|
||||||
|
ret.OtherSinger = input.OtherSinger
|
||||||
|
ret.SongTitle = input.SongTitle
|
||||||
|
ret.LastSungSong = input.LastSungSong
|
||||||
|
ret.NiceLastSungSong = humanize.Time(input.LastSungSong)
|
||||||
|
ret.LastSungSinger = input.LastSungSinger
|
||||||
|
if !ret.Duet {
|
||||||
|
ret.NiceLastSungSinger = "Solo performance"
|
||||||
|
} else {
|
||||||
|
ret.NiceLastSungSinger = humanize.Time(input.LastSungSinger)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func AugmentSingsVideoStructSlice(input []irc.SingsVideoStruct) []AugmentedSingsVideoStruct {
|
||||||
|
ret := make([]AugmentedSingsVideoStruct, 0)
|
||||||
|
for _, record := range input {
|
||||||
|
ret = append(ret, AugmentSingsVideoStruct(record))
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
func AdminHandler(response http.ResponseWriter, request *http.Request) {
|
func AdminHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
vars := mux.Vars(request)
|
vars := mux.Vars(request)
|
||||||
if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey {
|
if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey {
|
||||||
|
@ -165,12 +228,30 @@ func AdminHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
SinceTimeUTC string
|
SinceTimeUTC string
|
||||||
Leaving bool
|
Leaving bool
|
||||||
HasLeft bool
|
HasLeft bool
|
||||||
SongData []irc.SingsVideoStruct
|
SongData []AugmentedSingsVideoStruct
|
||||||
TopNSongs []SongSings
|
TopNSongs []SongSings
|
||||||
|
TopNSingers []SingerSings
|
||||||
|
ChannelKey string
|
||||||
}
|
}
|
||||||
channelData := ircBot.ChannelData[vars["channel"]]
|
channelData := ircBot.ChannelData[vars["channel"]]
|
||||||
|
if time.Now().Sub(ircBot.ChannelData[vars["channel"]].VideoCacheUpdated).Hours() > 1 {
|
||||||
|
fmt.Printf("Cache of %d performances is older than an hour - %.1f hours old to be precise... fetching.\n", len(ircBot.ChannelData[vars["channel"]].VideoCache), time.Now().Sub(ircBot.ChannelData[vars["channel"]].VideoCacheUpdated).Hours())
|
||||||
|
vids, err := fetchAllVoDs(channelData.TwitchUserID, channelData.Bearer)
|
||||||
|
if err != nil {
|
||||||
|
errCache := make([]irc.SingsVideoStruct, 0)
|
||||||
|
var ret irc.SingsVideoStruct
|
||||||
|
ret.FullTitle = "Error fetching videos: " + err.Error()
|
||||||
|
errCache = append(errCache, ret)
|
||||||
|
vids = errCache
|
||||||
|
}
|
||||||
|
updateCalculatedFields(vids)
|
||||||
|
ircBot.UpdateVideoCache(vars["channel"], vids)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Cache of %d performances is younger than an hour - %.1f hours old to be precise... not fetching.\n", len(ircBot.ChannelData[vars["channel"]].VideoCache), time.Now().Sub(ircBot.ChannelData[vars["channel"]].VideoCacheUpdated).Hours())
|
||||||
|
}
|
||||||
topNSongs := calculateTopNSongs(channelData.VideoCache, 10)
|
topNSongs := calculateTopNSongs(channelData.VideoCache, 10)
|
||||||
var td = TemplateData{channelData.Name, channelData.Command, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft, channelData.VideoCache, topNSongs}
|
topNSingers := calculateTopNSingers(channelData.VideoCache, 10)
|
||||||
|
var td = TemplateData{channelData.Name, channelData.Command, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft, AugmentSingsVideoStructSlice(channelData.VideoCache), topNSongs, topNSingers, vars["key"]}
|
||||||
|
|
||||||
if request.Method == "POST" {
|
if request.Method == "POST" {
|
||||||
request.ParseForm()
|
request.ParseForm()
|
||||||
|
@ -196,7 +277,7 @@ func AdminHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
}
|
}
|
||||||
ircBot.Database.Write("channelData", vars["channel"], record)
|
ircBot.Database.Write("channelData", vars["channel"], record)
|
||||||
ircBot.ChannelData[vars["channel"]] = record
|
ircBot.ChannelData[vars["channel"]] = record
|
||||||
td = TemplateData{record.Name, record.Command, record.ExtraStrings, record.JoinTime, record.JoinTime.Format(irc.UTCFormat), false, record.HasLeft, channelData.VideoCache, topNSongs}
|
td = TemplateData{record.Name, record.Command, record.ExtraStrings, record.JoinTime, record.JoinTime.Format(irc.UTCFormat), false, record.HasLeft, AugmentSingsVideoStructSlice(channelData.VideoCache), topNSongs, topNSingers, vars["key"]}
|
||||||
ircBot.JoinChannel(record.Name)
|
ircBot.JoinChannel(record.Name)
|
||||||
}
|
}
|
||||||
sourceData := ircBot.ChannelData[vars["channel"]]
|
sourceData := ircBot.ChannelData[vars["channel"]]
|
||||||
|
@ -212,9 +293,7 @@ func AdminHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
}
|
}
|
||||||
ircBot.Database.Write("channelData", vars["channel"], sourceData)
|
ircBot.Database.Write("channelData", vars["channel"], sourceData)
|
||||||
}
|
}
|
||||||
tmpl, err := template.New("admin.html").Funcs(template.FuncMap{
|
tmpl, err := template.New("admin.html").ParseFiles("web/admin.html")
|
||||||
"niceDate": humanize.Time,
|
|
||||||
}).ParseFiles("web/admin.html")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err.Error())
|
panic(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -303,8 +382,8 @@ type SongSings struct {
|
||||||
Sings int
|
Sings int
|
||||||
}
|
}
|
||||||
type SingerSings struct {
|
type SingerSings struct {
|
||||||
SongTitle string
|
SingerName string
|
||||||
Sings int
|
Sings int
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateTopNSongs(songCache []irc.SingsVideoStruct, howMany int) []SongSings {
|
func calculateTopNSongs(songCache []irc.SingsVideoStruct, howMany int) []SongSings {
|
||||||
|
@ -329,6 +408,30 @@ func calculateTopNSongs(songCache []irc.SingsVideoStruct, howMany int) []SongSin
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func calculateTopNSingers(songCache []irc.SingsVideoStruct, howMany int) []SingerSings {
|
||||||
|
songMap := map[string]int{}
|
||||||
|
for _, record := range songCache {
|
||||||
|
if record.Duet {
|
||||||
|
sings := songMap[record.OtherSinger]
|
||||||
|
sings += 1
|
||||||
|
songMap[record.OtherSinger] = sings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slice := make([]SingerSings, 0)
|
||||||
|
for key, value := range songMap {
|
||||||
|
ss := SingerSings{key, value}
|
||||||
|
slice = append(slice, ss)
|
||||||
|
}
|
||||||
|
sort.SliceStable(slice, func(i, j int) bool {
|
||||||
|
return slice[i].Sings > slice[j].Sings
|
||||||
|
})
|
||||||
|
var ret []SingerSings
|
||||||
|
for i := 1; i <= howMany; i++ {
|
||||||
|
ret = append(ret, slice[i])
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
func calculateLastSungSongDate(songCache []irc.SingsVideoStruct, SongTitle string) time.Time {
|
func calculateLastSungSongDate(songCache []irc.SingsVideoStruct, SongTitle string) time.Time {
|
||||||
var t time.Time
|
var t time.Time
|
||||||
for _, record := range songCache {
|
for _, record := range songCache {
|
||||||
|
@ -480,21 +583,7 @@ func TwitchAdminHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
user := usersObject.Data[0]
|
user := usersObject.Data[0]
|
||||||
magicCode := ircBot.ReadOrCreateChannelKey(user.Login)
|
magicCode := ircBot.ReadOrCreateChannelKey(user.Login)
|
||||||
ircBot.UpdateBearerToken(user.Login, oauthResponse.Access_token)
|
ircBot.UpdateBearerToken(user.Login, oauthResponse.Access_token)
|
||||||
if time.Now().Sub(ircBot.ChannelData[user.Login].VideoCacheUpdated).Hours() > 1 {
|
ircBot.UpdateTwitchUserID(user.Login, user.Id)
|
||||||
fmt.Printf("Cache of %d performances is older than an hour - %.1f hours old to be precise... fetching.\n", len(ircBot.ChannelData[user.Login].VideoCache), time.Now().Sub(ircBot.ChannelData[user.Login].VideoCacheUpdated).Hours())
|
|
||||||
vids, err := fetchAllVoDs(user.Id, oauthResponse.Access_token)
|
|
||||||
if err != nil {
|
|
||||||
errCache := make([]irc.SingsVideoStruct, 0)
|
|
||||||
var ret irc.SingsVideoStruct
|
|
||||||
ret.FullTitle = "Error fetching videos: " + err.Error()
|
|
||||||
errCache = append(errCache, ret)
|
|
||||||
vids = errCache
|
|
||||||
}
|
|
||||||
updateCalculatedFields(vids)
|
|
||||||
ircBot.UpdateVideoCache(user.Login, vids)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Cache of %d performances is younger than an hour - %.1f hours old to be precise... not fetching.\n", len(ircBot.ChannelData[user.Login].VideoCache), time.Now().Sub(ircBot.ChannelData[user.Login].VideoCacheUpdated).Hours())
|
|
||||||
}
|
|
||||||
url := "https://" + ircBot.Config.ExternalUrl + "/admin/" + user.Login + "/" + magicCode
|
url := "https://" + ircBot.Config.ExternalUrl + "/admin/" + user.Login + "/" + magicCode
|
||||||
http.Redirect(response, request, url, http.StatusFound)
|
http.Redirect(response, request, url, http.StatusFound)
|
||||||
|
|
||||||
|
@ -554,6 +643,49 @@ func TwitchBackendHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CSVHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
|
vars := mux.Vars(request)
|
||||||
|
if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey {
|
||||||
|
UnauthorizedHandler(response, request)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type TemplateData struct {
|
||||||
|
Channel string
|
||||||
|
Command string
|
||||||
|
ExtraStrings string
|
||||||
|
SinceTime time.Time
|
||||||
|
SinceTimeUTC string
|
||||||
|
Leaving bool
|
||||||
|
HasLeft bool
|
||||||
|
SongData []AugmentedSingsVideoStruct
|
||||||
|
TopNSongs []SongSings
|
||||||
|
TopNSingers []SingerSings
|
||||||
|
}
|
||||||
|
channelData := ircBot.ChannelData[vars["channel"]]
|
||||||
|
if time.Now().Sub(ircBot.ChannelData[vars["channel"]].VideoCacheUpdated).Hours() > 1 {
|
||||||
|
fmt.Printf("Cache of %d performances is older than an hour - %.1f hours old to be precise... fetching.\n", len(ircBot.ChannelData[vars["channel"]].VideoCache), time.Now().Sub(ircBot.ChannelData[vars["channel"]].VideoCacheUpdated).Hours())
|
||||||
|
vids, err := fetchAllVoDs(channelData.TwitchUserID, channelData.Bearer)
|
||||||
|
if err != nil {
|
||||||
|
errCache := make([]irc.SingsVideoStruct, 0)
|
||||||
|
var ret irc.SingsVideoStruct
|
||||||
|
ret.FullTitle = "Error fetching videos: " + err.Error()
|
||||||
|
errCache = append(errCache, ret)
|
||||||
|
vids = errCache
|
||||||
|
}
|
||||||
|
updateCalculatedFields(vids)
|
||||||
|
ircBot.UpdateVideoCache(vars["channel"], vids)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Cache of %d performances is younger than an hour - %.1f hours old to be precise... not fetching.\n", len(ircBot.ChannelData[vars["channel"]].VideoCache), time.Now().Sub(ircBot.ChannelData[vars["channel"]].VideoCacheUpdated).Hours())
|
||||||
|
}
|
||||||
|
topNSongs := calculateTopNSongs(channelData.VideoCache, 10)
|
||||||
|
topNSingers := calculateTopNSingers(channelData.VideoCache, 10)
|
||||||
|
var td = TemplateData{channelData.Name, channelData.Command, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft, AugmentSingsVideoStructSliceForCSV(channelData.VideoCache), topNSongs, topNSingers}
|
||||||
|
response.Header().Add("Content-Disposition", "attachment; filename=\"duets.csv\"")
|
||||||
|
response.Header().Add("Content-type", "text/csv")
|
||||||
|
tmpl := template.Must(template.ParseFiles("web/data.csv"))
|
||||||
|
tmpl.Execute(response, td)
|
||||||
|
}
|
||||||
|
|
||||||
func HandleHTTP(passedIrcBot *irc.KardBot) {
|
func HandleHTTP(passedIrcBot *irc.KardBot) {
|
||||||
ircBot = passedIrcBot
|
ircBot = passedIrcBot
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
|
@ -565,6 +697,7 @@ func HandleHTTP(passedIrcBot *irc.KardBot) {
|
||||||
r.PathPrefix("/static/").Handler(http.FileServer(http.Dir("./web/")))
|
r.PathPrefix("/static/").Handler(http.FileServer(http.Dir("./web/")))
|
||||||
r.HandleFunc("/cover.css", CSSHandler)
|
r.HandleFunc("/cover.css", CSSHandler)
|
||||||
r.HandleFunc("/admin/{channel}/{key}", AdminHandler)
|
r.HandleFunc("/admin/{channel}/{key}", AdminHandler)
|
||||||
|
r.HandleFunc("/csv/{channel}/{key}", CSVHandler)
|
||||||
//r.HandleFunc("/twitchadmin", TwitchAdminHandler)
|
//r.HandleFunc("/twitchadmin", TwitchAdminHandler)
|
||||||
//r.HandleFunc("/twitchtobackend", TwitchBackendHandler)
|
//r.HandleFunc("/twitchtobackend", TwitchBackendHandler)
|
||||||
r.Path("/twitchtobackend").Queries("access_token", "{access_token}", "scope", "{scope}", "token_type", "{token_type}").HandlerFunc(TwitchBackendHandler)
|
r.Path("/twitchtobackend").Queries("access_token", "{access_token}", "scope", "{scope}", "token_type", "{token_type}").HandlerFunc(TwitchBackendHandler)
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<meta name="description" content="Karaokards bot for twitch chat">
|
<meta name="description" content="Twitch Sings tools for data analysis">
|
||||||
<meta name="author" content="Martyn Ranyard">
|
<meta name="author" content="Martyn Ranyard">
|
||||||
<title>Karaokards</title>
|
<title>Twitch Sings Tools</title>
|
||||||
|
|
||||||
<!-- Bootstrap core CSS -->
|
<!-- Bootstrap core CSS -->
|
||||||
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
#insightspanel > table:nth-child(1) > thead:nth-child(1) > tr:nth-child(1) > td:nth-child(1)
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.bd-placeholder-img-lg {
|
.bd-placeholder-img-lg {
|
||||||
font-size: 3.5rem;
|
font-size: 3.5rem;
|
||||||
|
@ -76,7 +76,7 @@
|
||||||
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
|
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
|
||||||
<header class="masthead mb-auto">
|
<header class="masthead mb-auto">
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
<h3 class="masthead-brand">Karaokards</h3>
|
<h3 class="masthead-brand">Twitch Sings Tools</h3>
|
||||||
<nav class="nav nav-masthead justify-content-center">
|
<nav class="nav nav-masthead justify-content-center">
|
||||||
<a class="nav-link active" href="/">Home</a>
|
<a class="nav-link active" href="/">Home</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -108,8 +108,8 @@
|
||||||
document.getElementById("yuhateme").classList.add("visibleSave")
|
document.getElementById("yuhateme").classList.add("visibleSave")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<h1 class="cover-heading">Karaokards admin panel for {{.Channel}}!!!</h1>
|
<h1 class="cover-heading">Twitch Sings Tools admin panel for {{.Channel}}!!!</h1>
|
||||||
<ul class="nav nav-tabs" style="width: 80%;">
|
<ul class="nav nav-tabs" style="width: 100%;">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a id="insights" class="nav-link active panel" href="#" onclick="tabClick(this)">Insights</a>
|
<a id="insights" class="nav-link active panel" href="#" onclick="tabClick(this)">Insights</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -123,27 +123,35 @@
|
||||||
<a id="bot" class="nav-link panel" href="#" onclick="tabClick(this)">Bot</a>
|
<a id="bot" class="nav-link panel" href="#" onclick="tabClick(this)">Bot</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div style="width: 80%;overflow-y: scroll; display: block;" id="insightspanel" class="controlpanel">
|
<div style="width: 100%; overflow-y: scroll; display: block;" id="insightspanel" class="controlpanel">
|
||||||
Top 10 Songs :
|
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><td>What</td><td>Sings</td>
|
<tr><th><h2>Top 10 Songs : </h2></th><th><h2>Top 10 Singers : </h2></th></tr>
|
||||||
{{ range .TopNSongs }}
|
<tr><td><table>
|
||||||
<tr><td>{{ .SongTitle }}</td><td>{{ .Sings }}</td></tr>
|
<thead><tr><th>Song</th><th>Sings</th></tr></thead>
|
||||||
{{ end }}
|
{{ range .TopNSongs }}
|
||||||
|
<tr><td>{{ .SongTitle }}</td><td>{{ .Sings }}</td></tr>
|
||||||
|
{{ end }}
|
||||||
|
</table></td><td><table>
|
||||||
|
<thead><tr><th>Singer</th><th>Sings</th></tr></thead>
|
||||||
|
{{ range .TopNSingers }}
|
||||||
|
<tr><td>{{ .SingerName }}</td><td>{{ .Sings }}</td></tr>
|
||||||
|
{{ end }}
|
||||||
|
</table></td></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 80%;overflow-y: scroll; display: none;" id="datapanel" class="controlpanel">
|
<div style="width: 100%; overflow-y: scroll; display: none;" id="datapanel" class="controlpanel">
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><td>Published</td><td>Who</td><td>What</td><td>Last sang this song</td><td>Last dueted with performer</td></tr></thead>
|
<thead><tr><th>Published</th><th>Who</th><th>What</th><th>Last sang this song</th><th>Last dueted with performer</th></th></thead>
|
||||||
{{ range .SongData }}
|
{{ range .SongData }}
|
||||||
<tr><td title="{{ .Date }}">{{ .Date | niceDate }}</td><td>{{ .SongTitle }}</td><td>{{ .OtherSinger }}</td><td title="{{ .LastSungSong }}">{{ .LastSungSong | niceDate }}</td><td title="{{ .LastSungSinger }}">{{ .LastSungSinger | niceDate }}</td></tr>
|
<tr><td title="{{ .Date }}">{{ .NiceDate }}</td><td>{{ .SongTitle }}</td><td>{{ .OtherSinger }}</td><td title="{{ .LastSungSong }}">{{ .NiceLastSungSong }}</td><td title="{{ .LastSungSinger }}">{{ .NiceLastSungSinger }}</td></tr>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div style="overflow-y: scroll; display: none;" id="csvpanel" class="controlpanel">
|
<div style="width: 100%; overflow-y: scroll; display: none;" id="csvpanel" class="controlpanel">
|
||||||
<h2>CSV support coming!</h2>
|
<h2>Click <a href="/csv/{{.Channel}}/{{.ChannelKey}}">here</a> to download the data as a CSV</h2>
|
||||||
</div>
|
</div>
|
||||||
<div style="overflow-y: scroll; display: none;" id="botpanel" class="controlpanel">
|
<div style="width: 100%; overflow-y: scroll; display: none;" id="botpanel" class="controlpanel">
|
||||||
|
<h2>The bot isn't really ready yet... it just has the old Karaokards facility at the moment. I woudn't bother inviting it yet.</h2>
|
||||||
{{ if .HasLeft }}
|
{{ if .HasLeft }}
|
||||||
<h2>Not in your channel at the moment!</h2>
|
<h2>Not in your channel at the moment!</h2>
|
||||||
<p>The bot is not currently in your channel, chances are you've not ever asked it to join, you asked it to leave, or something went horribly wrong.</p>
|
<p>The bot is not currently in your channel, chances are you've not ever asked it to join, you asked it to leave, or something went horribly wrong.</p>
|
||||||
|
@ -156,7 +164,7 @@
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<h2>Note you can give your moderators the url you are on right now to control this bot. They don't have to be logged into twitch to do so.</h2>
|
<h2>Note you can give your moderators the url you are on right now to control this bot. They don't have to be logged into twitch to do so.</h2>
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><td>Channel Data :</td><td><input type="button" value="Edit" onclick="javascript:editMode();"></td></tr></thead>
|
<thead><tr><th>Channel Data :</th><th><input type="button" value="Edit" onclick="javascript:editMode();"></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>Member of channel since {{.SinceTimeUTC}}</td></tr>
|
<tr><td>Member of channel since {{.SinceTimeUTC}}</td></tr>
|
||||||
<tr><td>Command for prompt:</td><td class="displaySetting"></tdclass>{{.Command}}</td><td class="editSetting"><input type="text" name="Command" value="{{.Command}}"></td></tr>
|
<tr><td>Command for prompt:</td><td class="displaySetting"></tdclass>{{.Command}}</td><td class="editSetting"><input type="text" name="Command" value="{{.Command}}"></td></tr>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
Published,Who,What,Last sang this song,Last dueted with performer
|
Published,Who,What,Last sang this song,Last dueted with performer
|
||||||
{{ range .SongData -}}
|
{{ range .SongData -}}
|
||||||
{{- .Date }},{{ .SongTitle }},{{ .OtherSinger }},{{ .LastSungSong }},{{ .LastSungSinger }}
|
{{- .NiceDate }},{{ .SongTitle }},{{ .OtherSinger }},{{ .NiceLastSungSong }},{{ .NiceLastSungSinger }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
|
@ -63,9 +63,33 @@
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main role="main" class="inner cover">
|
<main role="main" class="inner cover">
|
||||||
<h1 class="cover-heading">Karaokards!!!</h1>
|
<h1 class="cover-heading">Twitch Sings Tools?</h1>
|
||||||
|
<h2>Data about Twitch Sings <b>published</b> performances</h2>
|
||||||
|
<p>This set of tools uses the standard twitch APIs to create a list of songs you have sung and singers you have sung with. <i>Note</i>: If twitch sings ever changes how they name published performances, this may get harder to do.</p>
|
||||||
|
<h2>Some insights</h2>
|
||||||
|
<p>There's a "top 10 people you sing with" and a "top 10 songs you sing". There's actually not that much insight that can be drawn other than those without getting people involved :-)</p>
|
||||||
|
<h2>CSV Export!</h2>
|
||||||
|
<p>You can bring the data into Excel, Google Sheets, Libre/OpenOffice, Lotus 1-2-3 or whatever, and analyse/graph to your hearts content!</p>
|
||||||
|
<h2>Chatbot</h2>
|
||||||
|
<p>There's a chat bot and lots of features are planned :</p>
|
||||||
|
<ul>
|
||||||
|
<li>[NOT YET IMPLEMENTED] Suggest a singer to sing with I haven't sung with in a while</li>
|
||||||
|
<li>[NOT YET IMPLEMENTED] Suggest a song to sing that I haven't sung in a while</li>
|
||||||
|
<li>[NOT YET IMPLEMENTED] List recent duets with <i>singer</i> so I don't have to look it up via the twitch web interface</li>
|
||||||
|
<li>[NOT YET IMPLEMENTED] Check the <a href="https://songlist.sings.twitch.tv">songlist</a> for a song</li>
|
||||||
|
<li>Give a random prompt to give you an idea of a song to sing</li>
|
||||||
|
</ul>
|
||||||
<p>Random prompt for you : {{.Prompt}}</p>
|
<p>Random prompt for you : {{.Prompt}}</p>
|
||||||
<p>There are a total of {{.AvailCount}} prompts available. This bot is hanging out in {{.ChannelCount}} channels and has served {{.MessageCount}} prompts via twitch chat!</p>
|
<p>There are a total of {{.AvailCount}} prompts available. This bot is hanging out in {{.ChannelCount}} channels and has served {{.MessageCount}} prompts via twitch chat!</p>
|
||||||
|
<h2>FAQ</h2>
|
||||||
|
<ul><li>What about Open Duets</li>
|
||||||
|
<li>Unfortunately Twitch Sings publishes Open Duets in a way that they don't appear in the APIs. Whilst I could reverse-engineer the calls TS itself uses, I'm already skirting danger by calling this page "Twitch Sings Tools" (hint, they closed one account because of that!)</li></ul>
|
||||||
|
<ul><li>Can you Open-source this?</li>
|
||||||
|
<li>Can <a href="https://git.martyn.berlin/martyn/twitchsingstools">and have</a>! It's a bit of a mess architecturally because I started with a twitch chat bot and grew it out, badly. One day I <i>might</i> refactor the code.</li></ul>
|
||||||
|
<ul><li>What about unpublished duets?</li>
|
||||||
|
<li>Unfortunately, there's no way to get that data. I live by the rule of "publish everything", but if you're a perfectionist (I am with seeds), I'm sorry, that data isn't accessible.</li></ul>
|
||||||
|
<ul><li>Will you implement X</li>
|
||||||
|
<li>If it's not too hard, and the data is availabe via published APIs I'll consider it. Remember that the data is downloadable as a CSV so you can probably do a lot with that.</li></ul>
|
||||||
</main>
|
</main>
|
||||||
<footer class="mastfoot mt-auto">
|
<footer class="mastfoot mt-auto">
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
|
|
Loading…
Reference in New Issue