cleanup - twitch-go - twitch.tv web application in Go
 (HTM) git clone git://git.codemadness.org/twitch-go
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit e11fa18a7c6d467bbb58e57eea61bf300f008e26
 (DIR) parent 8d76f4f7ae7abefc7ae35e0644f9c86b299020ba
 (HTM) Author: Hiltjo Posthuma <hiltjo@codemadness.org>
       Date:   Sun, 10 May 2015 10:26:14 +0200
       
       cleanup
       
       - separate twitch into src module.
       - remove regex route system, its overkill, use handlefunc.
       - separate handlers into handlers.go
       
       Diffstat:
         R static/twitch.css -> data/static/t… |       0 
         R static/twitch.sh -> data/static/tw… |       0 
         A data/templates/pages/featured.html  |      35 +++++++++++++++++++++++++++++++
         A data/templates/pages/game.html      |      34 +++++++++++++++++++++++++++++++
         A data/templates/pages/games.html     |      23 +++++++++++++++++++++++
         R templates/pages/links.html -> data… |       0 
         A data/templates/pages/playlist.html  |       8 ++++++++
         R templates/themes/default/page.html… |       0 
         A handlers.go                         |      69 ++++++++++++++++++++++++++++++
         M main.go                             |     414 ++++++-------------------------
         A src/twitch/twitch.go                |     167 +++++++++++++++++++++++++++++++
         D templates/pages/featured.html       |      35 -------------------------------
         D templates/pages/game.html           |      34 -------------------------------
         D templates/pages/games.html          |      23 -----------------------
         D templates/pages/playlist.html       |       7 -------
       
       15 files changed, 406 insertions(+), 443 deletions(-)
       ---
 (DIR) diff --git a/static/twitch.css b/data/static/twitch.css
 (DIR) diff --git a/static/twitch.sh b/data/static/twitch.sh
 (DIR) diff --git a/data/templates/pages/featured.html b/data/templates/pages/featured.html
       @@ -0,0 +1,35 @@
       +{{define "title"}}Featured streams{{end}}
       +{{define "class"}}featured{{end}}
       +
       +{{define "content"}}
       +
       +<table class="table" border="0">
       +<thead>
       +        <tr>
       +                <th class="game"><b>Game</b></th>
       +                <th class="name"><b>Name</b></th>
       +                <th class="title"><b>Title</b></th>
       +                <th class="playlist"><b>Playlist</b></th>
       +                <th class="viewers" align="right"><b>Viewers</b></th>
       +        </tr>
       +</thead>
       +<tbody>
       +        
       +{{range .Featured }}
       +<tr>
       +        <td class="game"><a href="/game?g={{.Stream.Game}}">{{.Stream.Game}}</a></td>
       +        <td class="name"><a href="{{.Stream.Channel.Url}}">{{.Stream.Channel.Display_name}}</a></td>
       +        <td class="title"><a href="{{.Stream.Channel.Url}}" title="{{.Stream.Channel.Status}}">{{.Stream.Channel.Status}}</a></td>
       +        <td class="playlist">
       +                <a href="/playlist?c={{.Stream.Channel.Name}}" title="redirect to playlist file">m3u8</a> |
       +                <a href="/playlist?c={{.Stream.Channel.Name}}&f=html" title="page with playlist link">page</a> |
       +                <a href="/playlist?c={{.Stream.Channel.Name}}&f=plain" title="get link to url in plain-text">plain</a>
       +        </td>
       +        <td align="right">{{.Stream.Viewers}}</td>
       +</tr>
       +{{end}}
       +
       +</tbody>
       +</table>
       +
       +{{end}}
 (DIR) diff --git a/data/templates/pages/game.html b/data/templates/pages/game.html
       @@ -0,0 +1,34 @@
       +{{define "title"}}Game: {{.Name}}{{end}}
       +{{define "class"}}game{{end}}
       +
       +{{define "content"}}
       +
       +<table class="table" border="0">
       +<thead>
       +        <tr>
       +                <th class="name"><b>Name</b></th>
       +                <th class="title"><b>Title</b></th>
       +                <th class="playlist"><b>Playlist</b></th>
       +                <th class="viewers" align="right"><b>Viewers</b></th>
       +        </tr>
       +</thead>
       +<tbody>
       +{{with .TwitchGame}}
       +        {{range .Streams}}
       +                <tr>
       +                        <td class="name"><a href="{{.Channel.Url}}">{{.Channel.Display_name}}</a></td>
       +                        <td class="title"><a href="{{.Channel.Url}}" title="{{.Channel.Status}}">{{.Channel.Status}}</a></td>
       +                        <td class="playlist">
       +                                <a href="/playlist?c={{.Channel.Name}}" title="redirect to playlist file">m3u8</a> |
       +                                <a href="/playlist?c={{.Channel.Name}}&f=html" title="page with playlist link">page</a> |
       +                                <a href="/playlist?c={{.Channel.Name}}&f=plain" title="get link to url in plain-text">plain</a>
       +                        </td>
       +                        <td align="right">{{.Viewers}}</td>
       +                </tr>
       +
       +        {{end}}
       +{{end}}
       +</tbody>
       +</table>
       +
       +{{end}}
 (DIR) diff --git a/data/templates/pages/games.html b/data/templates/pages/games.html
       @@ -0,0 +1,23 @@
       +{{define "title"}}Games{{end}}
       +{{define "class"}}games{{end}}
       +
       +{{define "content"}}
       +<table class="table" border="0">
       +<thead>
       +<tr>
       +        <th class="game"><b>Game</b></th>
       +        <th class="viewers" align="right"><b>Viewers</b></th>
       +        <th class="channels" align="right"><b>Channels</b></th>
       +</tr>
       +</thead>
       +<tbody>
       +{{range .Top}}
       +<tr>
       +        <td class="game"><a href="/game?g={{.Game.Name}}">{{.Game.Name}}</a></td>
       +        <td class="viewers" align="right">{{.Viewers}}</td>
       +        <td class="channels" align="right">{{.Channels}}</td>
       +</tr>
       +{{end}}
       +</tbody>
       +</table>
       +{{end}}
 (DIR) diff --git a/templates/pages/links.html b/data/templates/pages/links.html
 (DIR) diff --git a/data/templates/pages/playlist.html b/data/templates/pages/playlist.html
       @@ -0,0 +1,8 @@
       +{{define "title"}}Get playlist{{end}}
       +{{define "class"}}playlist{{end}}
       +
       +{{define "content"}}
       +        <p>Copy paste the following link in <a href="http://www.videolan.org/">VLC</a> /
       +        <a href="http://mpv.io/installation/">mpv</a> as a network stream:</p>
       +        <a href="{{.Url}}">{{.Url}}</a>
       +{{end}}
 (DIR) diff --git a/templates/themes/default/page.html b/data/templates/themes/default/page.html
 (DIR) diff --git a/handlers.go b/handlers.go
       @@ -0,0 +1,69 @@
       +package main
       +
       +import (
       +        "fmt"
       +        "net/http"
       +)
       +
       +import "twitch"
       +
       +func FeaturedHandler(w http.ResponseWriter, r *http.Request) error {
       +        featured, err := twitch.GetFeatured()
       +        if err != nil {
       +                return err
       +        }
       +        return templates.Render(w, "featured.html", "page.html", featured)
       +}
       +
       +func PlaylistHandler(w http.ResponseWriter, r *http.Request) error {
       +        channel := r.FormValue("c")
       +        format := r.FormValue("f")
       +        token, err := twitch.GetToken(channel)
       +        if err != nil {
       +                return err
       +        }
       +        url := fmt.Sprintf("http://usher.justin.tv/api/channel/hls/%s.m3u8?token=%s&sig=%s", channel, token.Token, token.Sig)
       +        switch format {
       +        case "html":
       +                return templates.Render(w, "playlist.html", "page.html", struct {
       +                        Url string
       +                }{
       +                        Url: url,
       +                })
       +        case "plain":
       +                w.Write([]byte(url))
       +        default: // redirect
       +                w.Header().Set("Location", url)
       +                w.WriteHeader(http.StatusFound)
       +                w.Write([]byte(url))
       +        }
       +        return nil
       +}
       +
       +func GameHandler(w http.ResponseWriter, r *http.Request) error {
       +        gamename := r.FormValue("g")
       +        game, err := twitch.GetGame(gamename)
       +        if err != nil {
       +                return err
       +        }
       +        v := struct {
       +                Name       string
       +                TwitchGame *twitch.Game
       +        }{
       +                Name:       gamename,
       +                TwitchGame: game,
       +        }
       +        return templates.Render(w, "game.html", "page.html", v)
       +}
       +
       +func GamesHandler(w http.ResponseWriter, r *http.Request) error {
       +        games, err := twitch.GetGames()
       +        if err != nil {
       +                return err
       +        }
       +        return templates.Render(w, "games.html", "page.html", games)
       +}
       +
       +func LinksHandler(w http.ResponseWriter, r *http.Request) error {
       +        return templates.Render(w, "links.html", "page.html", make(map[string]string))
       +}
 (DIR) diff --git a/main.go b/main.go
       @@ -1,7 +1,6 @@
        package main
        
        import (
       -        "encoding/json"
                "errors"
                "flag"
                "fmt"
       @@ -10,200 +9,54 @@ import (
                "io/ioutil"
                "net"
                "net/http"
       -        "net/url"
                "os"
                "os/signal"
                "path/filepath"
       -        "regexp"
                "strings"
                "syscall"
                "time"
        )
        
       -type AppConfig struct {
       +// config
       +type Config struct {
                Addr             string
                AddrType         string
                Password         string // password to reload templates etc, see /admin route.
                TemplateThemeDir string
                TemplatePageDir  string
       +        StaticContentDir string
                Pidfile          string
        }
        
       -type TwitchToken struct {
       -        Mobile_restricted bool
       -        Sig               string
       -        Token             string
       +type Templates struct {
       +        Pages  map[string]*template.Template
       +        Themes map[string]*template.Template
        }
        
       -type TwitchFeatured struct {
       -        Featured []struct {
       -                Image     string
       -                Priority  int
       -                Scheduled bool
       -                Sponsored bool
       -                Stream    struct {
       -                        Id          int
       -                        Average_fps float64
       -                        Created_at  string
       -                        Channel     struct {
       -                                Broadcaster_language string
       -                                Delay                int
       -                                Followers            int
       -                                Display_name         string
       -                                Language             string
       -                                Name                 string
       -                                Logo                 string
       -                                Mature               bool
       -                                Partner              bool
       -                                Status               string
       -                                Updated_at           string
       -                                Url                  string
       -                                Views                int
       -                        }
       -                        Game         string
       -                        Video_height int
       -                        Viewers      int
       -                }
       -                Text  string
       -                Title string
       -        }
       -}
       -
       -type TwitchGame struct {
       -        Streams []struct {
       -                Id          int
       -                Average_fps float64
       -                Channel     struct {
       -                        Broadcaster_language string
       -                        Delay                int
       -                        Followers            int
       -                        Display_name         string
       -                        Language             string
       -                        Name                 string
       -                        Logo                 string
       -                        Mature               bool
       -                        Partner              bool
       -                        Status               string
       -                        Updated_at           string
       -                        Url                  string
       -                        Views                int
       -                }
       -                Created_at   string
       -                Game         string
       -                Video_height int
       -                Viewers      int
       -        }
       -}
       -
       -type TwitchGames struct {
       -        Top []struct {
       -                Channels int
       -                Game     struct {
       -                        Box struct {
       -                                Large    string
       -                                Medium   string
       -                                Small    string
       -                                Template string
       -                        }
       -                        Giantbomb_id int
       -                        Logo         struct {
       -                                Large    string
       -                                Medium   string
       -                                Small    string
       -                                Template string
       -                        }
       -                        Name string
       -                }
       -                Viewers int
       -        }
       -}
       -
       -type Route struct {
       -        Regexstr string
       -        Re       *regexp.Regexp
       -        Fn       HandlerCallback
       -        Method   string // POST, GET, HEAD etc.
       -}
       -
       -type RouteMatch struct {
       -        Route *Route
       -        Names map[string]string
       -}
       -
       -type RouteHandler struct {
       -        Routes []Route
       -}
       -
       -type HandlerCallback func(http.ResponseWriter, *http.Request, *RouteMatch) error
       -
       -var pages map[string]*template.Template
       -var themes map[string]*template.Template
       -var appconfig AppConfig
       -
       -func (r *RouteHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
       -        match := FindRouteMatch(req, r.Routes)
       -        if match != nil {
       -                err := match.Route.Fn(res, req, match)
       -                if err != nil {
       -                        res.WriteHeader(http.StatusInternalServerError)
       -                        res.Write([]byte(err.Error()))
       -                        return
       -                }
       -        } else {
       -                http.FileServer(http.Dir("static")).ServeHTTP(res, req)
       -        }
       -}
       -
       -func NewRoute(method, restr string, fn HandlerCallback) Route {
       -        return Route{
       -                Method:   method,
       -                Regexstr: restr,
       -                Re:       regexp.MustCompile(restr),
       -                Fn:       fn,
       -        }
       -}
       +var appconfig Config
       +var templates *Templates
        
       -func FindRouteMatch(req *http.Request, rs []Route) *RouteMatch {
       -        for _, r := range rs {
       -                // check HTTP method, empty in route always matches.
       -                if len(r.Method) > 0 && req.Method != r.Method {
       -                        continue
       -                }
       -                // check path (regex).
       -                matches := r.Re.FindStringSubmatch(req.URL.Path)
       -                matcheslen := len(matches)
       -                if matcheslen == 0 {
       -                        continue
       -                }
       -                names := r.Re.SubexpNames()
       -                nameslen := len(names)
       -                // make map of named group matches in regex match.
       -                tomap := map[string]string{}
       -                for i := 1; i < matcheslen && i < nameslen; i++ {
       -                        tomap[names[i]] = matches[i]
       -                }
       -                return &RouteMatch{
       -                        Route: &r,
       -                        Names: tomap,
       -                }
       -        }
       -        return nil
       +func NewTemplates() *Templates {
       +        t := &Templates{}
       +        t.Pages = make(map[string]*template.Template)
       +        t.Themes = make(map[string]*template.Template)
       +        return t
        }
        
       -// NOTE: uses global "themes" and "pages" variable: map[string]*Template.
       -func RenderTemplate(w io.Writer, pagename string, themename string, data interface{}) error {
       -        if _, ok := themes[themename]; !ok {
       +// NOTE: uses "themes" and "pages" variable: map[string]*Template.
       +func (t *Templates) Render(w io.Writer, pagename string, themename string, data interface{}) error {
       +        if _, ok := t.Themes[themename]; !ok {
                        return errors.New(fmt.Sprintf("theme template \"%s\" not found", themename))
                }
       -        if _, ok := pages[pagename]; !ok {
       +        if _, ok := t.Pages[pagename]; !ok {
                        return errors.New(fmt.Sprintf("page template \"%s\" not found", pagename))
                }
       -        render, err := pages[pagename].Clone()
       +        render, err := t.Pages[pagename].Clone()
                if err != nil {
                        return err
                }
                // NOTE: the template.Tree must be copied after Clone() too.
       -        _, err = render.AddParseTree("render", themes[themename].Tree.Copy())
       +        _, err = render.AddParseTree("render", t.Themes[themename].Tree.Copy())
                if err != nil {
                        return err
                }
       @@ -214,7 +67,23 @@ func RenderTemplate(w io.Writer, pagename string, themename string, data interfa
                return nil
        }
        
       -func LoadPages(path string) (map[string]*template.Template, error) {
       +func (t *Templates) LoadPages(path string) error {
       +        templates, err := t.LoadTemplates(path)
       +        if err == nil {
       +                t.Pages = templates
       +        }
       +        return err
       +}
       +
       +func (t *Templates) LoadThemes(path string) error {
       +        templates, err := t.LoadTemplates(path)
       +        if err == nil {
       +                t.Themes = templates
       +        }
       +        return err
       +}
       +
       +func (t *Templates) LoadTemplates(path string) (map[string]*template.Template, error) {
                m := make(map[string]*template.Template)
                path, err := filepath.Abs(path)
                if err != nil {
       @@ -245,163 +114,19 @@ func LoadPages(path string) (map[string]*template.Template, error) {
                return m, err
        }
        
       -func ReadAllUrl(url string) ([]byte, error) {
       -        resp, err := http.Get(url)
       -        if err != nil {
       -                return nil, err
       -        }
       -        defer resp.Body.Close()
       -        body, err := ioutil.ReadAll(resp.Body)
       -        if err != nil {
       -                return nil, err
       -        }
       -        return body, nil
       -}
       -
       -func GetToken(channel string) (*TwitchToken, error) {
       -        url := fmt.Sprintf("https://api.twitch.tv/api/channels/%s/access_token", channel)
       -        body, err := ReadAllUrl(url)
       -        if err != nil {
       -                return nil, err
       -        }
       -        var v TwitchToken
       -        err = json.Unmarshal(body, &v)
       -        if err != nil {
       -                return nil, err
       -        }
       -        return &v, nil
       -}
       -
       -func GetFeatured() (*TwitchFeatured, error) {
       -        url := "https://api.twitch.tv/kraken/streams/featured?limit=100"
       -        body, err := ReadAllUrl(url)
       -        if err != nil {
       -                return nil, err
       -        }
       -        var v TwitchFeatured
       -        err = json.Unmarshal(body, &v)
       -        if err != nil {
       -                return nil, err
       -        }
       -        return &v, nil
       -}
       -
       -func GetGames() (*TwitchGames, error) {
       -        url := "https://api.twitch.tv/kraken/games/top?limit=100"
       -        body, err := ReadAllUrl(url)
       -        if err != nil {
       -                return nil, err
       -        }
       -        var v TwitchGames
       -        err = json.Unmarshal(body, &v)
       -        if err != nil {
       -                return nil, err
       -        }
       -        return &v, nil
       -}
       -
       -func GetGame(game string) (*TwitchGame, error) {
       -        s := fmt.Sprintf("https://api.twitch.tv/kraken/streams?game=%s", url.QueryEscape(game))
       -        body, err := ReadAllUrl(s)
       -        if err != nil {
       -                return nil, err
       -        }
       -        var v TwitchGame
       -        err = json.Unmarshal(body, &v)
       -        if err != nil {
       -                return nil, err
       -        }
       -        return &v, nil
       -}
       -
       -func FeaturedHandler(w http.ResponseWriter, r *http.Request, m *RouteMatch) error {
       -        featured, err := GetFeatured()
       -        if err != nil {
       -                return err
       -        }
       -        return RenderTemplate(w, "featured.html", "page.html", featured)
       -}
       -
       -func PlaylistHandler(w http.ResponseWriter, r *http.Request, m *RouteMatch) error {
       -        channel := m.Names["channel"]
       -        format := m.Names["format"]
       -        if channel == "" {
       -                return errors.New("no channel specified")
       -        }
       -        token, err := GetToken(channel)
       -        if err != nil {
       -                return err
       -        }
       -        url := fmt.Sprintf("http://usher.justin.tv/api/channel/hls/%s.m3u8?token=%s&sig=%s", channel, token.Token, token.Sig)
       -        switch format {
       -        case "html":
       -                return RenderTemplate(w, "playlist.html", "page.html", struct {
       -                        Url string
       -                }{
       -                        Url: url,
       -                })
       -                break
       -        case "plain":
       -                w.Write([]byte(url))
       -                break
       -        default: // redirect
       -                w.WriteHeader(http.StatusFound)
       -                w.Header().Set("Location", url)
       -                w.Write([]byte(url))
       -        }
       -        return nil
       -}
       -
       -func GameHandler(w http.ResponseWriter, r *http.Request, m *RouteMatch) error {
       -        if m.Names["game"] == "" {
       -                return errors.New("no channel specified")
       -        }
       -        game, err := GetGame(m.Names["game"])
       -        if err != nil {
       -                return err
       -        }
       -        v := struct {
       -                Name       string
       -                TwitchGame TwitchGame
       -        }{
       -                Name:       m.Names["game"],
       -                TwitchGame: *game,
       -        }
       -        return RenderTemplate(w, "game.html", "page.html", v)
       -}
       -
       -func GamesHandler(w http.ResponseWriter, r *http.Request, m *RouteMatch) error {
       -        games, err := GetGames()
       -        if err != nil {
       -                return err
       -        }
       -        return RenderTemplate(w, "games.html", "page.html", games)
       -}
       -
       -func LinksHandler(w http.ResponseWriter, r *http.Request, m *RouteMatch) error {
       -        return RenderTemplate(w, "links.html", "page.html", make(map[string]string))
       -}
       +type TwitchHandler func(http.ResponseWriter, *http.Request) error
        
       -// Reload templates, write status to client.
       -// on error the old templates are kept in memory.
       -func ReloadTemplateHandler(w http.ResponseWriter, r *http.Request, m *RouteMatch) error {
       -        if appconfig.Password == "" || m.Names["password"] != appconfig.Password {
       -                w.WriteHeader(http.StatusUnauthorized)
       -                w.Write([]byte("401: unauthorized"))
       -                return nil
       -        }
       -        newpages, err := LoadPages(appconfig.TemplatePageDir)
       -        if err != nil {
       -                return err
       -        }
       -        newthemes, err := LoadPages(appconfig.TemplateThemeDir)
       -        if err != nil {
       -                return err
       +func MakeHandler(h TwitchHandler) func(http.ResponseWriter, *http.Request) {
       +        return func(w http.ResponseWriter, r *http.Request) {
       +                err := r.ParseForm()
       +                if err == nil {
       +                        err = h(w, r)
       +                }
       +                if err != nil {
       +                        w.WriteHeader(500)
       +                        w.Write([]byte("500 " + err.Error()))
       +                }
                }
       -        pages = newpages
       -        themes = newthemes
       -        w.Write([]byte("OK"))
       -        return nil
        }
        
        func usage() {
       @@ -410,9 +135,10 @@ func usage() {
        }
        
        func main() {
       -        appconfig = AppConfig{
       -                TemplateThemeDir: "templates/themes/default",
       -                TemplatePageDir:  "templates/pages/",
       +        appconfig = Config{
       +                TemplateThemeDir: "data/templates/themes/default",
       +                TemplatePageDir:  "data/templates/pages/",
       +                StaticContentDir: "data/static/",
                }
                appconfig.Pidfile = *flag.String("f", "", "PID file")
                appconfig.Addr = *flag.String("l", "127.0.0.1:8080", "listen address")
       @@ -425,22 +151,6 @@ func main() {
                        panic(err)
                }
        
       -        routes := []Route{
       -                NewRoute("", `^/$`, FeaturedHandler),
       -                NewRoute("", `^/featured[/]?$`, FeaturedHandler),
       -                NewRoute("", `^/games[/]?$`, GamesHandler),
       -                NewRoute("", `^/game[/]?$`, GamesHandler),
       -                NewRoute("", `^/game/(?P<game>[a-zA-Z0-9_ :+'"\-]*)[/]?$`, GameHandler),
       -                NewRoute("", `/playlist/(?P<channel>[^/]*)/(?P<format>.*)[/]?$`, PlaylistHandler),
       -                NewRoute("", `/playlist/(?P<channel>[^/]*)[/]?$`, PlaylistHandler),
       -                NewRoute("", `^/links[/]?$`, LinksHandler),
       -                // special admin handlers: should require a password.
       -                NewRoute("", `^/admin/reloadtemplates/(?P<password>.*)$`, ReloadTemplateHandler),
       -        }
       -        r := &RouteHandler{
       -                Routes: routes,
       -        }
       -
                // Write PID to pid file.
                if appconfig.Pidfile != "" {
                        pid := os.Getpid()
       @@ -473,18 +183,34 @@ func main() {
                        }
                }()
        
       +        templates = NewTemplates()
                // Parse templates and keep in-memory.
       -        pages, err = LoadPages(appconfig.TemplatePageDir)
       +        err = templates.LoadPages(appconfig.TemplatePageDir)
                if err != nil {
                        panic(err)
                }
       -        themes, err = LoadPages(appconfig.TemplateThemeDir)
       +        err = templates.LoadThemes(appconfig.TemplateThemeDir)
                if err != nil {
                        panic(err)
                }
        
       +        http.HandleFunc("/featured", MakeHandler(FeaturedHandler))
       +        http.HandleFunc("/playlist", MakeHandler(PlaylistHandler))
       +        http.HandleFunc("/games", MakeHandler(GamesHandler))
       +        http.HandleFunc("/game", MakeHandler(GameHandler))
       +        http.HandleFunc("/links", MakeHandler(LinksHandler))
       +
       +        fileserv := http.FileServer(http.Dir(appconfig.StaticContentDir)).ServeHTTP
       +        http.HandleFunc("/", MakeHandler(func(w http.ResponseWriter, r *http.Request) error {
       +                if r.URL.Path == "/" {
       +                        return FeaturedHandler(w, r)
       +                } else {
       +                        fileserv(w, r)
       +                        return nil
       +                }
       +        }))
       +
                s := &http.Server{
       -                Handler:        r,
                        ReadTimeout:    10 * time.Second,
                        WriteTimeout:   10 * time.Second,
                        MaxHeaderBytes: 1 << 20,
 (DIR) diff --git a/src/twitch/twitch.go b/src/twitch/twitch.go
       @@ -0,0 +1,167 @@
       +package twitch
       +
       +import (
       +        "encoding/json"
       +        "fmt"
       +        "io/ioutil"
       +        "net/http"
       +        "net/url"
       +)
       +
       +type Token struct {
       +        Mobile_restricted bool
       +        Sig               string
       +        Token             string
       +}
       +
       +type Featured struct {
       +        Featured []struct {
       +                Image     string
       +                Priority  int
       +                Scheduled bool
       +                Sponsored bool
       +                Stream    struct {
       +                        Id          int
       +                        Average_fps float64
       +                        Created_at  string
       +                        Channel     struct {
       +                                Broadcaster_language string
       +                                Delay                int
       +                                Followers            int
       +                                Display_name         string
       +                                Language             string
       +                                Name                 string
       +                                Logo                 string
       +                                Mature               bool
       +                                Partner              bool
       +                                Status               string
       +                                Updated_at           string
       +                                Url                  string
       +                                Views                int
       +                        }
       +                        Game         string
       +                        Video_height int
       +                        Viewers      int
       +                }
       +                Text  string
       +                Title string
       +        }
       +}
       +
       +type Game struct {
       +        Streams []struct {
       +                Id          int
       +                Average_fps float64
       +                Channel     struct {
       +                        Broadcaster_language string
       +                        Delay                int
       +                        Followers            int
       +                        Display_name         string
       +                        Language             string
       +                        Name                 string
       +                        Logo                 string
       +                        Mature               bool
       +                        Partner              bool
       +                        Status               string
       +                        Updated_at           string
       +                        Url                  string
       +                        Views                int
       +                }
       +                Created_at   string
       +                Game         string
       +                Video_height int
       +                Viewers      int
       +        }
       +}
       +
       +type Games struct {
       +        Top []struct {
       +                Channels int
       +                Game     struct {
       +                        Box struct {
       +                                Large    string
       +                                Medium   string
       +                                Small    string
       +                                Template string
       +                        }
       +                        Giantbomb_id int
       +                        Logo         struct {
       +                                Large    string
       +                                Medium   string
       +                                Small    string
       +                                Template string
       +                        }
       +                        Name string
       +                }
       +                Viewers int
       +        }
       +}
       +
       +func ReadAllUrl(url string) ([]byte, error) {
       +        resp, err := http.Get(url)
       +        if err != nil {
       +                return nil, err
       +        }
       +        defer resp.Body.Close()
       +        body, err := ioutil.ReadAll(resp.Body)
       +        if err != nil {
       +                return nil, err
       +        }
       +        return body, nil
       +}
       +
       +func GetToken(channel string) (*Token, error) {
       +        url := fmt.Sprintf("https://api.twitch.tv/api/channels/%s/access_token", channel)
       +        body, err := ReadAllUrl(url)
       +        if err != nil {
       +                return nil, err
       +        }
       +        var v Token
       +        err = json.Unmarshal(body, &v)
       +        if err != nil {
       +                return nil, err
       +        }
       +        return &v, nil
       +}
       +
       +func GetFeatured() (*Featured, error) {
       +        url := "https://api.twitch.tv/kraken/streams/featured?limit=100"
       +        body, err := ReadAllUrl(url)
       +        if err != nil {
       +                return nil, err
       +        }
       +        var v Featured
       +        err = json.Unmarshal(body, &v)
       +        if err != nil {
       +                return nil, err
       +        }
       +        return &v, nil
       +}
       +
       +func GetGames() (*Games, error) {
       +        url := "https://api.twitch.tv/kraken/games/top?limit=100"
       +        body, err := ReadAllUrl(url)
       +        if err != nil {
       +                return nil, err
       +        }
       +        var v Games
       +        err = json.Unmarshal(body, &v)
       +        if err != nil {
       +                return nil, err
       +        }
       +        return &v, nil
       +}
       +
       +func GetGame(game string) (*Game, error) {
       +        s := fmt.Sprintf("https://api.twitch.tv/kraken/streams?game=%s", url.QueryEscape(game))
       +        body, err := ReadAllUrl(s)
       +        if err != nil {
       +                return nil, err
       +        }
       +        var v Game
       +        err = json.Unmarshal(body, &v)
       +        if err != nil {
       +                return nil, err
       +        }
       +        return &v, nil
       +}
 (DIR) diff --git a/templates/pages/featured.html b/templates/pages/featured.html
       @@ -1,35 +0,0 @@
       -{{define "title"}}Featured streams{{end}}
       -{{define "class"}}featured{{end}}
       -
       -{{define "content"}}
       -
       -<table class="table" border="0">
       -<thead>
       -        <tr>
       -                <th class="game"><b>Game</b></th>
       -                <th class="name"><b>Name</b></th>
       -                <th class="title"><b>Title</b></th>
       -                <th class="playlist"><b>Playlist</b></th>
       -                <th class="viewers" align="right"><b>Viewers</b></th>
       -        </tr>
       -</thead>
       -<tbody>
       -        
       -{{range .Featured }}
       -<tr>
       -        <td class="game"><a href="/game/{{.Stream.Game}}">{{.Stream.Game}}</a></td>
       -        <td class="name"><a href="{{.Stream.Channel.Url}}">{{.Stream.Channel.Display_name}}</a></td>
       -        <td class="title"><a href="{{.Stream.Channel.Url}}" title="{{.Stream.Channel.Status}}">{{.Stream.Channel.Status}}</a></td>
       -        <td class="playlist">
       -                <a href="/playlist/{{.Stream.Channel.Name}}" title="redirect to playlist file">m3u8</a> |
       -                <a href="/playlist/{{.Stream.Channel.Name}}/html" title="page with playlist link">page</a> |
       -                <a href="/playlist/{{.Stream.Channel.Name}}/plain" title="get link to url in plain-text">plain</a>
       -        </td>
       -        <td align="right">{{.Stream.Viewers}}</td>
       -</tr>
       -{{end}}
       -
       -</tbody>
       -</table>
       -
       -{{end}}
 (DIR) diff --git a/templates/pages/game.html b/templates/pages/game.html
       @@ -1,34 +0,0 @@
       -{{define "title"}}Game: {{.Name}}{{end}}
       -{{define "class"}}game{{end}}
       -
       -{{define "content"}}
       -
       -<table class="table" border="0">
       -<thead>
       -        <tr>
       -                <th class="name"><b>Name</b></th>
       -                <th class="title"><b>Title</b></th>
       -                <th class="playlist"><b>Playlist</b></th>
       -                <th class="viewers" align="right"><b>Viewers</b></th>
       -        </tr>
       -</thead>
       -<tbody>
       -{{with .TwitchGame}}
       -        {{range .Streams}}
       -                <tr>
       -                        <td class="name"><a href="{{.Channel.Url}}">{{.Channel.Display_name}}</a></td>
       -                        <td class="title"><a href="{{.Channel.Url}}" title="{{.Channel.Status}}">{{.Channel.Status}}</a></td>
       -                        <td class="playlist">
       -                                <a href="/playlist/{{.Channel.Name}}" title="redirect to playlist file">m3u8</a> |
       -                                <a href="/playlist/{{.Channel.Name}}/html" title="page with playlist link">page</a> |
       -                                <a href="/playlist/{{.Channel.Name}}/plain" title="get link to url in plain-text">plain</a>
       -                        </td>
       -                        <td align="right">{{.Viewers}}</td>
       -                </tr>
       -
       -        {{end}}
       -{{end}}
       -</tbody>
       -</table>
       -
       -{{end}}
 (DIR) diff --git a/templates/pages/games.html b/templates/pages/games.html
       @@ -1,23 +0,0 @@
       -{{define "title"}}Games{{end}}
       -{{define "class"}}games{{end}}
       -
       -{{define "content"}}
       -<table class="table" border="0">
       -<thead>
       -<tr>
       -        <th class="game"><b>Game</b></th>
       -        <th class="viewers" align="right"><b>Viewers</b></th>
       -        <th class="channels" align="right"><b>Channels</b></th>
       -</tr>
       -</thead>
       -<tbody>
       -{{range .Top}}
       -<tr>
       -        <td class="game"><a href="/game/{{.Game.Name}}">{{.Game.Name}}</a></td>
       -        <td class="viewers" align="right">{{.Viewers}}</td>
       -        <td class="channels" align="right">{{.Channels}}</td>
       -</tr>
       -{{end}}
       -</tbody>
       -</table>
       -{{end}}
 (DIR) diff --git a/templates/pages/playlist.html b/templates/pages/playlist.html
       @@ -1,7 +0,0 @@
       -{{define "title"}}Get playlist{{end}}
       -{{define "class"}}playlist{{end}}
       -
       -{{define "content"}}
       -        <p>Copy paste the following link in <a href="http://www.videolan.org/">VLC</a> / <a href="http://mpv.io/installation/">mpv</a> as a network stream:</p>
       -        <a href="{{.Url}}">{{.Url}}</a>
       -{{end}}