package media import ( "net/http" "strconv" "github.com/mithral/vellum/internal/auth" "github.com/mithral/vellum/internal/httpx" ) // itemRow is the browse-list shape. type itemRow struct { ID int64 `json:"id"` LibraryID int64 `json:"library_id"` Kind string `json:"kind"` Path string `json:"path"` Title string `json:"title"` Year int `json:"year"` } // handleListItems lists items across every library the caller may read, // optionally filtered by media kind or a single library. // // This is what powers "Browse > Ebooks" — one request, scoped by the same // access-control chokepoint as everything else. func (h *Handlers) handleListItems(w http.ResponseWriter, r *http.Request, u auth.User) { ctx := r.Context() libs, err := auth.VisibleLibraryIDs(ctx, h.db, u) if err != nil { h.fail(w, "visible libraries", err) return } limit := 300 if v := r.URL.Query().Get("limit"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 1000 { limit = n } } offset := 0 if v := r.URL.Query().Get("offset"); v != "" { if n, err := strconv.Atoi(v); err == nil && n >= 0 { offset = n } } if len(libs) == 0 { httpx.WriteJSON(w, http.StatusOK, map[string]any{ "items": []itemRow{}, "total": 0, "offset": offset, "limit": limit, }) return } in, args := inClause(libs) where := `missing = 0 AND library_id IN (` + in + `)` if kind := r.URL.Query().Get("kind"); kind != "" { where += ` AND kind = ?` args = append(args, kind) } if libStr := r.URL.Query().Get("library_id"); libStr != "" { libID, err := strconv.ParseInt(libStr, 10, 64) if err != nil || libID <= 0 { httpx.WriteError(w, http.StatusBadRequest, "invalid library_id") return } where += ` AND library_id = ?` args = append(args, libID) } // total counts every matching row, independent of the page window. var total int if err := h.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM media_items WHERE `+where, args...).Scan(&total); err != nil { h.fail(w, "count items", err) return } query := `SELECT id, library_id, kind, path, title, COALESCE(year, 0) FROM media_items WHERE ` + where + ` ORDER BY sort_title LIMIT ? OFFSET ?` pageArgs := append(append([]any{}, args...), limit, offset) rows, err := h.db.QueryContext(ctx, query, pageArgs...) if err != nil { h.fail(w, "list items", err) return } defer rows.Close() items := []itemRow{} for rows.Next() { var it itemRow if err := rows.Scan(&it.ID, &it.LibraryID, &it.Kind, &it.Path, &it.Title, &it.Year); err != nil { h.fail(w, "scan item", err) return } items = append(items, it) } if err := rows.Err(); err != nil { h.fail(w, "iterate items", err) return } httpx.WriteJSON(w, http.StatusOK, map[string]any{ "items": items, "total": total, "offset": offset, "limit": limit, }) }