package api import ( "encoding/json" "net/http" "strconv" "strings" "sheepflix/internal/library" "sheepflix/internal/probe" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) const authToken = "i-am-pepper" func NewRouter(lib *library.Library) http.Handler { r := chi.NewRouter() r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(authMiddleware) r.Get("/api/health", healthHandler) r.Get("/api/library", listHandler(lib)) r.Get("/api/library/{id}", detailHandler(lib)) r.Get("/api/search", searchHandler(lib)) r.Get("/api/rescan", rescanHandler(lib)) return r } func authMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Accept token from Authorization header or ?token= query param token := r.Header.Get("Authorization") if token == "" { token = r.URL.Query().Get("token") } else { // Strip "Bearer " prefix if present token = strings.TrimPrefix(token, "Bearer ") } if token != authToken { http.Error(w, "unauthorized", http.StatusUnauthorized) return } w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) } func healthHandler(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func listHandler(lib *library.Library) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { sortBy := r.URL.Query().Get("sort") if sortBy == "" { sortBy = "name" } offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) if limit <= 0 || limit > 500 { limit = 50 } items, total := lib.List(sortBy, offset, limit) writeJSON(w, http.StatusOK, map[string]any{ "items": items, "total": total, }) } } func detailHandler(lib *library.Library) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") item, fullPath, ok := lib.Get(id) if !ok { writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) return } probeResult, err := probe.Run(fullPath) if err != nil { writeJSON(w, http.StatusOK, map[string]any{ "item": item, "probe": nil, "probe_error": err.Error(), }) return } writeJSON(w, http.StatusOK, map[string]any{ "item": item, "probe": probeResult, }) } } func searchHandler(lib *library.Library) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query().Get("q") if q == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing q parameter"}) return } results := lib.Search(q) writeJSON(w, http.StatusOK, map[string]any{ "items": results, "total": len(results), }) } } func rescanHandler(lib *library.Library) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { go lib.Scan() writeJSON(w, http.StatusOK, map[string]string{"status": "rescan started"}) } }