diff --git a/deployments/helm/twitchsingstools/secrets.yaml b/deployments/helm/twitchsingstools/secrets.yaml
index a43201a..9077e18 100644
--- a/deployments/helm/twitchsingstools/secrets.yaml
+++ b/deployments/helm/twitchsingstools/secrets.yaml
@@ -1,14 +1,14 @@
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:
- client_id: ENC[AES256_GCM,data:Iy1Wb5WevXQtDpDPTKKkpYez+WWOGOUDre89qf1Y,iv:z/ZbcgADO5vDQOjX/A6xU99lGiD2k3qxor31KnVFE4o=,tag:pgf67HdxOjUSWEktgPfolw==,type:str]
- client_secret: ENC[AES256_GCM,data:5fY77ccyfqZMh30hMdhXy1h7fio4763NLgYu9tgP,iv:kLyEULWcgKHwpPxoBX69IcHAaeAob9AN8gD5/K5nIyg=,tag:nxtZMuCZdGyVhBBvgRz2SA==,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:E5zgitQ0cq+MDQsc5gjAyShwgoRS4TwewzLRus7M,iv:9TKxA2sASAvwbUOVVB3UPgowyBjUmLJA/tJCEpt2XGc=,tag:CIZw2pf3mBxASQLO48zJtQ==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
- lastmodified: '2020-07-14T18:03:02Z'
- mac: ENC[AES256_GCM,data:quUrWcjviC08daW0k+Y0a6oB+ZXT8NkF2z1cni2DeFUuSXjrxDQcMY3POj45DbzHrojXMkJKOk6JLOcj9oV1h8uCqyZ/rGWhgfly1YNLJEP8LcJbCQt2jUAhctxaQkvPtqIak7e/ABKHhMuKmBbNWzhTLBbyl2/YR2+pPsLxKQA=,iv:PbRPKBaLtns4+M0ydrVTecVHgtR3q7TkhKWN01cEhVE=,tag:nCK7gSQ+F97lJJhCOol7zw==,type:str]
+ lastmodified: '2020-07-15T20:38:41Z'
+ mac: ENC[AES256_GCM,data:8zIsya5YjfW/R1xD8Q2B1yIoXEOXJsDCqAeicFyNL6zkxPSCZSv5hJfBDH0kmNuAfR9oG7+XCqQbhNM8bVrNWbhYILc8aJuexth67riuVibWvg0r2xdMnozJYvJ6u1rRJ9xwRtYvBzjqK6SMuq5mkD2uq7wcmJr0rNoWjnXhuVc=,iv:wMQ8HgfskVGlt+x5t+ah+Yo1b0gbV/6jE1APBrxhdS8=,tag:JZXTby+v8meFEwbSBOyWlg==,type:str]
pgp:
- created_at: '2020-07-14T18:02:04Z'
enc: |
diff --git a/deployments/helm/twitchsingstools/values.yaml b/deployments/helm/twitchsingstools/values.yaml
index e382454..e294ee8 100644
--- a/deployments/helm/twitchsingstools/values.yaml
+++ b/deployments/helm/twitchsingstools/values.yaml
@@ -35,7 +35,7 @@ securityContext: {}
secretFiles: {}
irc:
- nick: twitchsingstools
+ nick: tstools
twitchapp: {}
@@ -45,7 +45,7 @@ service:
type: ClusterIP
port: 80
-externalHostname: twitchsingstools.ing.martyn.berlin
+externalHostname: twitchsingstools.martyn.berlin
ingress:
enabled: true
@@ -56,10 +56,16 @@ ingress:
- host: twitchsingstools.ing.martyn.berlin
paths:
- /
+ - host: twitchsingstools.martyn.berlin
+ paths:
+ - /
tls:
- secretName: tstools-tls
hosts:
- twitchsingstools.ing.martyn.berlin
+ - secretName: tstools-secondary-tls
+ hosts:
+ - twitchsingstools.martyn.berlin
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
diff --git a/internal/irc/irc.go b/internal/irc/irc.go
index 06487ed..3feaa49 100644
--- a/internal/irc/irc.go
+++ b/internal/irc/irc.go
@@ -106,6 +106,7 @@ type ChannelData struct {
VideoCache []SingsVideoStruct `json:"videoCache"`
VideoCacheUpdated time.Time `json:"videoCacheUpdated"`
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
@@ -159,6 +160,13 @@ func (bb *KardBot) UpdateBearerToken(user string, token string) {
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
// continues until it gets disconnected, told to shutdown, or forcefully shutdown.
func (bb *KardBot) HandleChat() error {
diff --git a/internal/webserver/webserver.go b/internal/webserver/webserver.go
index bb214b9..45900dc 100755
--- a/internal/webserver/webserver.go
+++ b/internal/webserver/webserver.go
@@ -108,10 +108,6 @@ func TemplateHandler(response http.ResponseWriter, request *http.Request) {
ClientID 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")
if strings.Index(request.URL.Path, "/") < 0 {
http.Error(response, "No slashes wat - "+request.URL.Path, http.StatusInternalServerError)
@@ -151,6 +147,73 @@ func humanTimeFromTimeString(s string) string {
// 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) {
vars := mux.Vars(request)
if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey {
@@ -165,12 +228,30 @@ func AdminHandler(response http.ResponseWriter, request *http.Request) {
SinceTimeUTC string
Leaving bool
HasLeft bool
- SongData []irc.SingsVideoStruct
+ SongData []AugmentedSingsVideoStruct
TopNSongs []SongSings
+ TopNSingers []SingerSings
+ ChannelKey string
}
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)
- 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" {
request.ParseForm()
@@ -196,7 +277,7 @@ func AdminHandler(response http.ResponseWriter, request *http.Request) {
}
ircBot.Database.Write("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)
}
sourceData := ircBot.ChannelData[vars["channel"]]
@@ -212,9 +293,7 @@ func AdminHandler(response http.ResponseWriter, request *http.Request) {
}
ircBot.Database.Write("channelData", vars["channel"], sourceData)
}
- tmpl, err := template.New("admin.html").Funcs(template.FuncMap{
- "niceDate": humanize.Time,
- }).ParseFiles("web/admin.html")
+ tmpl, err := template.New("admin.html").ParseFiles("web/admin.html")
if err != nil {
panic(err.Error())
}
@@ -303,8 +382,8 @@ type SongSings struct {
Sings int
}
type SingerSings struct {
- SongTitle string
- Sings int
+ SingerName string
+ Sings int
}
func calculateTopNSongs(songCache []irc.SingsVideoStruct, howMany int) []SongSings {
@@ -329,6 +408,30 @@ func calculateTopNSongs(songCache []irc.SingsVideoStruct, howMany int) []SongSin
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 {
var t time.Time
for _, record := range songCache {
@@ -480,21 +583,7 @@ func TwitchAdminHandler(response http.ResponseWriter, request *http.Request) {
user := usersObject.Data[0]
magicCode := ircBot.ReadOrCreateChannelKey(user.Login)
ircBot.UpdateBearerToken(user.Login, oauthResponse.Access_token)
- if time.Now().Sub(ircBot.ChannelData[user.Login].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[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())
- }
+ ircBot.UpdateTwitchUserID(user.Login, user.Id)
url := "https://" + ircBot.Config.ExternalUrl + "/admin/" + user.Login + "/" + magicCode
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) {
ircBot = passedIrcBot
r := mux.NewRouter()
@@ -565,6 +697,7 @@ func HandleHTTP(passedIrcBot *irc.KardBot) {
r.PathPrefix("/static/").Handler(http.FileServer(http.Dir("./web/")))
r.HandleFunc("/cover.css", CSSHandler)
r.HandleFunc("/admin/{channel}/{key}", AdminHandler)
+ r.HandleFunc("/csv/{channel}/{key}", CSVHandler)
//r.HandleFunc("/twitchadmin", TwitchAdminHandler)
//r.HandleFunc("/twitchtobackend", TwitchBackendHandler)
r.Path("/twitchtobackend").Queries("access_token", "{access_token}", "scope", "{scope}", "token_type", "{token_type}").HandlerFunc(TwitchBackendHandler)
diff --git a/web/admin.html b/web/admin.html
index 9ca1882..6d5a35d 100755
--- a/web/admin.html
+++ b/web/admin.html
@@ -4,9 +4,9 @@
-
+
- Karaokards
+ Twitch Sings Tools
@@ -22,7 +22,7 @@
-ms-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) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
@@ -76,7 +76,7 @@