refactor: use errors.Is()/errors.AsType() for error comparison and rename shadowed variables

Replace direct error equality checks (err == pgx.ErrNoRows, err != http.ErrServerClosed)
with the idiomatic errors.Is() function throughout handlers, services, middleware, and app
startup. This correctly handles wrapped error chains.

Also replace a raw type assertion (*HTTPError) with errors.AsType[*HTTPError]() in the
error handler middleware for consistency.

Additionally, rename shadowed variables for clarity:
- sidecar.go: config -> sidecarConfig, systemConfig (shadowed package-level vars)
- media_scanner.go: uuid -> uuidString (shadowed the uuid package import)
This commit is contained in:
2026-04-20 21:20:22 -04:00
parent d8d6334052
commit 9ccff320a1
15 changed files with 59 additions and 45 deletions
+2 -1
View File
@@ -2,6 +2,7 @@ package app
import ( import (
"context" "context"
"errors"
"log" "log"
"net/http" "net/http"
"os" "os"
@@ -40,7 +41,7 @@ func (a *App) StartServer(addr string) error {
// Start HTTP server in background // Start HTTP server in background
go func() { go func() {
if err := a.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := a.server.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) {
log.Fatalf("Server failed to start: %v", err) log.Fatalf("Server failed to start: %v", err)
} }
}() }()
+4 -3
View File
@@ -3,6 +3,7 @@ package handlers
import ( import (
"bookhoard/internal/database" "bookhoard/internal/database"
"context" "context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"time" "time"
@@ -98,7 +99,7 @@ func (h *AnalyticsHandler) GetReadingStats(c *echo.Context) error {
CreatedAt_2: pgtype.Timestamptz{Time: endTime, Valid: true}, CreatedAt_2: pgtype.Timestamptz{Time: endTime, Valid: true},
}) })
if err != nil && err != pgx.ErrNoRows { if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get reading history") return echo.NewHTTPError(http.StatusInternalServerError, "failed to get reading history")
} }
@@ -211,7 +212,7 @@ func (h *AnalyticsHandler) GetDeviceUsage(c *echo.Context) error {
ctx := context.Background() ctx := context.Background()
usage, err := h.db.GetUserDeviceUsage(ctx, user.ID) usage, err := h.db.GetUserDeviceUsage(ctx, user.ID)
if err != nil && err != pgx.ErrNoRows { if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get device usage") return echo.NewHTTPError(http.StatusInternalServerError, "failed to get device usage")
} }
@@ -263,7 +264,7 @@ func (h *AnalyticsHandler) GetPopularBooks(c *echo.Context) error {
Limit: limitInt, Limit: limitInt,
}) })
if err != nil && err != pgx.ErrNoRows { if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get popular books") return echo.NewHTTPError(http.StatusInternalServerError, "failed to get popular books")
} }
+3 -2
View File
@@ -7,6 +7,7 @@ import (
"bookhoard/internal/database" "bookhoard/internal/database"
"bookhoard/internal/middleware" "bookhoard/internal/middleware"
"context" "context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
@@ -887,7 +888,7 @@ func (h *AuthHandler) DeleteUser(c *echo.Context) error {
// Delete user (this will cascade to delete all related data) // Delete user (this will cascade to delete all related data)
err = h.db.DeleteUser(c.Request().Context(), targetUserUUID) err = h.db.DeleteUser(c.Request().Context(), targetUserUUID)
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
if c.Request().Header.Get("HX-Request") == "true" { if c.Request().Header.Get("HX-Request") == "true" {
return c.HTML(http.StatusNotFound, `<div class="text-red-500">User not found</div>`) return c.HTML(http.StatusNotFound, `<div class="text-red-500">User not found</div>`)
} }
@@ -940,7 +941,7 @@ func (h *AuthHandler) UpdateUserMaxDevices(c *echo.Context) error {
MaxDevices: pgtype.Int4{Int32: req.MaxDevices, Valid: true}, MaxDevices: pgtype.Int4{Int32: req.MaxDevices, Valid: true},
}) })
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"}) return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"})
} }
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
+2 -1
View File
@@ -6,6 +6,7 @@ import (
wsync "bookhoard/internal/sync" wsync "bookhoard/internal/sync"
"bookhoard/internal/utils" "bookhoard/internal/utils"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
@@ -891,7 +892,7 @@ func (h *CollectionHandler) PreviewCollection(c *echo.Context) error {
_, err = h.db.GetLibrary(c.Request().Context(), pgtype.UUID{Bytes: libUUID, Valid: true}) _, err = h.db.GetLibrary(c.Request().Context(), pgtype.UUID{Bytes: libUUID, Valid: true})
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "library not found"}) return c.JSON(http.StatusBadRequest, map[string]string{"error": "library not found"})
} }
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
+7 -6
View File
@@ -5,6 +5,7 @@ import (
wsync "bookhoard/internal/sync" wsync "bookhoard/internal/sync"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"time" "time"
@@ -83,7 +84,7 @@ func (h *ConflictHandler) GetConflictsData(c *echo.Context) ([]ConflictDetailRes
conflicts, err = h.db.ListSyncConflictsByUser(ctx, user.ID) conflicts, err = h.db.ListSyncConflictsByUser(ctx, user.ID)
} }
if err != nil && err != pgx.ErrNoRows { if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return nil, 0, 0, err return nil, 0, 0, err
} }
@@ -150,7 +151,7 @@ func (h *ConflictHandler) GetConflict(c *echo.Context) error {
conflictUUID := pgtype.UUID{Bytes: [16]byte(conflictID), Valid: true} conflictUUID := pgtype.UUID{Bytes: [16]byte(conflictID), Valid: true}
conflict, err := h.db.GetSyncConflict(context.Background(), conflictUUID) conflict, err := h.db.GetSyncConflict(context.Background(), conflictUUID)
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return echo.NewHTTPError(http.StatusNotFound, "conflict not found") return echo.NewHTTPError(http.StatusNotFound, "conflict not found")
} }
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get conflict") return echo.NewHTTPError(http.StatusInternalServerError, "failed to get conflict")
@@ -214,7 +215,7 @@ func (h *ConflictHandler) ResolveConflict(c *echo.Context) error {
conflictUUID := pgtype.UUID{Bytes: [16]byte(conflictID), Valid: true} conflictUUID := pgtype.UUID{Bytes: [16]byte(conflictID), Valid: true}
conflict, err := h.db.GetSyncConflict(context.Background(), conflictUUID) conflict, err := h.db.GetSyncConflict(context.Background(), conflictUUID)
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return echo.NewHTTPError(http.StatusNotFound, "conflict not found") return echo.NewHTTPError(http.StatusNotFound, "conflict not found")
} }
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get conflict") return echo.NewHTTPError(http.StatusInternalServerError, "failed to get conflict")
@@ -292,7 +293,7 @@ func (h *ConflictHandler) applyProgressResolution(mediaItemID pgtype.UUID, userI
MediaItemID: mediaItemID, MediaItemID: mediaItemID,
UserID: userID, UserID: userID,
}) })
if err != nil && err != pgx.ErrNoRows { if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return err return err
} }
@@ -380,7 +381,7 @@ func (h *ConflictHandler) DeleteConflict(c *echo.Context) error {
conflictUUID := pgtype.UUID{Bytes: [16]byte(conflictID), Valid: true} conflictUUID := pgtype.UUID{Bytes: [16]byte(conflictID), Valid: true}
conflict, err := h.db.GetSyncConflict(context.Background(), conflictUUID) conflict, err := h.db.GetSyncConflict(context.Background(), conflictUUID)
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return echo.NewHTTPError(http.StatusNotFound, "conflict not found") return echo.NewHTTPError(http.StatusNotFound, "conflict not found")
} }
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get conflict") return echo.NewHTTPError(http.StatusInternalServerError, "failed to get conflict")
@@ -641,7 +642,7 @@ func (h *ConflictHandler) applyResolution(mediaItemID pgtype.UUID, userID pgtype
MediaItemID: mediaItemID, MediaItemID: mediaItemID,
UserID: userID, UserID: userID,
}) })
if err != nil && err != pgx.ErrNoRows { if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return err return err
} }
+7 -6
View File
@@ -6,6 +6,7 @@ import (
"bookhoard/internal/utils" "bookhoard/internal/utils"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
@@ -728,7 +729,7 @@ func (mh *MediaHandler) GetMediaItem(c *echo.Context) error {
item, err := mh.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true}) item, err := mh.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true})
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "media item not found"}) return c.JSON(http.StatusNotFound, map[string]string{"error": "media item not found"})
} }
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -836,7 +837,7 @@ func (mh *MediaHandler) GetMediaRating(c *echo.Context) error {
UserID: pgtype.UUID{Bytes: userUUID, Valid: true}, UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
}) })
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusOK, map[string]interface{}{"rating": nil}) return c.JSON(http.StatusOK, map[string]interface{}{"rating": nil})
} }
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -894,7 +895,7 @@ func (mh *MediaHandler) GetMediaReadingProgress(c *echo.Context) error {
UserID: pgtype.UUID{Bytes: userUUID, Valid: true}, UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
}) })
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusOK, map[string]interface{}{ return c.JSON(http.StatusOK, map[string]interface{}{
"current_page": 0, "current_page": 0,
"total_pages": nil, "total_pages": nil,
@@ -1026,7 +1027,7 @@ func (mh *MediaHandler) CreateMediaItem(c *echo.Context) error {
_, err = mh.db.GetLibrary(c.Request().Context(), pgtype.UUID{Bytes: req.LibraryID, Valid: true}) _, err = mh.db.GetLibrary(c.Request().Context(), pgtype.UUID{Bytes: req.LibraryID, Valid: true})
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "library not found"}) return c.JSON(http.StatusBadRequest, map[string]string{"error": "library not found"})
} }
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -1237,7 +1238,7 @@ func (mh *MediaHandler) GetMediaNote(c *echo.Context) error {
note, err := mh.db.GetMediaNote(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true}) note, err := mh.db.GetMediaNote(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true})
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "note not found"}) return c.JSON(http.StatusNotFound, map[string]string{"error": "note not found"})
} }
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -1377,7 +1378,7 @@ func (mh *MediaHandler) GetMediaHighlight(c *echo.Context) error {
highlight, err := mh.db.GetMediaHighlight(c.Request().Context(), pgtype.UUID{Bytes: highlightUUID, Valid: true}) highlight, err := mh.db.GetMediaHighlight(c.Request().Context(), pgtype.UUID{Bytes: highlightUUID, Valid: true})
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "highlight not found"}) return c.JSON(http.StatusNotFound, map[string]string{"error": "highlight not found"})
} }
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
+2 -1
View File
@@ -5,6 +5,7 @@ import (
wsync "bookhoard/internal/sync" wsync "bookhoard/internal/sync"
"bookhoard/internal/utils" "bookhoard/internal/utils"
"context" "context"
"errors"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
@@ -43,7 +44,7 @@ func (h *Handler) GetUniversalProgress(c *echo.Context) error {
UserID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true}, UserID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true},
}) })
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusOK, map[string]interface{}{ return c.JSON(http.StatusOK, map[string]interface{}{
"media_item_id": mediaItemID, "media_item_id": mediaItemID,
"progress": nil, "progress": nil,
+5 -4
View File
@@ -4,6 +4,7 @@ import (
"bookhoard/internal/database" "bookhoard/internal/database"
"bookhoard/internal/services" "bookhoard/internal/services"
"context" "context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
@@ -63,7 +64,7 @@ func (h *ReaderHandler) ShowReader(c *echo.Context) error {
// Get media item // Get media item
mediaItem, err := h.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: parsedUUID, Valid: true}) mediaItem, err := h.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: parsedUUID, Valid: true})
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "Media item not found"}) return c.JSON(http.StatusNotFound, map[string]string{"error": "Media item not found"})
} }
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch media item"}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch media item"})
@@ -108,7 +109,7 @@ func (h *ReaderHandler) ShowReader(c *echo.Context) error {
MediaItemID: pgtype.UUID{Bytes: parsedUUID, Valid: true}, MediaItemID: pgtype.UUID{Bytes: parsedUUID, Valid: true},
UserID: userData.ID, UserID: userData.ID,
}) })
if err != nil && err != pgx.ErrNoRows { if err != nil && !errors.Is(err, pgx.ErrNoRows) {
progress = database.ReadingProgress{} progress = database.ReadingProgress{}
} }
@@ -316,7 +317,7 @@ func (h *ReaderHandler) GetReadingSpeed(c *echo.Context) error {
}) })
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
// Return zero values if no reading has occurred // Return zero values if no reading has occurred
return c.JSON(http.StatusOK, map[string]interface{}{ return c.JSON(http.StatusOK, map[string]interface{}{
"words_per_minute": 0, "words_per_minute": 0,
@@ -597,7 +598,7 @@ func (h *ReaderHandler) ParseEbook(c *echo.Context) error {
// Fetch media item // Fetch media item
mediaItem, err := h.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: parsedUUID, Valid: true}) mediaItem, err := h.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: parsedUUID, Valid: true})
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(404, map[string]string{"error": "Media item not found"}) return c.JSON(404, map[string]string{"error": "Media item not found"})
} }
return c.JSON(500, map[string]string{"error": "Failed to fetch media item"}) return c.JSON(500, map[string]string{"error": "Failed to fetch media item"})
+3 -2
View File
@@ -3,6 +3,7 @@ package handlers
import ( import (
"bookhoard/internal/database" "bookhoard/internal/database"
"context" "context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"time" "time"
@@ -52,7 +53,7 @@ func (h *AuthHandler) RefreshAccessToken(c *echo.Context) error {
tokenInfo, err := h.db.GetRefreshToken(c.Request().Context(), tokenUUID) tokenInfo, err := h.db.GetRefreshToken(c.Request().Context(), tokenUUID)
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid or expired refresh token"}) return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid or expired refresh token"})
} }
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to validate refresh token"}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to validate refresh token"})
@@ -88,7 +89,7 @@ func (h *AuthHandler) Logout(c *echo.Context) error {
} }
err = h.db.RevokeRefreshToken(c.Request().Context(), tokenUUID) err = h.db.RevokeRefreshToken(c.Request().Context(), tokenUUID)
if err != nil && err != pgx.ErrNoRows { if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to revoke refresh token"}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to revoke refresh token"})
} }
+4 -4
View File
@@ -171,7 +171,7 @@ func (h *SidecarHandler) GetSidecarConfig(c *echo.Context) error {
} }
// Build sidecar config // Build sidecar config
config := SidecarConfig{ sidecarConfig := SidecarConfig{
Version: "1.0", Version: "1.0",
Bookhoard: SidecarBookhoardConfig{ Bookhoard: SidecarBookhoardConfig{
OPDSCatalog: opdsCatalogURL, OPDSCatalog: opdsCatalogURL,
@@ -188,7 +188,7 @@ func (h *SidecarHandler) GetSidecarConfig(c *echo.Context) error {
LastUpdated: time.Now().Format(time.RFC3339), LastUpdated: time.Now().Format(time.RFC3339),
} }
return c.JSON(http.StatusOK, config) return c.JSON(http.StatusOK, sidecarConfig)
} }
// DownloadSidecarConfig generates a .bookhoard.json file for device setup // DownloadSidecarConfig generates a .bookhoard.json file for device setup
@@ -352,8 +352,8 @@ func (h *SidecarHandler) GetSystemConfiguration(c *echo.Context) error {
// Build config map // Build config map
result := make(map[string]string) result := make(map[string]string)
for _, config := range configs { for _, systemConfig := range configs {
result[config.Key] = config.Value result[systemConfig.Key] = systemConfig.Value
} }
return c.JSON(http.StatusOK, result) return c.JSON(http.StatusOK, result)
+5 -4
View File
@@ -2,6 +2,7 @@ package handlers
import ( import (
"bookhoard/internal/database" "bookhoard/internal/database"
"errors"
"net/http" "net/http"
"strconv" "strconv"
@@ -47,7 +48,7 @@ func (h *SystemSettingsHandler) UpdateScanSettings(c *echo.Context) error {
SettingValue: scanFrequencyValue, SettingValue: scanFrequencyValue,
}) })
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "system setting not found"}) return c.JSON(http.StatusNotFound, map[string]string{"error": "system setting not found"})
} }
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -58,7 +59,7 @@ func (h *SystemSettingsHandler) UpdateScanSettings(c *echo.Context) error {
SettingValue: autoScanValue, SettingValue: autoScanValue,
}) })
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "system setting not found"}) return c.JSON(http.StatusNotFound, map[string]string{"error": "system setting not found"})
} }
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -74,7 +75,7 @@ func (h *SystemSettingsHandler) UpdateScanSettings(c *echo.Context) error {
func (h *SystemSettingsHandler) GetScanSettings(c *echo.Context) error { func (h *SystemSettingsHandler) GetScanSettings(c *echo.Context) error {
scanFrequencySetting, err := h.db.GetSystemSetting(c.Request().Context(), "scan_poll_interval_seconds") scanFrequencySetting, err := h.db.GetSystemSetting(c.Request().Context(), "scan_poll_interval_seconds")
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusOK, ScanSettingsResponse{ return c.JSON(http.StatusOK, ScanSettingsResponse{
ScanPollIntervalSeconds: 60, ScanPollIntervalSeconds: 60,
AutoScanEnabled: true, AutoScanEnabled: true,
@@ -85,7 +86,7 @@ func (h *SystemSettingsHandler) GetScanSettings(c *echo.Context) error {
autoScanSetting, err := h.db.GetSystemSetting(c.Request().Context(), "auto_scan_enabled") autoScanSetting, err := h.db.GetSystemSetting(c.Request().Context(), "auto_scan_enabled")
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusOK, ScanSettingsResponse{ return c.JSON(http.StatusOK, ScanSettingsResponse{
ScanPollIntervalSeconds: 60, ScanPollIntervalSeconds: 60,
AutoScanEnabled: true, AutoScanEnabled: true,
+2 -1
View File
@@ -1,6 +1,7 @@
package middleware package middleware
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
@@ -83,7 +84,7 @@ func WrapHandler(fn func(*echo.Context) error) echo.HandlerFunc {
return func(c *echo.Context) error { return func(c *echo.Context) error {
err := fn(c) err := fn(c)
if err != nil { if err != nil {
if httpErr, ok := err.(*HTTPError); ok { if httpErr, ok := errors.AsType[*HTTPError](err); ok {
return RespondWithHTTPError(c, httpErr) return RespondWithHTTPError(c, httpErr)
} }
return RespondWithError(c, http.StatusInternalServerError, "Internal server error", err) return RespondWithError(c, http.StatusInternalServerError, "Internal server error", err)
+2 -1
View File
@@ -6,6 +6,7 @@ import (
"bookhoard/internal/services" "bookhoard/internal/services"
"bookhoard/templates" "bookhoard/templates"
"bytes" "bytes"
"errors"
"fmt" "fmt"
"net/http" "net/http"
@@ -73,7 +74,7 @@ func registerReaderRoutes(cfg *Config) {
MediaItemID: pgtype.UUID{Bytes: parsedUUID, Valid: true}, MediaItemID: pgtype.UUID{Bytes: parsedUUID, Valid: true},
UserID: uuidToPGType(userUUID), UserID: uuidToPGType(userUUID),
}) })
if err != nil && err != pgx.ErrNoRows { if err != nil && !errors.Is(err, pgx.ErrNoRows) {
progress = database.ReadingProgress{} progress = database.ReadingProgress{}
} }
// Get bookmarks // Get bookmarks
+9 -8
View File
@@ -15,6 +15,7 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"errors"
"fmt" "fmt"
"image" "image"
_ "image/jpeg" _ "image/jpeg"
@@ -590,7 +591,7 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool,
fmt.Printf("Media item already exists with same size, skipping: %s\n", path) fmt.Printf("Media item already exists with same size, skipping: %s\n", path)
return false, nil return false, nil
} }
} else if err != pgx.ErrNoRows { } else if !errors.Is(err, pgx.ErrNoRows) {
fmt.Printf("Database error checking media item existence: %v\n", err) fmt.Printf("Database error checking media item existence: %v\n", err)
return false, fmt.Errorf("failed to check if media item exists: %v", err) return false, fmt.Errorf("failed to check if media item exists: %v", err)
} }
@@ -2605,7 +2606,7 @@ func (s *MediaScanner) scanDirectory(ctx context.Context, dirPath string) {
LibraryID: libraryID, LibraryID: libraryID,
}) })
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
if _, err := s.processMediaFile(ctx, path); err != nil { if _, err := s.processMediaFile(ctx, path); err != nil {
s.errors++ s.errors++
} else { } else {
@@ -2780,7 +2781,7 @@ func (s *MediaScanner) SyncFilesystemWithDatabase(ctx context.Context) error {
FilePath: relPath, FilePath: relPath,
LibraryID: libraryID, LibraryID: libraryID,
}) })
if err == pgx.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
// New file found - scan it // New file found - scan it
absPath := folder + "/" + relPath absPath := folder + "/" + relPath
if _, err := os.Stat(absPath); err == nil { if _, err := os.Stat(absPath); err == nil {
@@ -2848,19 +2849,19 @@ func (s *MediaScanner) extractOPFIdentifiers(epubPath string) (opfIdentifier, op
return "", "", "low", nil return "", "", "low", nil
} }
var identifier, uuid string var identifier, uuidString string
for _, id := range identifiers { for _, id := range identifiers {
id = strings.TrimSpace(id) id = strings.TrimSpace(id)
// Check for UUID format (urn:uuid:) // Check for UUID format (urn:uuid:)
if trimmed, found := strings.CutPrefix(strings.ToLower(id), "urn:uuid:"); found { if trimmed, found := strings.CutPrefix(strings.ToLower(id), "urn:uuid:"); found {
uuid = trimmed uuidString = trimmed
continue continue
} }
// Check if it's a plain UUID (8-4-4-4-12 format) // Check if it's a plain UUID (8-4-4-4-12 format)
if isValidUUID(id) { if isValidUUID(id) {
uuid = id uuidString = id
continue continue
} }
@@ -2879,9 +2880,9 @@ func (s *MediaScanner) extractOPFIdentifiers(epubPath string) (opfIdentifier, op
} }
} }
confidence = s.determineHashConfidence(uuid, identifier) confidence = s.determineHashConfidence(uuidString, identifier)
return identifier, uuid, confidence, nil return identifier, uuidString, confidence, nil
} }
// isValidUUID checks if string is a valid UUID (8-4-4-4-12 format) // isValidUUID checks if string is a valid UUID (8-4-4-4-12 format)
+2 -1
View File
@@ -3,6 +3,7 @@ package services
import ( import (
"bookhoard/internal/database" "bookhoard/internal/database"
"context" "context"
"errors"
"strings" "strings"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
@@ -214,7 +215,7 @@ func (s *SearchService) SearchFieldValues(ctx context.Context, params FieldSearc
// Shared between JSON endpoint and HTML rendering // Shared between JSON endpoint and HTML rendering
func (s *SearchService) ExecuteSearch(ctx context.Context, params SearchParams) ([]database.SearchMediaItemsUnifiedRow, int, error) { func (s *SearchService) ExecuteSearch(ctx context.Context, params SearchParams) ([]database.SearchMediaItemsUnifiedRow, int, error) {
results, err := s.SearchMediaItemsUnified(ctx, params) results, err := s.SearchMediaItemsUnified(ctx, params)
if err != nil && err != pgx.ErrNoRows { if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return nil, 0, err return nil, 0, err
} }