Commit 87c15f01 authored by Simon Kirsten's avatar Simon Kirsten
Browse files

Updated and restructured server

parent 11427013
......@@ -8,8 +8,7 @@ import (
"github.com/urfave/cli"
"code.fbi.h-da.de/simons-nzse-2/stream-tv/internal/tv"
"code.fbi.h-da.de/simons-nzse-2/stream-tv/internal/twitch"
"code.fbi.h-da.de/simons-nzse-2/stream-tv/internal/api"
"code.fbi.h-da.de/simons-nzse-2/stream-tv/internal/util"
"code.fbi.h-da.de/simons-nzse-2/stream-tv/internal/website"
)
......@@ -99,8 +98,8 @@ func main() {
mux := http.NewServeMux() // new router
mux.Handle("/", website.Handler())
mux.Handle("/twitch/", twitch.Handler())
mux.Handle("/tv/", tv.Handler())
mux.Handle("/twitch/", api.TwitchHandler())
mux.Handle("/tv/", api.TVHandler())
return http.ListenAndServe(listenAddr, mux)
}
......
package tv
package api
import (
"encoding/json"
......@@ -162,8 +162,8 @@ func eventsHandleFunc(w http.ResponseWriter, r *http.Request) {
client.Run(r.Context())
}
// Handler returns a http.Handler that serves requests for the /tv backend
func Handler() http.Handler {
// TVHandler returns a http.Handler that serves requests for the /tv backend
func TVHandler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/tv/state", stateHandleFunc)
......
package api
import (
"net/http"
"strings"
"code.fbi.h-da.de/simons-nzse-2/stream-tv/internal/util"
"code.fbi.h-da.de/simons-nzse-2/stream-tv/pkg/twitch"
)
func gamesTopHandleFunc(w http.ResponseWriter, r *http.Request) {
response, err := twitch.GetTopGames()
if err != nil {
util.ServeError(w, err)
return
}
_, err = util.ServeJSON(w, &response)
if err != nil {
util.ServeError(w, err)
return
}
}
func gamesSearchHandleFunc(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("query")
if query == "" {
http.Error(w, "Parameter 'query' is required and can not be empty.", http.StatusBadRequest)
return
}
response, err := twitch.SearchGames(query)
if err != nil {
util.ServeError(w, err)
return
}
_, err = util.ServeJSON(w, &response)
if err != nil {
util.ServeError(w, err)
return
}
}
func streamsTopHandleFunc(w http.ResponseWriter, r *http.Request) {
urlQuery := r.URL.Query()
// the following is needed as otherwise strings.Split will return [""] which we don't want
var channels []string
if channelsParam := urlQuery.Get("channels"); channelsParam != "" {
channels = strings.Split(channelsParam, ",")
}
game := urlQuery.Get("game")
language := urlQuery.Get("language")
response, err := twitch.GetTopStreams(channels, game, language)
if err != nil {
util.ServeError(w, err)
return
}
_, err = util.ServeJSON(w, &response)
if err != nil {
util.ServeError(w, err)
return
}
}
func streamsFeaturedHandleFunc(w http.ResponseWriter, r *http.Request) {
response, err := twitch.GetFeaturedStreams()
if err != nil {
util.ServeError(w, err)
return
}
_, err = util.ServeJSON(w, &response)
if err != nil {
util.ServeError(w, err)
return
}
}
// TwitchHandler returns a http.Handler that serves requests for the /twitch backend
func TwitchHandler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/twitch/games/top", gamesTopHandleFunc)
mux.HandleFunc("/twitch/games/search", gamesSearchHandleFunc)
mux.HandleFunc("/twitch/streams/top", streamsTopHandleFunc)
mux.HandleFunc("/twitch/streams/featured", streamsFeaturedHandleFunc)
return mux
}
package twitch
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"math"
"net/http"
"net/url"
"strings"
"code.fbi.h-da.de/simons-nzse-2/stream-tv/internal/util"
)
// twitchStream is the structure for streams that we get from twitch.
type twitchStream struct {
AverageFPS float64 `json:"average_fps"`
CreatedAt string `json:"created_at"`
Game string `json:"game"`
Preview struct {
Large string `json:"large"`
} `json:"preview"`
VideoHeight int `json:"video_height"`
Viewers int `json:"viewers"`
Channel struct {
Status string `json:"status"`
Language string `json:"language"`
Mature bool `json:"mature"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
Logo string `json:"logo"`
} `json:"channel"`
}
// Stream is the structure for streams that we return to the user.
type Stream struct {
AverageFPS int `json:"average_fps"`
StartedAt string `json:"started_at"`
Game string `json:"game"`
PreviewImgURL string `json:"preview_img_url"`
VideoHeight int `json:"video_height"`
Viewers int `json:"viewers"`
Status string `json:"status"`
Language string `json:"language"`
Mature bool `json:"mature"`
ChannelName string `json:"channel_name"`
ChannelDisplayName string `json:"channel_display_name"`
ChannelLogoImgURL string `json:"channel_logo_img_url"`
}
// twitchGame is the structure for games that we get from twitch.
type twitchGame struct {
// Viewers int `json:"viewers"`
// Game struct {
Name string `json:"name"`
Box struct {
Large string `json:"large"`
} `json:"box"`
Logo struct {
Large string `json:"large"`
} `json:"logo"`
Popularity int `json:"popularity"`
// }
}
// Game is the structure for games that we return to the user.
type Game struct {
Name string `json:"name"`
Viewers int `json:"viewers"`
BoxImgURL string `json:"box_img_url"`
LogoImgURL string `json:"logo_img_url"`
}
// toSimplified simplifies a twitchStream (that we got from twitch) to a Stream (that we return to the user).
func (s *twitchStream) toSimplified() Stream {
return Stream{
AverageFPS: int(math.Round(s.AverageFPS)),
StartedAt: s.CreatedAt,
Game: s.Game,
PreviewImgURL: s.Preview.Large,
VideoHeight: s.VideoHeight,
Viewers: s.Viewers,
Status: s.Channel.Status,
Language: s.Channel.Language,
Mature: s.Channel.Mature,
ChannelName: s.Channel.Name,
ChannelDisplayName: s.Channel.DisplayName,
ChannelLogoImgURL: s.Channel.Logo,
}
}
// toSimplified simplifies a twitchGame (that we got from twitch) to a Game (that we return to the user).
func (g *twitchGame) toSimplified() Game {
return Game{
Name: g.Name,
Viewers: g.Popularity,
BoxImgURL: g.Box.Large,
LogoImgURL: g.Logo.Large,
}
}
// TODO: comment from here on
func twitchRequest(endpoint string, query url.Values, output interface{}) error {
query.Set("limit", "100")
url := fmt.Sprintf("https://api.twitch.tv/kraken%s?%s", endpoint, query.Encode())
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
req.Header.Add("Client-ID", "0p78jqass6rz1alnquwivky4gah6sh")
req.Header.Add("Accept", "application/vnd.twitchtv.v5+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New(resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
err = json.Unmarshal(body, output)
if err != nil {
return err
}
return nil
}
var channelsToIDCache = make(map[string]string)
func channelsToID(channels []string) ([]string, error) {
var unknownChannels []string
for _, channel := range channels {
if _, ok := channelsToIDCache[channel]; !ok {
unknownChannels = append(unknownChannels, channel)
}
}
if unknownChannels != nil {
var twitchUsersResponse struct {
Users []struct {
ID string `json:"_id"`
Name string `json:"name"`
} `json:"users"`
}
params := url.Values{}
params.Set("login", strings.Join(unknownChannels, ","))
err := twitchRequest("/users", params, &twitchUsersResponse)
if err != nil {
return nil, err
}
for _, user := range twitchUsersResponse.Users {
channelsToIDCache[user.Name] = user.ID
}
}
var ids []string
for _, channel := range channels {
ids = append(ids, channelsToIDCache[channel])
}
return ids, nil
}
func streamsFeaturedHandleFunc(w http.ResponseWriter, r *http.Request) {
var twitchFeaturedStreamsResponse struct {
Featured []struct {
Stream twitchStream `json:"stream"`
} `json:"featured"`
}
err := twitchRequest("/streams/featured", url.Values{}, &twitchFeaturedStreamsResponse)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
featuredStreamsResponse := make([]Stream, 0)
for _, featured := range twitchFeaturedStreamsResponse.Featured {
if featured.Stream.Game != "" {
featuredStreamsResponse = append(featuredStreamsResponse, featured.Stream.toSimplified())
}
}
_, err = util.ServeJSON(w, &featuredStreamsResponse)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
}
func streamsTopHandleFunc(w http.ResponseWriter, r *http.Request) {
var twitchStreamsResponse struct {
Streams []twitchStream `json:"streams"`
}
query := r.URL.Query()
params := url.Values{}
if q := query.Get("channels"); q != "" {
// esl_csgo => 31239503 via https://dev.twitch.tv/docs/v5/reference/users/#get-users
channels := strings.Split(q, ",")
// fmt.Printf("%v\n", channels)
channels, err := channelsToID(channels)
// fmt.Printf("%v\n", channels)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
params.Set("channel", strings.Join(channels, ","))
}
if q := query.Get("game"); q != "" {
params.Set("game", q)
}
if q := query.Get("language"); q != "" {
params.Set("language", q)
}
err := twitchRequest("/streams/", params, &twitchStreamsResponse)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
topStreamsResponse := make([]Stream, 0)
for _, stream := range twitchStreamsResponse.Streams {
if stream.Game != "" { // For some reason sometimes the game is empty.
topStreamsResponse = append(topStreamsResponse, stream.toSimplified())
}
}
_, err = util.ServeJSON(w, &topStreamsResponse)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
}
func gamesTopHandleFunc(w http.ResponseWriter, r *http.Request) {
if q := r.URL.Query().Get("query"); q != "" {
gamesSearchHandleFunc(w, r)
return
}
var twitchTopGamesResponse struct {
Top []struct {
Game twitchGame
} `json:"top"`
}
err := twitchRequest("/games/top", url.Values{}, &twitchTopGamesResponse)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
topGamesResponse := make([]Game, 0)
for _, game := range twitchTopGamesResponse.Top {
topGamesResponse = append(topGamesResponse, game.Game.toSimplified())
}
_, err = util.ServeJSON(w, &topGamesResponse)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
}
func gamesSearchHandleFunc(w http.ResponseWriter, r *http.Request) {
var twitchSearchGamesResponse struct {
Games []twitchGame `json:"games"`
}
query := r.URL.Query()
if q := query.Get("query"); q == "" {
err := errors.New("Parameter 'query' required")
http.Error(w, err.Error(), http.StatusBadRequest)
log.Println(err)
return
}
params := url.Values{}
params.Set("query", query.Get("query"))
err := twitchRequest("/search/games", params, &twitchSearchGamesResponse)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
topGamesResponse := make([]Game, 0)
for _, game := range twitchSearchGamesResponse.Games {
topGamesResponse = append(topGamesResponse, game.toSimplified())
}
_, err = util.ServeJSON(w, &topGamesResponse)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
}
// TODO: implement handler for search streams
// Handler returns a http.Handler that serves requests for the /twitch backend
func Handler() http.Handler {
mux := http.NewServeMux()
// mux.Handle("/", http.NotFoundHandler()) // if the others don't match, return not found
mux.HandleFunc("/twitch/games/top", gamesTopHandleFunc)
// mux.HandleFunc("/twitch/games/search", gamesSearchHandleFunc)
mux.HandleFunc("/twitch/streams/top", streamsTopHandleFunc)
mux.HandleFunc("/twitch/streams/featured", streamsFeaturedHandleFunc)
return mux
}
......@@ -35,6 +35,11 @@ func ServeJSONWithStatus(w http.ResponseWriter, obj interface{}, status int) ([]
return body, nil
}
// ServeError is a shorthand for http.Error(w, err.Error(), http.StatusInternalServerError)
func ServeError(w http.ResponseWriter, err error) {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
// ListeningAddress represents a IP address, its interface name and if it is a loopback address.
// It is used to display all addresses a socket is listening on.
type ListeningAddress struct {
......
package twitch
import "net/url"
// twitchGame is the structure for games that we get from twitch.
type twitchGame struct {
// Viewers int `json:"viewers"`
// Game struct {
Name string `json:"name"`
Box struct {
Large string `json:"large"`
} `json:"box"`
Logo struct {
Large string `json:"large"`
} `json:"logo"`
Popularity int `json:"popularity"`
// }
}
// Game is the structure for games that we return to the user.
type Game struct {
Name string `json:"name"`
Viewers int `json:"viewers"`
BoxImgURL string `json:"box_img_url"`
LogoImgURL string `json:"logo_img_url"`
}
// toSimplified simplifies a twitchGame (that we got from twitch) to a Game (that we return to the user).
func (g *twitchGame) toSimplified() Game {
return Game{
Name: g.Name,
Viewers: g.Popularity,
BoxImgURL: g.Box.Large,
LogoImgURL: g.Logo.Large,
}
}
// GetTopGames gets the top games from the twitch API.
func GetTopGames() (response []Game, err error) {
var twitchResponse struct {
Top []struct {
Game twitchGame
} `json:"top"`
}
err = Request("/games/top", url.Values{}, &twitchResponse)
if err != nil {
return
}
response = make([]Game, 0)
for _, game := range twitchResponse.Top {
response = append(response, game.Game.toSimplified())
}
return
}
// SearchGames searches games via the twitch API.
func SearchGames(query string) (response []Game, err error) {
var twitchResponse struct {
Games []twitchGame `json:"games"`
}
params := url.Values{}
params.Set("query", query)
err = Request("/search/games", params, &twitchResponse)
if err != nil {
return
}
response = make([]Game, 0)
for _, game := range twitchResponse.Games {
response = append(response, game.toSimplified())
}
return
}
package twitch
import (
"math"
"net/url"
"strings"
)
// twitchStream is the structure for streams that we get from twitch.
type twitchStream struct {
AverageFPS float64 `json:"average_fps"`
CreatedAt string `json:"created_at"`
Game string `json:"game"`
Preview struct {
Large string `json:"large"`
} `json:"preview"`
VideoHeight int `json:"video_height"`
Viewers int `json:"viewers"`
Channel struct {
Status string `json:"status"`
Language string `json:"language"`