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  }