add support to list VODS by loginname using Twitch Helix API - 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 3cbd531da39473bbbb3afad4ec609ab635a48681
 (DIR) parent 045f529d96751a6e97eddf8ff83594e5a5c438b7
 (HTM) Author: Hiltjo Posthuma <hiltjo@codemadness.org>
       Date:   Sat,  6 Apr 2019 19:52:40 +0200
       
       add support to list VODS by loginname using Twitch Helix API
       
       and request an Atom feed.
       
       + improvements:
       
       - linewrap long titles for VODS.
       - get response data even on error as this contains useful debug data.
       
       Diffstat:
         M data/static/twitch.css              |       3 +++
         A data/templates/pages/vods.html      |      40 +++++++++++++++++++++++++++++++
         A data/templates/pages/vods.xml       |      12 ++++++++++++
         A data/templates/themes/default/atom… |       1 +
         M data/templates/themes/default/page… |       1 +
         M main.go                             |      51 +++++++++++++++++++++++++++++++
         M src/twitch/twitch.go                |      89 +++++++++++++++++++++++++++++--
       
       7 files changed, 194 insertions(+), 3 deletions(-)
       ---
 (DIR) diff --git a/data/static/twitch.css b/data/static/twitch.css
       @@ -50,6 +50,9 @@ table.table tr th {
        table.table tr td {
                white-space: nowrap;
        }
       +table.table tr td.wrap {
       +        white-space: normal;
       +}
        table.table tr th.viewers,
        table.table tr th.channels {
                text-align: right;
 (DIR) diff --git a/data/templates/pages/vods.html b/data/templates/pages/vods.html
       @@ -0,0 +1,40 @@
       +{{define "title"}}VODS{{if .Search}} for {{.User.Display_name}}{{end}}{{end}}
       +{{define "class"}}vods{{end}}
       +
       +{{define "content"}}
       +
       +{{if .Search}}
       +
       +<a href="?n={{.Search}}&amp;format=atom">Atom feed</a>
       +
       +<table class="table" border="0">
       +<thead>
       +        <tr>
       +                <th class="created_at" align="left">Created</th>
       +                <th class="title" align="left">Title</th>
       +                <th class="duration" align="right">Duration</th>
       +                <th class="viewcount" align="right">Viewcount</th>
       +        </tr>
       +</thead>
       +<tbody>
       +{{range .Videos}}
       +        <tr>
       +                <td>{{.Created_at}}</td>
       +                <td class="wrap"><a href="{{.Url}}">{{.Title}}</a></td>
       +                <td align="right"><a href="{{.Url}}">{{.Duration}}</a></td>
       +                <td align="right"><a href="{{.Url}}">{{.View_count}}</a></td>
       +        </tr>
       +{{end}}
       +</tbody>
       +</table>
       +
       +{{else}}
       +
       +<form method="get" action="">
       +        <input type="search" name="n" value="" placeholder="Login name..." />
       +        <input type="submit" name="list" value="List vods" />
       +</form>
       +
       +{{end}}
       +
       +{{end}}
 (DIR) diff --git a/data/templates/pages/vods.xml b/data/templates/pages/vods.xml
       @@ -0,0 +1,12 @@
       +{{define "content"}}<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
       +{{range .Videos}}
       +        <entry>
       +                <title type="html">{{.Title}}</title>
       +                <link rel="alternate" type="text/html" href="{{.Url}}" />
       +                <id>{{.Url}}</id>
       +                <updated>{{.Created_at}}</updated>
       +                <published>{{.Created_at}}</published>
       +        </entry>
       +{{end}}
       +</feed>
       +{{end}}
 (DIR) diff --git a/data/templates/themes/default/atom.xml b/data/templates/themes/default/atom.xml
       @@ -0,0 +1 @@
       +{{template "content" .}}
 (DIR) diff --git a/data/templates/themes/default/page.html b/data/templates/themes/default/page.html
       @@ -11,6 +11,7 @@
        <form method="get" action="/playlist">
                <a href="/featured">Featured</a> | 
                <a href="/games">Games</a> | 
       +        <a href="/vods">VODS</a> |
                <a href="https://git.codemadness.org/twitch-go/">Source-code</a> | 
                <a href="/twitch.sh">SH version</a> | 
                <a href="/links">Links</a>
 (DIR) diff --git a/main.go b/main.go
       @@ -118,6 +118,10 @@ func (t *Templates) LoadTemplates(path string) (map[string]*template.Template, e
                return m, err
        }
        
       +func NotFound(w http.ResponseWriter, err string) {
       +        http.Error(w, "404 "+err, http.StatusNotFound)
       +}
       +
        func BadRequest(w http.ResponseWriter, err string) {
                http.Error(w, "400 "+err, http.StatusBadRequest)
        }
       @@ -209,6 +213,52 @@ func LinksHandler(w http.ResponseWriter, r *http.Request) error {
                return templates.Render(w, "links.html", "page.html", struct{}{})
        }
        
       +func VODSHandler(w http.ResponseWriter, r *http.Request) error {
       +        var err error
       +        var users *twitch.Users
       +        var videos *twitch.Videos
       +
       +        format := r.FormValue("format")
       +        name := r.FormValue("n")
       +
       +        v := struct {
       +                Search string
       +                User   twitch.User
       +                Videos []twitch.Video
       +        }{
       +                Search: name,
       +        }
       +
       +        if len(name) > 0 {
       +                users, err = twitch.GetUseridByName(name)
       +                if err != nil {
       +                        NotFound(w, "(login) name not found or an error occurred")
       +                        return nil
       +                }
       +                if len(users.Items) == 0 {
       +                        NotFound(w, "(login) name not found")
       +                        return nil
       +                }
       +                for _, u := range users.Items {
       +                        videos, err = twitch.GetVideosByUser(u.Id)
       +                        if err != nil {
       +                                return err
       +                        }
       +                        break // return for first user.
       +                }
       +                v.User = users.Items[0]
       +                v.Videos = videos.Items
       +        }
       +
       +        if format == "atom" {
       +                w.Header().Set("Content-Type", "text/xml; charset=utf-8")
       +                fmt.Fprintf(w, `<?xml version="1.0" encoding="UTF-8"?>`+"\n")
       +                return templates.Render(w, "vods.xml", "atom.xml", v)
       +        } else {
       +                return templates.Render(w, "vods.html", "page.html", v)
       +        }
       +}
       +
        func usage() {
                fmt.Fprintf(os.Stderr, "Usage: %s\n", os.Args[0])
                flag.PrintDefaults()
       @@ -268,6 +318,7 @@ func main() {
                http.HandleFunc("/games", MakeHandler(GamesHandler))
                http.HandleFunc("/game", MakeHandler(GameHandler))
                http.HandleFunc("/links", MakeHandler(LinksHandler))
       +        http.HandleFunc("/vods", MakeHandler(VODSHandler))
        
                fileserv := http.FileServer(http.Dir(config_staticcontentdir)).ServeHTTP
                http.HandleFunc("/", MakeHandler(func(w http.ResponseWriter, r *http.Request) error {
 (DIR) diff --git a/src/twitch/twitch.go b/src/twitch/twitch.go
       @@ -102,6 +102,23 @@ type Games struct {
                }
        }
        
       +type User struct {
       +        Broadcaster_type  string `json:"broadcaster_type"`
       +        Description       string `json:"description"`
       +        Display_name      string `json:"display_name"`
       +        Email             string `json:"email"`
       +        Id                string `json:"id"`
       +        Login             string `json:"login"`
       +        Offline_image_url string `json:"offline_image_url"`
       +        Profile_image_url string `json:"profile_image_url"`
       +        Type              string `json:"type"`
       +        View_count        int64  `json:"view_count"`
       +}
       +
       +type Users struct {
       +        Items []User `json:"data"`
       +}
       +
        func ReadAllUrl(url string) ([]byte, error) {
                client := http.Client{
                        Timeout: time.Duration(30) * time.Second,
       @@ -118,13 +135,13 @@ func ReadAllUrl(url string) ([]byte, error) {
                if err != nil {
                        return nil, err
                }
       -        if resp.StatusCode != 200 {
       -                return nil, errors.New(resp.Status)
       -        }
                body, err := ioutil.ReadAll(resp.Body)
                if err != nil {
                        return nil, err
                }
       +        if resp.StatusCode != 200 {
       +                return nil, errors.New(resp.Status)
       +        }
                return body, nil
        }
        
       @@ -162,6 +179,7 @@ func GetGames() (*Games, error) {
                if err != nil {
                        return nil, err
                }
       +
                var v Games
                err = json.Unmarshal(body, &v)
                if err != nil {
       @@ -183,3 +201,68 @@ func GetGame(game string) (*Game, error) {
                }
                return &v, nil
        }
       +
       +type Video struct {
       +        Created_at    string `json:"created_at"`
       +        Description   string `json:"description"`
       +        Duration      string `json:"duration"`
       +        Id            string `json:"id"`
       +        Language      string `json:"language"`
       +        Published_at  string `json:"published_at"`
       +        Thumbnail_url string `json:"thumbnail_url"`
       +        Title         string `json:"title"`
       +        Type          string `json:"type"`
       +        Url           string `json:"url"`
       +        User_id       string `json:"user_id"`
       +        User_name     string `json:"user_name"`
       +        View_count    int64  `json:"view_count"`
       +        Viewable      string `json:"viewable"`
       +}
       +
       +type Videos struct {
       +        Items []Video `json:"data"`
       +}
       +
       +func GetVideosByGame(gameid string) (*Videos, error) {
       +        s := fmt.Sprintf("https://api.twitch.tv/helix/videos?first=100&game_id=%s", url.QueryEscape(gameid))
       +        body, err := ReadAllUrl(s)
       +        if err != nil {
       +                return nil, err
       +        }
       +        var v Videos
       +        err = json.Unmarshal(body, &v)
       +        if err != nil {
       +                return nil, err
       +        }
       +        return &v, nil
       +}
       +
       +func GetVideosByUser(userid string) (*Videos, error) {
       +        s := fmt.Sprintf("https://api.twitch.tv/helix/videos?first=100&user_id=%s",
       +                url.QueryEscape(userid))
       +        body, err := ReadAllUrl(s)
       +        if err != nil {
       +                return nil, err
       +        }
       +        var v Videos
       +        err = json.Unmarshal(body, &v)
       +        if err != nil {
       +                return nil, err
       +        }
       +        return &v, nil
       +}
       +
       +func GetUseridByName(name string) (*Users, error) {
       +        s := fmt.Sprintf("https://api.twitch.tv/helix/users?login=%s",
       +                url.QueryEscape(name))
       +        body, err := ReadAllUrl(s)
       +        if err != nil {
       +                return nil, err
       +        }
       +        var u Users
       +        err = json.Unmarshal(body, &u)
       +        if err != nil {
       +                return nil, err
       +        }
       +        return &u, nil
       +}