Compare commits
	
		
			2 commits
		
	
	
		
			b891b74d4b
			...
			d13c00eff0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d13c00eff0 | |||
| 4572f7dae1 | 
					 10 changed files with 579 additions and 20 deletions
				
			
		
							
								
								
									
										3
									
								
								Makefile
									
										
									
									
									
								
							
							
						
						
									
										3
									
								
								Makefile
									
										
									
									
									
								
							| 
						 | 
					@ -4,6 +4,9 @@ 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
 | 
				
			||||||
  - go get
 | 
					  - make test
 | 
				
			||||||
  - go build
 | 
					  - make
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- 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
 | 
				
			||||||
  - go get
 | 
					  - make test
 | 
				
			||||||
  - go build
 | 
					  - make
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trigger:
 | 
					trigger:
 | 
				
			||||||
  ref:
 | 
					  ref:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,6 +29,9 @@ 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,6 +51,7 @@ 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,6 +85,16 @@ 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"`
 | 
				
			||||||
| 
						 | 
					@ -93,6 +103,9 @@ 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
 | 
				
			||||||
| 
						 | 
					@ -130,6 +143,22 @@ 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,9 +1,13 @@
 | 
				
			||||||
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"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,6 +48,31 @@ 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) {
 | 
				
			||||||
| 
						 | 
					@ -115,6 +144,13 @@ 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 {
 | 
				
			||||||
| 
						 | 
					@ -129,9 +165,12 @@ 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"]]
 | 
				
			||||||
	var td = TemplateData{channelData.Name, channelData.Command, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft}
 | 
						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}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if request.Method == "POST" {
 | 
						if request.Method == "POST" {
 | 
				
			||||||
		request.ParseForm()
 | 
							request.ParseForm()
 | 
				
			||||||
| 
						 | 
					@ -157,7 +196,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}
 | 
								td = TemplateData{record.Name, record.Command, record.ExtraStrings, record.JoinTime, record.JoinTime.Format(irc.UTCFormat), false, record.HasLeft, channelData.VideoCache, topNSongs}
 | 
				
			||||||
			ircBot.JoinChannel(record.Name)
 | 
								ircBot.JoinChannel(record.Name)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		sourceData := ircBot.ChannelData[vars["channel"]]
 | 
							sourceData := ircBot.ChannelData[vars["channel"]]
 | 
				
			||||||
| 
						 | 
					@ -173,7 +212,12 @@ func AdminHandler(response http.ResponseWriter, request *http.Request) {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		ircBot.Database.Write("channelData", vars["channel"], sourceData)
 | 
							ircBot.Database.Write("channelData", vars["channel"], sourceData)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	tmpl := template.Must(template.ParseFiles("web/admin.html"))
 | 
						tmpl, err := template.New("admin.html").Funcs(template.FuncMap{
 | 
				
			||||||
 | 
							"niceDate": humanize.Time,
 | 
				
			||||||
 | 
						}).ParseFiles("web/admin.html")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic(err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	tmpl.Execute(response, td)
 | 
						tmpl.Execute(response, td)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -203,6 +247,174 @@ 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)
 | 
				
			||||||
| 
						 | 
					@ -266,8 +478,23 @@ 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -275,7 +502,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.Fprint(response, "%s = %s\n", key, val)
 | 
								fmt.Fprintf(response, "%s = %s\n", key, val)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										238
									
								
								internal/webserver/webserver_test.go
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										238
									
								
								internal/webserver/webserver_test.go
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
					@ -0,0 +1,238 @@
 | 
				
			||||||
 | 
					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,6 +65,10 @@
 | 
				
			||||||
                    .visibleSave {
 | 
					                    .visibleSave {
 | 
				
			||||||
                      display: inline;
 | 
					                      display: inline;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                    th {
 | 
				
			||||||
 | 
					                      font-weight: bold;
 | 
				
			||||||
 | 
					                      font-style: italic;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
            </style>
 | 
					            </style>
 | 
				
			||||||
          </head>
 | 
					          </head>
 | 
				
			||||||
| 
						 | 
					@ -105,12 +109,47 @@
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    </script>
 | 
					                    </script>
 | 
				
			||||||
                        <h1 class="cover-heading">Karaokards admin panel for {{.Channel}}!!!</h1>
 | 
					                        <h1 class="cover-heading">Karaokards admin panel for {{.Channel}}!!!</h1>
 | 
				
			||||||
                        <form method="POST">
 | 
					                        <ul class="nav nav-tabs" style="width: 80%;">
 | 
				
			||||||
 | 
					                          <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>
 | 
				
			||||||
| 
						 | 
					@ -129,7 +168,22 @@
 | 
				
			||||||
                          </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">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										4
									
								
								web/data.csv
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								web/data.csv
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					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>(Not endorsed by Twitch, the app is called "Twitch Sings" and these are tools for it, whaddya want it to be called?!)</p>
 | 
					                  <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>
 | 
				
			||||||
                  <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…
	
	Add table
		
		Reference in a new issue