2019-03-18 04:46:46 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
2019-06-19 04:13:53 +02:00
|
|
|
"io/ioutil"
|
2019-06-02 01:41:28 +02:00
|
|
|
"log"
|
2019-03-18 04:46:46 +01:00
|
|
|
"net/http"
|
|
|
|
"os"
|
2019-06-19 04:13:53 +02:00
|
|
|
"path/filepath"
|
2019-03-18 04:46:46 +01:00
|
|
|
"strings"
|
2019-03-30 21:20:06 +01:00
|
|
|
|
2019-06-02 01:41:28 +02:00
|
|
|
"github.com/pkg/errors"
|
2019-06-19 04:13:53 +02:00
|
|
|
"github.com/zorchenhimer/MovieNight/common"
|
2019-03-18 04:46:46 +01:00
|
|
|
)
|
|
|
|
|
2020-08-21 11:59:58 +02:00
|
|
|
const emoteDir = "/static/emotes/"
|
2019-06-02 01:41:28 +02:00
|
|
|
|
|
|
|
type TwitchUser struct {
|
|
|
|
ID string
|
|
|
|
Login string
|
|
|
|
}
|
2019-03-18 04:46:46 +01:00
|
|
|
|
2019-06-02 01:41:28 +02:00
|
|
|
type EmoteInfo struct {
|
|
|
|
ID int
|
|
|
|
Code string
|
|
|
|
}
|
2019-03-18 04:46:46 +01:00
|
|
|
|
2020-08-21 20:05:11 +02:00
|
|
|
func loadEmotes() error {
|
2020-08-23 17:54:01 +02:00
|
|
|
newEmotes, err := processEmoteDir(common.RunPath() + emoteDir)
|
2019-06-19 04:13:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-09-22 22:51:13 +02:00
|
|
|
common.Emotes = newEmotes
|
|
|
|
|
2019-06-02 01:41:28 +02:00
|
|
|
return nil
|
|
|
|
}
|
2019-03-18 04:46:46 +01:00
|
|
|
|
2019-09-22 22:51:13 +02:00
|
|
|
func processEmoteDir(path string) (common.EmotesMap, error) {
|
2019-06-19 04:13:53 +02:00
|
|
|
dirInfo, err := ioutil.ReadDir(path)
|
2019-06-02 01:41:28 +02:00
|
|
|
if err != nil {
|
2019-09-22 22:51:13 +02:00
|
|
|
return nil, errors.Wrap(err, "could not open emoteDir:")
|
2019-03-18 04:46:46 +01:00
|
|
|
}
|
|
|
|
|
2019-06-19 04:13:53 +02:00
|
|
|
subDirs := []string{}
|
2019-03-18 04:46:46 +01:00
|
|
|
|
2019-06-19 04:13:53 +02:00
|
|
|
for _, item := range dirInfo {
|
|
|
|
// Get first level subdirs (eg, "twitch", "discord", etc)
|
|
|
|
if item.IsDir() {
|
|
|
|
subDirs = append(subDirs, item.Name())
|
|
|
|
continue
|
|
|
|
}
|
2019-03-18 04:46:46 +01:00
|
|
|
}
|
|
|
|
|
2019-09-22 22:51:13 +02:00
|
|
|
em := common.NewEmotesMap()
|
2019-06-19 04:13:53 +02:00
|
|
|
// Find top level emotes
|
2019-09-22 22:51:13 +02:00
|
|
|
em, err = findEmotes(path, em)
|
2019-03-18 04:46:46 +01:00
|
|
|
if err != nil {
|
2019-09-22 22:51:13 +02:00
|
|
|
return nil, errors.Wrap(err, "could not findEmotes() in top level directory:")
|
2019-03-18 04:46:46 +01:00
|
|
|
}
|
|
|
|
|
2019-06-19 04:13:53 +02:00
|
|
|
// Get second level subdirs (eg, "twitch", "zorchenhimer", etc)
|
|
|
|
for _, dir := range subDirs {
|
|
|
|
subd, err := ioutil.ReadDir(filepath.Join(path, dir))
|
2019-03-18 04:46:46 +01:00
|
|
|
if err != nil {
|
2019-09-22 22:51:13 +02:00
|
|
|
fmt.Printf("Error reading dir %q: %v\n", subd, err)
|
2019-06-19 04:13:53 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
for _, d := range subd {
|
|
|
|
if d.IsDir() {
|
2020-10-20 20:57:05 +02:00
|
|
|
// emotes = append(emotes, findEmotes(filepath.Join(path, dir, d.Name()))...)
|
2019-09-22 22:51:13 +02:00
|
|
|
p := filepath.Join(path, dir, d.Name())
|
|
|
|
em, err = findEmotes(p, em)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("Error finding emotes in %q: %v\n", p, err)
|
|
|
|
}
|
2019-06-19 04:13:53 +02:00
|
|
|
}
|
2019-03-18 04:46:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-20 20:57:05 +02:00
|
|
|
common.LogInfof("processEmoteDir: %d\n", len(em))
|
2019-09-22 22:51:13 +02:00
|
|
|
return em, nil
|
2019-06-19 04:13:53 +02:00
|
|
|
}
|
|
|
|
|
2020-10-20 20:57:05 +02:00
|
|
|
func substr(input string, start int, length int) string {
|
|
|
|
asRunes := []rune(input)
|
|
|
|
|
|
|
|
if start >= len(asRunes) {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
if start+length > len(asRunes) {
|
|
|
|
length = len(asRunes) - start
|
|
|
|
}
|
|
|
|
|
|
|
|
return string(asRunes[start : start+length])
|
|
|
|
}
|
|
|
|
|
2019-09-22 22:51:13 +02:00
|
|
|
func findEmotes(dir string, em common.EmotesMap) (common.EmotesMap, error) {
|
2020-10-20 20:57:05 +02:00
|
|
|
var runPathLength = len(common.RunPath() + "/static/")
|
2019-09-22 22:51:13 +02:00
|
|
|
|
2020-10-20 20:57:05 +02:00
|
|
|
common.LogDebugf("finding emotes in %q\n", dir)
|
2019-06-19 04:13:53 +02:00
|
|
|
emotePNGs, err := filepath.Glob(filepath.Join(dir, "*.png"))
|
|
|
|
if err != nil {
|
2019-09-22 22:51:13 +02:00
|
|
|
return em, fmt.Errorf("unable to glob emote directory: %s\n", err)
|
2019-06-19 04:13:53 +02:00
|
|
|
}
|
2020-10-20 20:57:05 +02:00
|
|
|
common.LogInfof("Found %d emotePNGs\n", len(emotePNGs))
|
2019-06-19 04:13:53 +02:00
|
|
|
|
|
|
|
emoteGIFs, err := filepath.Glob(filepath.Join(dir, "*.gif"))
|
|
|
|
if err != nil {
|
2019-09-22 22:51:13 +02:00
|
|
|
return em, errors.Wrap(err, "unable to glob emote directory:")
|
2019-06-19 04:13:53 +02:00
|
|
|
}
|
2020-10-20 20:57:05 +02:00
|
|
|
common.LogInfof("Found %d emoteGIFs\n", len(emoteGIFs))
|
2019-06-19 04:13:53 +02:00
|
|
|
|
2021-08-18 02:25:04 +02:00
|
|
|
emoteJPGs, err := filepath.Glob(filepath.Join(dir, "*.jpg"))
|
|
|
|
if err != nil {
|
|
|
|
return em, errors.Wrap(err, "unable to glob emote directory:")
|
|
|
|
}
|
|
|
|
common.LogInfof("Found %d emoteJPGs\n", len(emoteJPGs))
|
|
|
|
|
|
|
|
emoteSVGs, err := filepath.Glob(filepath.Join(dir, "*.svg"))
|
|
|
|
if err != nil {
|
|
|
|
return em, errors.Wrap(err, "unable to glob emote directory:")
|
|
|
|
}
|
|
|
|
common.LogInfof("Found %d emoteSVGs\n", len(emoteSVGs))
|
|
|
|
|
2019-06-19 04:13:53 +02:00
|
|
|
for _, file := range emotePNGs {
|
2020-10-20 20:57:05 +02:00
|
|
|
png := strings.ReplaceAll(common.Substr(file, runPathLength, len(file)), "\\", "/")
|
|
|
|
//common.LogDebugf("Emote PNG: %s", png)
|
|
|
|
em = em.Add(png)
|
2019-06-19 04:13:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, file := range emoteGIFs {
|
2020-10-20 20:57:05 +02:00
|
|
|
gif := strings.ReplaceAll(common.Substr(file, runPathLength, len(file)), "\\", "/")
|
|
|
|
//common.LogDebugf("Emote GIF: %s", gif)
|
|
|
|
em = em.Add(gif)
|
2019-06-19 04:13:53 +02:00
|
|
|
}
|
|
|
|
|
2021-08-18 02:25:04 +02:00
|
|
|
for _, file := range emoteJPGs {
|
2021-08-18 19:04:52 +02:00
|
|
|
jpg := strings.ReplaceAll(common.Substr(file, runPathLength, len(file)), "\\", "/")
|
2021-08-18 02:25:04 +02:00
|
|
|
//common.LogDebugf("Emote JPG: %s", jpg)
|
|
|
|
em = em.Add(jpg)
|
|
|
|
}
|
|
|
|
|
2021-08-18 02:38:21 +02:00
|
|
|
for _, file := range emoteSVGs {
|
2021-08-18 19:04:52 +02:00
|
|
|
svg := strings.ReplaceAll(common.Substr(file, runPathLength, len(file)), "\\", "/")
|
2021-08-18 02:25:04 +02:00
|
|
|
//common.LogDebugf("Emote SVG: %s", svg)
|
|
|
|
em = em.Add(svg)
|
|
|
|
}
|
|
|
|
|
2019-09-22 22:51:13 +02:00
|
|
|
return em, nil
|
2019-06-02 01:41:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func getEmotes(names []string) error {
|
|
|
|
users := getUserIDs(names)
|
2019-06-19 04:13:53 +02:00
|
|
|
users = append(users, TwitchUser{ID: "0", Login: "twitch"})
|
2019-06-02 01:41:28 +02:00
|
|
|
|
|
|
|
for _, user := range users {
|
|
|
|
emotes, cheers, err := getChannelEmotes(user.ID)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "could not get emote data for \"%s\"", user.ID)
|
|
|
|
}
|
|
|
|
|
2020-10-20 20:57:05 +02:00
|
|
|
emoteUserDir := filepath.Join(common.RunPath()+emoteDir, "twitch", user.Login)
|
2019-06-02 01:41:28 +02:00
|
|
|
if _, err := os.Stat(emoteUserDir); os.IsNotExist(err) {
|
|
|
|
os.MkdirAll(emoteUserDir, os.ModePerm)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, emote := range emotes {
|
|
|
|
if !strings.ContainsAny(emote.Code, `:;\[]|?&`) {
|
2019-06-19 04:13:53 +02:00
|
|
|
filePath := filepath.Join(emoteUserDir, emote.Code+".png")
|
2019-06-02 01:41:28 +02:00
|
|
|
file, err := os.Create(filePath)
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return errors.Wrapf(err, "could not create emote file in path \"%s\":", filePath)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = downloadEmote(emote.ID, file)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "could not download emote %s:", emote.Code)
|
|
|
|
}
|
2019-03-18 04:46:46 +01:00
|
|
|
}
|
|
|
|
}
|
2019-06-02 01:41:28 +02:00
|
|
|
|
|
|
|
for amount, sizes := range cheers {
|
|
|
|
name := fmt.Sprintf("%sCheer%s.gif", user.Login, amount)
|
2019-06-19 04:13:53 +02:00
|
|
|
filePath := filepath.Join(emoteUserDir, name)
|
2019-06-02 01:41:28 +02:00
|
|
|
file, err := os.Create(filePath)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "could not create emote file in path \"%s\":", filePath)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = downloadCheerEmote(sizes["4"], file)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "could not download emote %s:", name)
|
|
|
|
}
|
2019-03-18 04:46:46 +01:00
|
|
|
}
|
|
|
|
}
|
2019-06-02 01:41:28 +02:00
|
|
|
return nil
|
2019-03-18 04:46:46 +01:00
|
|
|
}
|
|
|
|
|
2019-06-02 01:41:28 +02:00
|
|
|
func getUserIDs(names []string) []TwitchUser {
|
|
|
|
logins := strings.Join(names, "&login=")
|
|
|
|
request, err := http.NewRequest("GET", fmt.Sprintf("https://api.twitch.tv/helix/users?login=%s", logins), nil)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalln("Error generating new request:", err)
|
|
|
|
}
|
|
|
|
request.Header.Set("Client-ID", settings.TwitchClientID)
|
2020-06-04 09:32:01 +02:00
|
|
|
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", settings.TwitchClientSecret))
|
2019-06-02 01:41:28 +02:00
|
|
|
|
|
|
|
client := http.Client{}
|
|
|
|
resp, err := client.Do(request)
|
2019-03-18 04:46:46 +01:00
|
|
|
if err != nil {
|
2019-06-02 01:41:28 +02:00
|
|
|
log.Fatalln("Error sending request:", err)
|
2019-03-18 04:46:46 +01:00
|
|
|
}
|
|
|
|
|
2019-06-02 01:41:28 +02:00
|
|
|
decoder := json.NewDecoder(resp.Body)
|
|
|
|
type userResponse struct {
|
|
|
|
Data []TwitchUser
|
|
|
|
}
|
|
|
|
var data userResponse
|
2019-03-18 04:46:46 +01:00
|
|
|
|
2019-06-02 01:41:28 +02:00
|
|
|
err = decoder.Decode(&data)
|
2019-03-18 04:46:46 +01:00
|
|
|
if err != nil {
|
2019-06-02 01:41:28 +02:00
|
|
|
log.Fatalln("Error decoding data:", err)
|
2019-03-18 04:46:46 +01:00
|
|
|
}
|
|
|
|
|
2019-06-02 01:41:28 +02:00
|
|
|
return data.Data
|
|
|
|
}
|
2019-03-18 04:46:46 +01:00
|
|
|
|
2019-06-02 01:41:28 +02:00
|
|
|
func getChannelEmotes(ID string) ([]EmoteInfo, map[string]map[string]string, error) {
|
|
|
|
resp, err := http.Get("https://api.twitchemotes.com/api/v4/channels/" + ID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, errors.Wrap(err, "could not get emotes")
|
|
|
|
}
|
|
|
|
decoder := json.NewDecoder(resp.Body)
|
2019-03-18 04:46:46 +01:00
|
|
|
|
2019-06-02 01:41:28 +02:00
|
|
|
type EmoteResponse struct {
|
|
|
|
Emotes []EmoteInfo
|
|
|
|
Cheermotes map[string]map[string]string
|
|
|
|
}
|
|
|
|
var data EmoteResponse
|
2019-03-18 04:46:46 +01:00
|
|
|
|
2019-06-02 01:41:28 +02:00
|
|
|
err = decoder.Decode(&data)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, errors.Wrap(err, "could not decode emotes")
|
|
|
|
}
|
2019-03-18 04:46:46 +01:00
|
|
|
|
2019-06-02 01:41:28 +02:00
|
|
|
return data.Emotes, data.Cheermotes, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func downloadEmote(ID int, file *os.File) error {
|
|
|
|
resp, err := http.Get(fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v1/%d/3.0", ID))
|
|
|
|
if err != nil {
|
|
|
|
return errors.Errorf("could not download emote file %s: %v", file.Name(), err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
_, err = io.Copy(file, resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Errorf("could not save emote: %v", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func downloadCheerEmote(url string, file *os.File) error {
|
|
|
|
resp, err := http.Get(url)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Errorf("could not download cheer file %s: %v", file.Name(), err)
|
2019-03-18 04:46:46 +01:00
|
|
|
}
|
2019-06-02 01:41:28 +02:00
|
|
|
defer resp.Body.Close()
|
2019-03-18 04:46:46 +01:00
|
|
|
|
2019-06-02 01:41:28 +02:00
|
|
|
_, err = io.Copy(file, resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Errorf("could not save cheer: %v", err)
|
|
|
|
}
|
|
|
|
return nil
|
2019-03-18 04:46:46 +01:00
|
|
|
}
|