Compare commits
No commits in common. "d13c00eff03224271410daec263261af7e6e3b8d" and "b891b74d4b719c6a3370a4b074f8f57ea41c87b4" have entirely different histories.
d13c00eff0
...
b891b74d4b
3
Makefile
3
Makefile
|
@ -4,9 +4,6 @@ LDFLAGS=-ldflags "-X main.buildDate=${BUILD}"
|
||||||
|
|
||||||
.PHONY: build deps static
|
.PHONY: build deps static
|
||||||
|
|
||||||
test:
|
|
||||||
go test git.martyn.berlin/martyn/twitchsingstools/internal/webserver
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build ${LDFLAGS}
|
go build ${LDFLAGS}
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,8 @@ steps:
|
||||||
- mkdir -p /go/src/git.martyn.berlin/martyn
|
- mkdir -p /go/src/git.martyn.berlin/martyn
|
||||||
- ln -s /drone/src /go/src/git.martyn.berlin/martyn/twitchsingstools
|
- ln -s /drone/src /go/src/git.martyn.berlin/martyn/twitchsingstools
|
||||||
- cd /go/src/git.martyn.berlin/martyn/twitchsingstools
|
- cd /go/src/git.martyn.berlin/martyn/twitchsingstools
|
||||||
- make test
|
- go get
|
||||||
- make
|
- go build
|
||||||
|
|
||||||
- name: publish
|
- name: publish
|
||||||
image: plugins/docker:18
|
image: plugins/docker:18
|
||||||
|
@ -54,8 +54,8 @@ steps:
|
||||||
- mkdir -p /go/src/git.martyn.berlin/martyn
|
- mkdir -p /go/src/git.martyn.berlin/martyn
|
||||||
- ln -s /drone/src /go/src/git.martyn.berlin/martyn/twitchsingstools
|
- ln -s /drone/src /go/src/git.martyn.berlin/martyn/twitchsingstools
|
||||||
- cd /go/src/git.martyn.berlin/martyn/twitchsingstools
|
- cd /go/src/git.martyn.berlin/martyn/twitchsingstools
|
||||||
- make test
|
- go get
|
||||||
- make
|
- go build
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
ref:
|
ref:
|
||||||
|
|
|
@ -29,9 +29,6 @@ spec:
|
||||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||||
containers:
|
containers:
|
||||||
- name: {{ .Chart.Name }}
|
- name: {{ .Chart.Name }}
|
||||||
env:
|
|
||||||
- name: TSTOOLS_DATA_FOLDER
|
|
||||||
value: /data
|
|
||||||
securityContext:
|
securityContext:
|
||||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default "latest" }}"
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default "latest" }}"
|
||||||
|
|
|
@ -51,7 +51,6 @@ ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
annotations:
|
annotations:
|
||||||
kubernetes.io/ingress.class: nginx
|
kubernetes.io/ingress.class: nginx
|
||||||
cert-manager.io/cluster-issuer: letsencrypt
|
|
||||||
hosts:
|
hosts:
|
||||||
- host: twitchsingstools.ing.martyn.berlin
|
- host: twitchsingstools.ing.martyn.berlin
|
||||||
paths:
|
paths:
|
||||||
|
|
|
@ -85,16 +85,6 @@ type KardBot struct {
|
||||||
Config ConfigStruct
|
Config ConfigStruct
|
||||||
}
|
}
|
||||||
|
|
||||||
type SingsVideoStruct struct {
|
|
||||||
Date time.Time `json:"date"` // Golang date of creation
|
|
||||||
FullTitle string `json:"fullTitle"` // Full Title
|
|
||||||
Duet bool `json:"duet"` // Is it a duet?
|
|
||||||
OtherSinger string `json:"otherSinger"` // Twitch NAME of the other singer, extracted from the title
|
|
||||||
SongTitle string `json:"songTitle"` // extracted from title
|
|
||||||
LastSungSong time.Time `json:"LastSungSong"` // Last time this SONG was sung
|
|
||||||
LastSungSinger time.Time `json:"LastSungSinger"` // Last time a duet was sung with this SINGER, regardless of song, only Duets have this date initialised
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChannelData struct {
|
type ChannelData struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
AdminKey string `json:"value,omitempty"`
|
AdminKey string `json:"value,omitempty"`
|
||||||
|
@ -103,9 +93,6 @@ type ChannelData struct {
|
||||||
JoinTime time.Time `json:"jointime"`
|
JoinTime time.Time `json:"jointime"`
|
||||||
ControlChannel bool
|
ControlChannel bool
|
||||||
HasLeft bool `json:"hasleft"`
|
HasLeft bool `json:"hasleft"`
|
||||||
VideoCache []SingsVideoStruct `json:"videoCache"`
|
|
||||||
VideoCacheUpdated time.Time `json:"videoCacheUpdated"`
|
|
||||||
Bearer string `json:"bearer"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -143,22 +130,6 @@ func (bb *KardBot) ActiveChannels() int {
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bb *KardBot) UpdateVideoCache(user string, videos []SingsVideoStruct) {
|
|
||||||
record := bb.ChannelData[user]
|
|
||||||
fmt.Printf("Replacing cache of %d performances with a new cache of %d performances", len(record.VideoCache), len(videos))
|
|
||||||
record.VideoCache = videos
|
|
||||||
record.VideoCacheUpdated = time.Now()
|
|
||||||
bb.Database.Write("channelData", user, record)
|
|
||||||
bb.ChannelData[user] = record
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bb *KardBot) UpdateBearerToken(user string, token string) {
|
|
||||||
record := bb.ChannelData[user]
|
|
||||||
record.Bearer = token
|
|
||||||
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 {
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
package webserver
|
package webserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
irc "git.martyn.berlin/martyn/twitchsingstools/internal/irc"
|
irc "git.martyn.berlin/martyn/twitchsingstools/internal/irc"
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
@ -48,31 +44,6 @@ type twitchUsersBigResponse struct {
|
||||||
Data []twitchUser `json:"data"`
|
Data []twitchUser `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type twitchCursor struct {
|
|
||||||
Cursor string `json:"cursor"`
|
|
||||||
}
|
|
||||||
type videoStruct struct {
|
|
||||||
CreatedAt string `json:"created_at"` // Date when the video was created.
|
|
||||||
Description string `json:"description"` // Description of the video.
|
|
||||||
Duration string `json:"duration"` // Length of the video.
|
|
||||||
ID string `json:"id"` // ID of the video.
|
|
||||||
Language string `json:"language"` // Language of the video.
|
|
||||||
Pagination string `json:"pagination"` // A cursor value, to be used in a subsequent request to specify the starting point of the next set of results.
|
|
||||||
PublishedAt string `json:"published_at"` // Date when the video was published.
|
|
||||||
ThumbnailURL string `json:"thumbnail_url"` // Template URL for the thumbnail of the video.
|
|
||||||
Title string `json:"title"` // Title of the video.
|
|
||||||
Type string `json:"type"` // Type of video. Valid values: "upload", "archive", "highlight".
|
|
||||||
URL string `json:"url"` // URL of the video.
|
|
||||||
UserID string `json:"user_id"` // ID of the user who owns the video.
|
|
||||||
UserName string `json:"user_name"` // Display name corresponding to user_id.
|
|
||||||
ViewCount int `json:"view_count"` // Number of times the video has been viewed.
|
|
||||||
Viewable string `json:"viewable"` // Indicates whether the video is publicly viewable. Valid values: "public", "private".
|
|
||||||
}
|
|
||||||
type videosResponse struct {
|
|
||||||
Data []videoStruct `json:"data"`
|
|
||||||
Pagination twitchCursor `json:"pagination"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var ircBot *irc.KardBot
|
var ircBot *irc.KardBot
|
||||||
|
|
||||||
func HealthHandler(response http.ResponseWriter, request *http.Request) {
|
func HealthHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
|
@ -144,13 +115,6 @@ func LeaveHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
TemplateHandler(response, request)
|
TemplateHandler(response, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func humanTimeFromTimeString(s string) string {
|
|
||||||
return s
|
|
||||||
// format := "2006-01-02 15:04:05 +0000 UTC"
|
|
||||||
// d, _ := time.Parse(format, s)
|
|
||||||
// return humanize.Time(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 +129,9 @@ func AdminHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
SinceTimeUTC string
|
SinceTimeUTC string
|
||||||
Leaving bool
|
Leaving bool
|
||||||
HasLeft bool
|
HasLeft bool
|
||||||
SongData []irc.SingsVideoStruct
|
|
||||||
TopNSongs []SongSings
|
|
||||||
}
|
}
|
||||||
channelData := ircBot.ChannelData[vars["channel"]]
|
channelData := ircBot.ChannelData[vars["channel"]]
|
||||||
topNSongs := calculateTopNSongs(channelData.VideoCache, 10)
|
var td = TemplateData{channelData.Name, channelData.Command, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft}
|
||||||
var td = TemplateData{channelData.Name, channelData.Command, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft, channelData.VideoCache, topNSongs}
|
|
||||||
|
|
||||||
if request.Method == "POST" {
|
if request.Method == "POST" {
|
||||||
request.ParseForm()
|
request.ParseForm()
|
||||||
|
@ -196,7 +157,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}
|
||||||
ircBot.JoinChannel(record.Name)
|
ircBot.JoinChannel(record.Name)
|
||||||
}
|
}
|
||||||
sourceData := ircBot.ChannelData[vars["channel"]]
|
sourceData := ircBot.ChannelData[vars["channel"]]
|
||||||
|
@ -212,12 +173,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 := template.Must(template.ParseFiles("web/admin.html"))
|
||||||
"niceDate": humanize.Time,
|
|
||||||
}).ParseFiles("web/admin.html")
|
|
||||||
if err != nil {
|
|
||||||
panic(err.Error())
|
|
||||||
}
|
|
||||||
tmpl.Execute(response, td)
|
tmpl.Execute(response, td)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,174 +203,6 @@ func twitchHTTPClient(call string, bearer string) (string, error) {
|
||||||
return string([]byte(body)), nil
|
return string([]byte(body)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidateTwitchBearerToken(bearer string) (bool, error) {
|
|
||||||
url := "https://id.twitch.tv/oauth2/validate"
|
|
||||||
var bearerHeader = "Bearer " + bearer
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
|
||||||
req.Header.Add("Client-ID", ircBot.AppCredentials.ClientID)
|
|
||||||
req.Header.Add("Authorization", bearerHeader)
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
_, _ = ioutil.ReadAll(resp.Body)
|
|
||||||
return resp.StatusCode == 200, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func twitchVidToSingsVid(twitchFormat videoStruct) (irc.SingsVideoStruct, error) {
|
|
||||||
var ret irc.SingsVideoStruct
|
|
||||||
layout := "2006-01-02T15:04:05Z"
|
|
||||||
var d time.Time
|
|
||||||
d, err := time.Parse(layout, twitchFormat.CreatedAt)
|
|
||||||
if err != nil {
|
|
||||||
return ret, err
|
|
||||||
}
|
|
||||||
ret.Date = d
|
|
||||||
|
|
||||||
var DuetRegex = regexp.MustCompile(`^Duet with ([^ ]*): (.*)$`)
|
|
||||||
matches := DuetRegex.FindAllStringSubmatch(twitchFormat.Title, -1)
|
|
||||||
if matches == nil {
|
|
||||||
var SoloRegex = regexp.MustCompile(`^Solo performance: (.*)$`)
|
|
||||||
matches := SoloRegex.FindAllStringSubmatch(twitchFormat.Title, -1)
|
|
||||||
if matches == nil {
|
|
||||||
ret.SongTitle = "Does not match either solo or duet performance regex "
|
|
||||||
ret.FullTitle = twitchFormat.Title
|
|
||||||
return ret, errors.New("Does not match either solo or duet performance regex : " + twitchFormat.Title)
|
|
||||||
}
|
|
||||||
ret.Duet = false
|
|
||||||
ret.SongTitle = matches[0][1]
|
|
||||||
ret.FullTitle = twitchFormat.Title
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
ret.Duet = true
|
|
||||||
ret.OtherSinger = matches[0][1]
|
|
||||||
ret.SongTitle = matches[0][2]
|
|
||||||
ret.FullTitle = twitchFormat.Title
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SongSings struct {
|
|
||||||
SongTitle string
|
|
||||||
Sings int
|
|
||||||
}
|
|
||||||
type SingerSings struct {
|
|
||||||
SongTitle string
|
|
||||||
Sings int
|
|
||||||
}
|
|
||||||
|
|
||||||
func calculateTopNSongs(songCache []irc.SingsVideoStruct, howMany int) []SongSings {
|
|
||||||
songMap := map[string]int{}
|
|
||||||
for _, record := range songCache {
|
|
||||||
sings := songMap[record.SongTitle]
|
|
||||||
sings += 1
|
|
||||||
songMap[record.SongTitle] = sings
|
|
||||||
}
|
|
||||||
slice := make([]SongSings, 0)
|
|
||||||
for key, value := range songMap {
|
|
||||||
ss := SongSings{key, value}
|
|
||||||
slice = append(slice, ss)
|
|
||||||
}
|
|
||||||
sort.SliceStable(slice, func(i, j int) bool {
|
|
||||||
return slice[i].Sings > slice[j].Sings
|
|
||||||
})
|
|
||||||
var ret []SongSings
|
|
||||||
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 {
|
|
||||||
if record.SongTitle == SongTitle {
|
|
||||||
if record.Date.After(t) {
|
|
||||||
t = record.Date
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
func calculateLastSungSingerDate(songCache []irc.SingsVideoStruct, Singer string) time.Time {
|
|
||||||
var t time.Time
|
|
||||||
for _, record := range songCache {
|
|
||||||
if record.OtherSinger == Singer {
|
|
||||||
if record.Date.After(t) {
|
|
||||||
t = record.Date
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateCalculatedFields(songCache []irc.SingsVideoStruct) {
|
|
||||||
for i, record := range songCache {
|
|
||||||
if record.Duet {
|
|
||||||
songCache[i].LastSungSinger = calculateLastSungSingerDate(songCache, record.OtherSinger)
|
|
||||||
}
|
|
||||||
songCache[i].LastSungSong = calculateLastSungSongDate(songCache, record.SongTitle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchVoDsPagesRecursive(userID string, bearer string, from string) ([]irc.SingsVideoStruct, error) {
|
|
||||||
url := ""
|
|
||||||
if from == "" {
|
|
||||||
url = "videos?user_id=" + userID + "&first=100&type=upload"
|
|
||||||
} else {
|
|
||||||
url = "videos?user_id=" + userID + "&first=100&type=upload&after=" + from
|
|
||||||
}
|
|
||||||
|
|
||||||
vidResponse, err := twitchHTTPClient(url, bearer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var fullResponse videosResponse
|
|
||||||
err = json.Unmarshal([]byte(vidResponse), &fullResponse)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
titles := make([]irc.SingsVideoStruct, 0)
|
|
||||||
for _, videoData := range fullResponse.Data {
|
|
||||||
ret, err := twitchVidToSingsVid(videoData)
|
|
||||||
if err != nil {
|
|
||||||
titles = append(titles, ret)
|
|
||||||
} else {
|
|
||||||
titles = append(titles, ret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if fullResponse.Pagination.Cursor != "" {
|
|
||||||
fmt.Println("NOTICE: Recursion needed, cursor is " + fullResponse.Pagination.Cursor)
|
|
||||||
recurse, err := fetchVoDsPagesRecursive(userID, bearer, fullResponse.Pagination.Cursor)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("ERROR: Bailing out during recursion because of error : " + err.Error())
|
|
||||||
}
|
|
||||||
titles = append(titles, recurse...)
|
|
||||||
}
|
|
||||||
return titles, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchAllVoDs(userID string, bearer string) ([]irc.SingsVideoStruct, error) {
|
|
||||||
tokenValid, err := ValidateTwitchBearerToken(bearer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !tokenValid {
|
|
||||||
return nil, errors.New("Failed to validate token with twitch (authorization revoked?!)")
|
|
||||||
}
|
|
||||||
titles, err := fetchVoDsPagesRecursive(userID, bearer, "")
|
|
||||||
if err != nil {
|
|
||||||
return make([]irc.SingsVideoStruct, 0), err
|
|
||||||
}
|
|
||||||
return titles, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TwitchAdminHandler(response http.ResponseWriter, request *http.Request) {
|
func TwitchAdminHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
|
|
||||||
vars := mux.Vars(request)
|
vars := mux.Vars(request)
|
||||||
|
@ -478,23 +266,8 @@ 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)
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
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)
|
||||||
|
|
||||||
|
@ -502,7 +275,7 @@ func TwitchAdminHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
|
|
||||||
fmt.Fprintf(response, "I'm not okay jack! %v \n", vars)
|
fmt.Fprintf(response, "I'm not okay jack! %v \n", vars)
|
||||||
for key, val := range vars {
|
for key, val := range vars {
|
||||||
fmt.Fprintf(response, "%s = %s\n", key, val)
|
fmt.Fprint(response, "%s = %s\n", key, val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,238 +0,0 @@
|
||||||
package webserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
irc "git.martyn.berlin/martyn/twitchsingstools/internal/irc"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDateRegex(t *testing.T) {
|
|
||||||
var sourceValue videoStruct
|
|
||||||
sourceValue.Title = "Duet with FullOfEmily: Words Fail"
|
|
||||||
sourceValue.CreatedAt = "2018-03-02T20:53:41Z"
|
|
||||||
convert, err := twitchVidToSingsVid(sourceValue)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("twitchVidToSingsVid threw error : %s\n", err.Error())
|
|
||||||
}
|
|
||||||
fmt.Printf("Testing Video date '%s'\n", sourceValue.CreatedAt)
|
|
||||||
if convert.Date.Format(time.RFC1123Z) != "Fri, 02 Mar 2018 20:53:41 +0000" {
|
|
||||||
t.Errorf("%s should give Date of %s, gave '%s'\n", sourceValue.CreatedAt, "Fri, 02 Mar 2018 20:53:41 +0000", convert.Date.Format(time.RFC1123Z))
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Parsed date is %s\n", convert.Date.Format(time.RFC1123Z))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDuetRegexes(t *testing.T) {
|
|
||||||
var sourceValue videoStruct
|
|
||||||
sourceValue.Title = "Duet with FullOfEmily: Words Fail"
|
|
||||||
sourceValue.CreatedAt = "2018-03-02T20:53:41Z"
|
|
||||||
convert, err := twitchVidToSingsVid(sourceValue)
|
|
||||||
fmt.Printf("Testing Video title '%s'\n", sourceValue.Title)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("twitchVidToSingsVid threw error : %s\n", err.Error())
|
|
||||||
}
|
|
||||||
if convert.SongTitle != "Words Fail" {
|
|
||||||
t.Errorf("%s should give Title %s, gave '%s'\n", sourceValue.Title, "Words Fail", convert.SongTitle)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Song Title is '%s'\n", convert.SongTitle)
|
|
||||||
}
|
|
||||||
if convert.OtherSinger != "FullOfEmily" {
|
|
||||||
t.Errorf("%s should give Other Singer of %s, gave '%s'\n", sourceValue.Title, "FullOfEmily", convert.OtherSinger)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Other Singer is '%s'\n", convert.OtherSinger)
|
|
||||||
}
|
|
||||||
if !convert.Duet {
|
|
||||||
t.Errorf("%s should be reported as Duet, returned false\n", sourceValue.Title)
|
|
||||||
} else {
|
|
||||||
fmt.Println("Correctly seen as Duet")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSoloRegexes(t *testing.T) {
|
|
||||||
var sourceValue videoStruct
|
|
||||||
sourceValue.Title = "Solo performance: Freedom! '90 x Cups"
|
|
||||||
sourceValue.CreatedAt = "2018-03-02T20:53:41Z"
|
|
||||||
convert, err := twitchVidToSingsVid(sourceValue)
|
|
||||||
fmt.Printf("Testing Video title '%s'\n", sourceValue.Title)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("twitchVidToSingsVid threw error : %s\n", err.Error())
|
|
||||||
}
|
|
||||||
if convert.SongTitle != "Freedom! '90 x Cups" {
|
|
||||||
t.Errorf("%s should give Title %s, gave '%s'\n", sourceValue.Title, "Freedom! '90 x Cups", convert.SongTitle)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Song Title is '%s'\n", convert.SongTitle)
|
|
||||||
}
|
|
||||||
if convert.Duet {
|
|
||||||
t.Errorf("%s should be reported as Not a Duet, returned true!\n", sourceValue.Title)
|
|
||||||
} else {
|
|
||||||
fmt.Println("Correctly seen as Solo performance")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCalculatedDates(t *testing.T) {
|
|
||||||
var record irc.SingsVideoStruct
|
|
||||||
format := "2006-01-02 15:04:05 +0000 UTC"
|
|
||||||
var mockCache []irc.SingsVideoStruct
|
|
||||||
record.Date, _ = time.Parse(format, "2020-07-13 19:43:11 +0000 UTC")
|
|
||||||
record.SongTitle = "Words Fail"
|
|
||||||
record.OtherSinger = "FullOfEmily"
|
|
||||||
record.Duet = true
|
|
||||||
mockCache = append(mockCache, record)
|
|
||||||
//2020-07-13 19:43:11 +0000 UTC Words Fail FullOfEmily
|
|
||||||
record.Date, _ = time.Parse(format, "2020-07-13 19:20:27 +0000 UTC")
|
|
||||||
record.SongTitle = "Only Us"
|
|
||||||
record.OtherSinger = "PrincessPashley"
|
|
||||||
record.Duet = true
|
|
||||||
mockCache = append(mockCache, record)
|
|
||||||
//2020-07-13 19:20:27 +0000 UTC Only Us PrincessPashley
|
|
||||||
record.Date, _ = time.Parse(format, "2020-07-11 16:20:57 +0000 UTC")
|
|
||||||
record.SongTitle = "Words Fail"
|
|
||||||
record.OtherSinger = "springfanS"
|
|
||||||
record.Duet = true
|
|
||||||
mockCache = append(mockCache, record)
|
|
||||||
//2020-07-11 16:20:57 +0000 UTC Words Fail springfanS
|
|
||||||
record.Date, _ = time.Parse(format, "2020-06-06 17:00:00 +0000 UTC")
|
|
||||||
record.SongTitle = "Don't Speak"
|
|
||||||
record.OtherSinger = "PrincessPashley"
|
|
||||||
record.Duet = true
|
|
||||||
mockCache = append(mockCache, record)
|
|
||||||
//2020-06-06 17:00:00 +0000 UTC Don't Speak PrincessPashley
|
|
||||||
|
|
||||||
// First test, only sung once with this person
|
|
||||||
testPerson := "FullOfEmily"
|
|
||||||
testDate, _ := time.Parse(format, "2020-07-13 19:43:11 +0000 UTC")
|
|
||||||
checker := calculateLastSungSingerDate(mockCache, testPerson)
|
|
||||||
if checker.IsZero() {
|
|
||||||
t.Errorf("[functional test] There is a record in the cache for %s, but we got zero from the check!\n", testPerson)
|
|
||||||
} else {
|
|
||||||
if checker.Equal(testDate) {
|
|
||||||
fmt.Printf("[functional test] Correctly assertained last sing with %s was %v\n", testPerson, testDate)
|
|
||||||
} else {
|
|
||||||
t.Errorf("[functional test] Expected to have last sung with %s on %v, found %v", testPerson, testDate, checker)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second test, sang more than once with this person
|
|
||||||
testPerson = "PrincessPashley"
|
|
||||||
testDate, _ = time.Parse(format, "2020-07-13 19:20:27 +0000 UTC")
|
|
||||||
checker = calculateLastSungSingerDate(mockCache, testPerson)
|
|
||||||
if checker.IsZero() {
|
|
||||||
t.Errorf("[functional test] There is a record in the cache for %s, but we got zero from the check!\n", testPerson)
|
|
||||||
} else {
|
|
||||||
if checker.Equal(testDate) {
|
|
||||||
fmt.Printf("[functional test] Correctly assertained last sing with %s was %v\n", testPerson, testDate)
|
|
||||||
} else {
|
|
||||||
t.Errorf("[functional test] Expected to have last sung with %s on %v, found %v", testPerson, testDate, checker)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCalculatedFields(mockCache)
|
|
||||||
|
|
||||||
// Test for full cache - same as first test
|
|
||||||
testRecord := mockCache[0]
|
|
||||||
testPerson = testRecord.OtherSinger
|
|
||||||
testDate = mockCache[0].Date
|
|
||||||
if testRecord.LastSungSinger.IsZero() {
|
|
||||||
t.Errorf("[full cache test] There is a record in the cache for %s, but we got zero from the check!\n", testPerson)
|
|
||||||
} else {
|
|
||||||
if testRecord.LastSungSinger.Equal(testDate) {
|
|
||||||
fmt.Printf("[full cache test] Correctly assertained last sing with %s was %v\n", testPerson, testDate)
|
|
||||||
} else {
|
|
||||||
t.Errorf("[full cache test] Expected to have last sung with %s on %v, found %v", testPerson, testDate, testRecord.LastSungSinger)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for full cache - same as second test
|
|
||||||
testRecord = mockCache[1]
|
|
||||||
testPerson = testRecord.OtherSinger
|
|
||||||
testDate = mockCache[1].Date
|
|
||||||
if testRecord.LastSungSinger.IsZero() {
|
|
||||||
t.Errorf("[full cache test - 2nd hit] There is a record in the cache for %s, but we got zero from the check!\n", testPerson)
|
|
||||||
} else {
|
|
||||||
if testRecord.LastSungSinger.Equal(testDate) {
|
|
||||||
fmt.Printf("[full cache test - 2nd hit] Correctly assertained last sing with %s was %v\n", testPerson, testDate)
|
|
||||||
} else {
|
|
||||||
t.Errorf("[full cache test - 2nd hit] Expected to have last sung with %s on %v, found %v", testPerson, testDate, testRecord.LastSungSinger)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for full cache - should give same date as 2nd test, but with record of earlier sing.
|
|
||||||
testRecord = mockCache[3]
|
|
||||||
testPerson = testRecord.OtherSinger
|
|
||||||
testDate = mockCache[1].Date
|
|
||||||
if testRecord.LastSungSinger.IsZero() {
|
|
||||||
t.Errorf("[full cache test - 3rd hit] There is a record in the cache for %s, but we got zero from the check!\n", testPerson)
|
|
||||||
} else {
|
|
||||||
if testRecord.LastSungSinger.Equal(testDate) {
|
|
||||||
fmt.Printf("[full cache test - 3rd hit] Correctly assertained last sing with %s was %v\n", testPerson, testDate)
|
|
||||||
} else {
|
|
||||||
t.Errorf("[full cache test - 3rd hit] Expected to have last sung with %s on %v, found %v", testPerson, testDate, testRecord.LastSungSinger)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// First song test, only sung this song once
|
|
||||||
testSong := mockCache[1].SongTitle
|
|
||||||
testDate = mockCache[1].Date
|
|
||||||
checker = calculateLastSungSongDate(mockCache, testSong)
|
|
||||||
if checker.IsZero() {
|
|
||||||
t.Errorf("[functional test] There is a record in the cache for %s, but we got zero from the check!\n", testSong)
|
|
||||||
} else {
|
|
||||||
if checker.Equal(testDate) {
|
|
||||||
fmt.Printf("[functional test] Correctly assertained last sing of %s was %v\n", testPerson, testSong)
|
|
||||||
} else {
|
|
||||||
t.Errorf("[functional test] Expected to have last sing of %s on %v, found %v", testPerson, testSong, checker)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Same check, but against the cache
|
|
||||||
testSong = mockCache[1].SongTitle
|
|
||||||
testDate = mockCache[1].Date
|
|
||||||
checker = mockCache[1].LastSungSong
|
|
||||||
if checker.IsZero() {
|
|
||||||
t.Errorf("[functional test] There is a record in the cache for %s, but we got zero from the check!\n", testSong)
|
|
||||||
} else {
|
|
||||||
if checker.Equal(testDate) {
|
|
||||||
fmt.Printf("[functional test] Correctly assertained last sing of %s was %v\n", testSong, testDate)
|
|
||||||
} else {
|
|
||||||
t.Errorf("[functional test] Expected to have last sing of %s on %v, found %v", testSong, testDate, checker)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second song test, this time we expect the latest sing to match
|
|
||||||
testSong = mockCache[2].SongTitle
|
|
||||||
testDate = mockCache[0].Date
|
|
||||||
checker = calculateLastSungSongDate(mockCache, testSong)
|
|
||||||
if checker.IsZero() {
|
|
||||||
t.Errorf("[functional test] There is a record in the cache for %s, but we got zero from the check!\n", testSong)
|
|
||||||
} else {
|
|
||||||
if checker.Equal(testDate) {
|
|
||||||
fmt.Printf("[functional test] Correctly assertained last sing of %s was %v\n", testSong, testDate)
|
|
||||||
} else {
|
|
||||||
t.Errorf("[functional test] Expected to have last sing of %s on %v, found %v", testSong, testDate, checker)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now check cache for same as above
|
|
||||||
testSong = mockCache[2].SongTitle
|
|
||||||
testDate = mockCache[0].Date
|
|
||||||
checker = mockCache[2].LastSungSong
|
|
||||||
if checker.IsZero() {
|
|
||||||
t.Errorf("[functional test] There is a record in the cache for %s, but we got zero from the check!\n", testSong)
|
|
||||||
} else {
|
|
||||||
if checker.Equal(testDate) {
|
|
||||||
fmt.Printf("[functional test] Correctly assertained last sing of %s was %v\n", testSong, testDate)
|
|
||||||
} else {
|
|
||||||
t.Errorf("[functional test] Expected to have last sing of %s on %v, found %v", testSong, testDate, checker)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// func TestHumanDate(t *testing.T) {
|
|
||||||
// format := "2006-01-02 15:04:05 +0000 UTC"
|
|
||||||
// d, _ := time.Parse(format, "2020-06-06 17:00:00 +0000 UTC")
|
|
||||||
// t.Errorf("%v\n", humanize.Time(d))
|
|
||||||
// t.Errorf("%v\n", humanTimeFromTimeString("2020-07-11 16:20:57 +0000 UTC"))
|
|
||||||
|
|
||||||
// }
|
|
|
@ -65,10 +65,6 @@
|
||||||
.visibleSave {
|
.visibleSave {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
th {
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
@ -109,47 +105,12 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<h1 class="cover-heading">Karaokards admin panel for {{.Channel}}!!!</h1>
|
<h1 class="cover-heading">Karaokards admin panel for {{.Channel}}!!!</h1>
|
||||||
<ul class="nav nav-tabs" style="width: 80%;">
|
<form method="POST">
|
||||||
<li class="nav-item">
|
|
||||||
<a id="insights" class="nav-link active panel" href="#" onclick="tabClick(this)">Insights</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a id="data" class="nav-link panel" href="#" onclick="tabClick(this)">Data</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a id="csv" class="nav-link panel" href="#" onclick="tabClick(this)">CSV</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a id="bot" class="nav-link panel" href="#" onclick="tabClick(this)">Bot</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div style="width: 80%;overflow-y: scroll; display: block;" id="insightspanel" class="controlpanel">
|
|
||||||
Top 10 Songs :
|
|
||||||
<table>
|
|
||||||
<thead><tr><td>What</td><td>Sings</td>
|
|
||||||
{{ range .TopNSongs }}
|
|
||||||
<tr><td>{{ .SongTitle }}</td><td>{{ .Sings }}</td></tr>
|
|
||||||
{{ end }}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div style="width: 80%;overflow-y: scroll; display: none;" id="datapanel" class="controlpanel">
|
|
||||||
<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>
|
|
||||||
{{ 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>
|
|
||||||
{{ end }}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div style="overflow-y: scroll; display: none;" id="csvpanel" class="controlpanel">
|
|
||||||
<h2>CSV support coming!</h2>
|
|
||||||
</div>
|
|
||||||
<div style="overflow-y: scroll; display: none;" id="botpanel" class="controlpanel">
|
|
||||||
{{ 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>
|
||||||
<p>You can invite the bot to your channel by clicking here : <input id="joinButton" type="submit" name="join" value="Come on in"></p>
|
<p>You can invite the bot to your channel by clicking here : <input id="joinButton" type="submit" name="join" value="Come on in"></p>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<form method="POST">
|
|
||||||
{{ if .Leaving }}
|
{{ if .Leaving }}
|
||||||
<h2>Do you really want this bot to leave your channel?</h2>
|
<h2>Do you really want this bot to leave your channel?</h2>
|
||||||
<p><input id="leaveButton" type="submit" name="reallyleave" value="Really leave twitch channel"></p>
|
<p><input id="leaveButton" type="submit" name="reallyleave" value="Really leave twitch channel"></p>
|
||||||
|
@ -168,22 +129,7 @@
|
||||||
</table>
|
</table>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
<script type="text/javascript">
|
|
||||||
function tabClick(self) {
|
|
||||||
var theTabs = document.querySelectorAll(".panel")
|
|
||||||
for (tab of theTabs) {
|
|
||||||
tab.className = "nav-link panel"
|
|
||||||
}
|
|
||||||
self.className = "nav-link panel active"
|
|
||||||
var thePanels = document.querySelectorAll(".controlpanel")
|
|
||||||
for (panel of thePanels) {
|
|
||||||
panel.style.display = "none"
|
|
||||||
}
|
|
||||||
document.getElementById(self.id+"panel").style.display = "block"
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</main>
|
</main>
|
||||||
<footer class="mastfoot mt-auto">
|
<footer class="mastfoot mt-auto">
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
Published,Who,What,Last sang this song,Last dueted with performer
|
|
||||||
{{ range .SongData -}}
|
|
||||||
{{- .Date }},{{ .SongTitle }},{{ .OtherSinger }},{{ .LastSungSong }},{{ .LastSungSinger }}
|
|
||||||
{{ end }}
|
|
|
|
@ -55,7 +55,7 @@
|
||||||
<header class="masthead mb-auto">
|
<header class="masthead mb-auto">
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
<h3 class="masthead-brand">Twitch Sings Tools</h3>
|
<h3 class="masthead-brand">Twitch Sings Tools</h3>
|
||||||
<p class="masthead-brand">(Not endorsed by Twitch, the app is called "Twitch Sings" and these are tools for it, whaddya want it to be called?!)</p>
|
<p>(Not endorsed by Twitch, the app is called "Twitch Sings" and these are tools for it, whaddya want it to be called?!)</p>
|
||||||
<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>
|
||||||
<a class="nav-link active" href="https://id.twitch.tv/oauth2/authorize?client_id={{.ClientID}}&redirect_uri={{.BaseURI}}/twitchadmin&response_type=code&scope=user:read:broadcast">Admin - log in with twitch</a>
|
<a class="nav-link active" href="https://id.twitch.tv/oauth2/authorize?client_id={{.ClientID}}&redirect_uri={{.BaseURI}}/twitchadmin&response_type=code&scope=user:read:broadcast">Admin - log in with twitch</a>
|
||||||
|
|
Loading…
Reference in New Issue