main.go - 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
---
main.go (8057B)
---
1 package main
2
3 import (
4 "errors"
5 "flag"
6 "fmt"
7 "html/template"
8 "io"
9 "io/ioutil"
10 "log"
11 "net"
12 "net/http"
13 "os"
14 "path/filepath"
15 "strings"
16 "time"
17 )
18
19 import "twitch"
20
21 type TwitchHandler func(http.ResponseWriter, *http.Request) error
22
23 type Templates struct {
24 Pages map[string]*template.Template
25 Themes map[string]*template.Template
26 }
27
28 var templates *Templates
29
30 // config
31 var config_addr string
32 var config_addrtype string
33 var config_chmod uint
34 var config_clientid string
35 var config_datadir string
36 var config_templatethemedir string = "templates/themes/default/"
37 var config_templatepagedir string = "templates/pages/"
38 var config_staticcontentdir string = "static/"
39
40 func NewTemplates() *Templates {
41 t := &Templates{}
42 t.Pages = make(map[string]*template.Template)
43 t.Themes = make(map[string]*template.Template)
44 return t
45 }
46
47 // NOTE: uses "themes" and "pages" variable: map[string]*Template.
48 func (t *Templates) Render(w io.Writer, pagename string, themename string, data interface{}) error {
49 if _, ok := t.Themes[themename]; !ok {
50 return errors.New(fmt.Sprintf("theme template \"%s\" not found", themename))
51 }
52 if _, ok := t.Pages[pagename]; !ok {
53 return errors.New(fmt.Sprintf("page template \"%s\" not found", pagename))
54 }
55 render, err := t.Pages[pagename].Clone()
56 if err != nil {
57 return err
58 }
59 // NOTE: the template.Tree must be copied after Clone() too.
60 _, err = render.AddParseTree("render", t.Themes[themename].Tree.Copy())
61 if err != nil {
62 return err
63 }
64 err = render.ExecuteTemplate(w, "render", data)
65 if err != nil {
66 return err
67 }
68 return nil
69 }
70
71 func (t *Templates) LoadPages(path string) error {
72 templates, err := t.LoadTemplates(path)
73 if err == nil {
74 t.Pages = templates
75 }
76 return err
77 }
78
79 func (t *Templates) LoadThemes(path string) error {
80 templates, err := t.LoadTemplates(path)
81 if err == nil {
82 t.Themes = templates
83 }
84 return err
85 }
86
87 func (t *Templates) LoadTemplates(path string) (map[string]*template.Template, error) {
88 m := make(map[string]*template.Template)
89 path, err := filepath.Abs(path)
90 if err != nil {
91 return m, err
92 }
93 err = filepath.Walk(path, func(p string, info os.FileInfo, err error) error {
94 if err != nil {
95 return err
96 }
97 if info.IsDir() {
98 return nil
99 }
100 // strip prefix.
101 name := strings.TrimPrefix(p, path)
102 // replace potentially inconsistent paths (Windows).
103 name = strings.Replace(name, "\\", "/", -1)
104 name = strings.TrimPrefix(name, "/")
105 // read template data from file..
106 data, err := ioutil.ReadFile(p)
107 if err != nil {
108 return err
109 }
110 t := template.New(name)
111 _, err = t.Parse(string(data))
112 if err != nil {
113 return err
114 }
115 m[name] = t
116 return err
117 })
118 return m, err
119 }
120
121 func NotFound(w http.ResponseWriter, err string) {
122 http.Error(w, "404 "+err, http.StatusNotFound)
123 }
124
125 func BadRequest(w http.ResponseWriter, err string) {
126 http.Error(w, "400 "+err, http.StatusBadRequest)
127 }
128
129 func MakeHandler(h TwitchHandler) func(http.ResponseWriter, *http.Request) {
130 return func(w http.ResponseWriter, r *http.Request) {
131 err := r.ParseForm()
132 if err == nil {
133 err = h(w, r)
134 } else {
135 BadRequest(w, "Can't parse form: "+err.Error())
136 return
137 }
138 // unhandled error so far: 500
139 if err != nil {
140 http.Error(w, "500 "+err.Error(), http.StatusInternalServerError)
141 }
142 }
143 }
144
145 func FeaturedHandler(w http.ResponseWriter, r *http.Request) error {
146 featured, err := twitch.GetFeatured()
147 if err != nil {
148 return err
149 }
150 return templates.Render(w, "featured.html", "page.html", featured)
151 }
152
153 func PlaylistHandler(w http.ResponseWriter, r *http.Request) error {
154 channel := r.FormValue("c")
155 if channel == "" {
156 BadRequest(w, "No channel name specified")
157 return nil
158 }
159 format := r.FormValue("f") // if empty "redirect".
160 token, err := twitch.GetToken(channel)
161 if err != nil {
162 return err
163 }
164 url := fmt.Sprintf("http://usher.justin.tv/api/channel/hls/%s.m3u8?token=%s&sig=%s", channel, token.Token, token.Sig)
165 switch format {
166 case "html":
167 return templates.Render(w, "playlist.html", "page.html", struct {
168 Url string
169 }{
170 Url: url,
171 })
172 case "plain":
173 w.Write([]byte(url))
174 default: // redirect
175 w.Header().Set("Location", url)
176 w.WriteHeader(http.StatusFound)
177 if r.Method == "GET" {
178 w.Write([]byte(url))
179 }
180 }
181 return nil
182 }
183
184 func GameHandler(w http.ResponseWriter, r *http.Request) error {
185 gamename := r.FormValue("g")
186 if gamename == "" {
187 BadRequest(w, "No game name specified")
188 return nil
189 }
190 game, err := twitch.GetGame(gamename)
191 if err != nil {
192 return err
193 }
194 v := struct {
195 Name string
196 TwitchGame *twitch.Game
197 }{
198 Name: gamename,
199 TwitchGame: game,
200 }
201 return templates.Render(w, "game.html", "page.html", v)
202 }
203
204 func GamesHandler(w http.ResponseWriter, r *http.Request) error {
205 games, err := twitch.GetGames()
206 if err != nil {
207 return err
208 }
209 return templates.Render(w, "games.html", "page.html", games)
210 }
211
212 func LinksHandler(w http.ResponseWriter, r *http.Request) error {
213 return templates.Render(w, "links.html", "page.html", struct{}{})
214 }
215
216 func VODSHandler(w http.ResponseWriter, r *http.Request) error {
217 var err error
218 var users *twitch.Users
219 var videos *twitch.Videos
220
221 format := r.FormValue("f")
222 name := r.FormValue("n")
223
224 v := struct {
225 Search string
226 User twitch.User
227 Videos []twitch.Video
228 }{
229 Search: name,
230 }
231
232 if len(name) > 0 {
233 users, err = twitch.GetUseridByName(name)
234 if err != nil {
235 NotFound(w, "(login) name not found or an error occurred")
236 return nil
237 }
238 if len(users.Items) == 0 {
239 NotFound(w, "(login) name not found")
240 return nil
241 }
242 for _, u := range users.Items {
243 videos, err = twitch.GetVideosByUser(u.Id)
244 if err != nil {
245 return err
246 }
247 break // return for first user.
248 }
249 v.User = users.Items[0]
250 v.Videos = videos.Items
251 }
252
253 switch format {
254 case "atom":
255 w.Header().Set("Content-Type", "text/xml; charset=utf-8")
256 fmt.Fprintf(w, `<?xml version="1.0" encoding="UTF-8"?>`+"\n")
257 return templates.Render(w, "vods.xml", "atom.xml", v)
258 default:
259 return templates.Render(w, "vods.html", "page.html", v)
260 }
261 }
262
263 func usage() {
264 fmt.Fprintf(os.Stderr, "Usage: %s\n", os.Args[0])
265 flag.PrintDefaults()
266 }
267
268 func main() {
269 flag.StringVar(&twitch.Clientid, "c", "", "Client-ID token")
270 flag.StringVar(&config_datadir, "d", "", "Chdir to data directory")
271 flag.StringVar(&config_addr, "l", "127.0.0.1:8080", "listen address")
272 flag.UintVar(&config_chmod, "m", 0755, "Permission for unix domain socket")
273 flag.StringVar(&config_addrtype, "t", "tcp4", `listen type: "tcp", "tcp4", "tcp6", "unix" or "unixpacket"`)
274 flag.Parse()
275
276 if len(twitch.Clientid) == 0 {
277 usage()
278 os.Exit(1)
279 }
280
281 // Remove previous UDS if it exists.
282 if config_addrtype == "unix" {
283 os.Remove(config_addr)
284 }
285
286 l, err := net.Listen(config_addrtype, config_addr)
287 if err != nil {
288 log.Fatalln(err)
289 }
290
291 // Set permission on UDS.
292 if config_addrtype == "unix" {
293 err = os.Chmod(config_addr, os.FileMode(config_chmod))
294 if err != nil {
295 log.Fatalln(err)
296 }
297 }
298
299 if config_datadir != "" {
300 err = os.Chdir(config_datadir)
301 if err != nil {
302 log.Fatalln(err)
303 }
304 }
305
306 templates = NewTemplates()
307 // Parse templates and keep in-memory.
308 err = templates.LoadPages(config_templatepagedir)
309 if err != nil {
310 log.Fatalln(err)
311 }
312 err = templates.LoadThemes(config_templatethemedir)
313 if err != nil {
314 log.Fatalln(err)
315 }
316
317 http.HandleFunc("/featured", MakeHandler(FeaturedHandler))
318 http.HandleFunc("/playlist", MakeHandler(PlaylistHandler))
319 http.HandleFunc("/games", MakeHandler(GamesHandler))
320 http.HandleFunc("/game", MakeHandler(GameHandler))
321 http.HandleFunc("/links", MakeHandler(LinksHandler))
322 http.HandleFunc("/vods", MakeHandler(VODSHandler))
323
324 fileserv := http.FileServer(http.Dir(config_staticcontentdir)).ServeHTTP
325 http.HandleFunc("/", MakeHandler(func(w http.ResponseWriter, r *http.Request) error {
326 if r.URL.Path == "/" {
327 return FeaturedHandler(w, r)
328 } else {
329 fileserv(w, r)
330 return nil
331 }
332 }))
333
334 s := &http.Server{
335 ReadTimeout: 30 * time.Second,
336 WriteTimeout: 30 * time.Second,
337 MaxHeaderBytes: 1 << 20,
338 }
339 s.Serve(l)
340 }