github.com/jaredwarren/rpi_music@v0.1.2/server/server.go (about) 1 package server 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "html/template" 8 "net/http" 9 "sync" 10 "time" 11 12 "github.com/99designs/gqlgen/graphql/handler" 13 "github.com/99designs/gqlgen/graphql/playground" 14 "github.com/gorilla/mux" 15 "github.com/jaredwarren/rpi_music/db" 16 "github.com/jaredwarren/rpi_music/downloader" 17 "github.com/jaredwarren/rpi_music/graph" 18 "github.com/jaredwarren/rpi_music/graph/generated" 19 "github.com/jaredwarren/rpi_music/log" 20 "github.com/jaredwarren/rpi_music/model" 21 "github.com/jaredwarren/rpi_music/player" 22 "github.com/spf13/viper" 23 ) 24 25 // Config provides basic configuration 26 type Config struct { 27 Host string 28 ReadTimeout time.Duration 29 WriteTimeout time.Duration 30 Db db.DBer 31 Logger log.Logger 32 } 33 34 // HTMLServer represents the web service that serves up HTML 35 type HTMLServer struct { 36 server *http.Server 37 wg sync.WaitGroup 38 logger log.Logger 39 } 40 41 func CorsMiddleware(next http.Handler) http.Handler { 42 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 43 w.Header().Set("Access-Control-Allow-Origin", "*") 44 w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") 45 w.Header().Set("Access-Control-Allow-Methods", "GET, POST,OPTIONS") 46 if r.Method == "OPTIONS" { 47 w.WriteHeader(http.StatusOK) 48 return 49 } 50 next.ServeHTTP(w, r) 51 }) 52 } 53 54 // Start launches the HTML Server 55 func StartHTTPServer(cfg *Config) *HTMLServer { 56 // Setup Context 57 _, cancel := context.WithCancel(context.Background()) 58 defer cancel() 59 60 // init server 61 s := New(cfg.Db, cfg.Logger) 62 63 // Setup Handlers 64 r := mux.NewRouter() 65 r.Use(s.loggingMiddleware) 66 r.Use(mux.CORSMethodMiddleware(r)) 67 r.Use(CorsMiddleware) // for now all all 68 69 // Public Methods 70 r.HandleFunc("/login", s.LoginForm).Methods(http.MethodGet, http.MethodOptions) 71 r.HandleFunc("/logout", s.Logout).Methods(http.MethodGet, http.MethodOptions) 72 // r.HandleFunc("/login", s.Login).Methods(http.MethodPost, http.MethodOptions) 73 r.PathPrefix("/public/").Handler(http.StripPrefix("/public/", http.FileServer(http.Dir("./public")))) 74 75 // setup graphql 76 r.HandleFunc("/playground", playground.Handler("GraphQL playground", "/query")) 77 srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{ 78 Resolvers: &graph.Resolver{ 79 Db: cfg.Db, 80 }, 81 })) 82 graphql := r.PathPrefix("/graphql").Subrouter() 83 // graphql.Use(CorsMiddleware) 84 graphql.Handle("", srv).Methods(http.MethodPost, http.MethodGet, http.MethodOptions).Name("graphql") 85 86 // if viper.GetBool("csrf.enabled") { 87 // r.Use(s.requireCSRF) 88 // } 89 90 // login-required methods 91 sub := r.PathPrefix("/").Subrouter() 92 // sub.Use(s.requireLoginMiddleware)// TEMP for testing 93 94 sub.HandleFunc("/echo", s.HandleWS).Methods(http.MethodGet) 95 96 // list songs 97 sub.HandleFunc("/", s.ListSongHandler).Methods(http.MethodGet) 98 sub.HandleFunc("/songs", s.ListSongHandler).Methods(http.MethodGet) 99 100 // Song 101 ssub := sub.PathPrefix("/song").Subrouter() 102 ssub.HandleFunc(fmt.Sprintf("/%s", model.NewSongID), s.NewSongFormHandler).Methods(http.MethodGet) 103 ssub.HandleFunc("", s.NewSongHandler).Methods(http.MethodPost) 104 ssub.HandleFunc(fmt.Sprintf("/%s", model.NewSongID), s.NewSongHandler).Methods(http.MethodPost) 105 ssub.HandleFunc("/{song_id}", s.EditSongFormHandler).Methods(http.MethodGet) 106 ssub.HandleFunc("/{song_id}", s.UpdateSongHandler).Methods(http.MethodPut, http.MethodPost) 107 ssub.HandleFunc("/{song_id}", s.DeleteSongHandler).Methods(http.MethodDelete) 108 ssub.HandleFunc("/{song_id}/play", s.PlaySongHandler).Methods(http.MethodGet) 109 ssub.HandleFunc("/{song_id}/delete", s.DeleteSongHandler).Methods(http.MethodGet) 110 ssub.HandleFunc("/{song_id}/stop", s.StopSongHandler).Methods(http.MethodGet) 111 ssub.HandleFunc("/{song_id}/play_video", s.PlayVideoHandler).Methods(http.MethodGet) 112 ssub.HandleFunc("/{song_id}/print", s.PrintHandler).Methods(http.MethodGet) 113 ssub.HandleFunc("/{song_id}/json", s.JSONHandler).Methods(http.MethodGet) 114 ssub.HandleFunc("/json", s.JSONHandler).Methods(http.MethodGet) 115 116 // Config Endpoints 117 csub := sub.PathPrefix("/config").Subrouter() 118 csub.HandleFunc("", s.ConfigFormHandler).Methods(http.MethodGet) 119 csub.HandleFunc("", s.ConfigHandler).Methods(http.MethodPost) 120 121 // Player Endpoints 122 psub := sub.PathPrefix("/player").Subrouter() 123 psub.HandleFunc("/", s.PlayerHandler).Methods(http.MethodGet) 124 125 // Static files 126 sub.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) 127 sub.PathPrefix("/song_files/").Handler(http.StripPrefix("/song_files/", http.FileServer(http.Dir(viper.GetString("player.song_root"))))) 128 sub.PathPrefix("/thumb_files/").Handler(http.StripPrefix("/thumb_files/", http.FileServer(http.Dir(viper.GetString("player.thumb_root"))))) 129 130 // Create the HTML Server 131 htmlServer := HTMLServer{ 132 logger: cfg.Logger, 133 server: &http.Server{ 134 Addr: cfg.Host, 135 Handler: r, 136 ReadTimeout: cfg.ReadTimeout, 137 WriteTimeout: cfg.WriteTimeout, 138 MaxHeaderBytes: 1 << 20, 139 }, 140 } 141 142 // Start the listener 143 htmlServer.wg.Add(1) 144 go func() { 145 cfg.Logger.Info("Starting HTTP server", log.Any("host", cfg.Host), log.Any("https", viper.GetBool("https"))) 146 if viper.GetBool("https") { 147 htmlServer.server.ListenAndServeTLS("localhost.crt", "localhost.key") 148 } else { 149 htmlServer.server.ListenAndServe() 150 } 151 htmlServer.wg.Done() 152 }() 153 154 return &htmlServer 155 } 156 157 func (s *Server) loggingMiddleware(next http.Handler) http.Handler { 158 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 159 s.logger.Info(r.RequestURI, log.Any("r", r)) 160 // Call the next handler, which can be another middleware in the chain, or the final handler. 161 next.ServeHTTP(w, r) 162 }) 163 } 164 165 // Stop turns off the HTML Server 166 func (htmlServer *HTMLServer) StopHTTPServer() error { 167 // Create a context to attempt a graceful 5 second shutdown. 168 const timeout = 5 * time.Second 169 ctx, cancel := context.WithTimeout(context.Background(), timeout) 170 defer cancel() 171 172 htmlServer.logger.Info("Stopping HTTP service...") 173 174 // Attempt the graceful shutdown by closing the listener 175 // and completing all inflight requests 176 if err := htmlServer.server.Shutdown(ctx); err != nil { 177 // Looks like we timed out on the graceful shutdown. Force close. 178 if err := htmlServer.server.Close(); err != nil { 179 htmlServer.logger.Error("error stopping HTML service", log.Error(err)) 180 return err 181 } 182 } 183 184 // Wait for the listener to report that it is closed. 185 htmlServer.wg.Wait() 186 htmlServer.logger.Info("HTTP service stopped") 187 return nil 188 } 189 190 type Server struct { 191 db db.DBer 192 logger log.Logger 193 downloader downloader.Downloader 194 } 195 196 func New(db db.DBer, l log.Logger) *Server { 197 return &Server{ 198 db: db, 199 logger: l, 200 // downloader: &downloader.YoutubeDownloader{}, // TODO: get this from config 201 downloader: &downloader.YoutubeDLDownloader{}, // TODO: get this from config 202 } 203 } 204 205 // Render a template, or server error. 206 func (s *Server) render(w http.ResponseWriter, r *http.Request, tpl *template.Template, data interface{}) { 207 w.Header().Set("Content-Type", "text/html; charset=utf-8") 208 buf := new(bytes.Buffer) 209 if err := tpl.Execute(buf, data); err != nil { 210 s.logger.Error("template render error", log.Error(err)) 211 return 212 } 213 w.Write(buf.Bytes()) 214 } 215 216 // Push the given resource to the client. 217 func (s *Server) push(w http.ResponseWriter, resource string) { 218 pusher, ok := w.(http.Pusher) 219 if ok { 220 err := pusher.Push(resource, nil) 221 if err != nil { 222 s.logger.Error("push error", log.Error(err)) 223 } 224 return 225 } 226 } 227 228 func (s *Server) PlayerHandler(w http.ResponseWriter, r *http.Request) { 229 cp := player.GetPlayer() 230 song := player.GetPlaying() 231 232 fullData := map[string]interface{}{ 233 "Player": cp, 234 "Song": song, 235 TemplateTag: s.GetToken(w, r), 236 } 237 files := []string{ 238 "templates/player.html", 239 "templates/layout.html", 240 } 241 tpl := template.Must(template.New("base").ParseFiles(files...)) 242 s.render(w, r, tpl, fullData) 243 } 244 245 func (s *Server) PlaySongHandler(w http.ResponseWriter, r *http.Request) { 246 vars := mux.Vars(r) 247 key := vars["song_id"] 248 249 song, err := s.db.GetSong(key) 250 if err != nil { 251 s.httpError(w, fmt.Errorf("PlaySongHandler|db.View|%w", err), http.StatusInternalServerError) 252 return 253 } 254 player.Beep() 255 err = player.Play(song) 256 if err != nil { 257 // TODO: check if err is user error or system error 258 s.httpError(w, fmt.Errorf("PlaySongHandler|player.Play|%w", err), http.StatusInternalServerError) 259 return 260 } 261 262 http.Redirect(w, r, "/songs", 301) 263 } 264 265 func (s *Server) StopSongHandler(w http.ResponseWriter, r *http.Request) { 266 player.Stop() 267 http.Redirect(w, r, "/songs", 301) 268 }