- New GET /api/sidecar/labels — proxies PP's labels, recalculates
PhotoCount per user's BasePath via DB query
- New GET /api/sidecar/counts — returns user-scoped sidebar badges
(all, review, archived, private, photos, videos, favorites)
- Fixed auth middleware to expose userUID and basePath on context
- Fixed ppClient.resolveSession — uses correct endpoint
(GET /api/v1/session, not /api/v1/session/{token}) and correct
JSON field names (UID, Name instead of UserUID, UserName)
- Frontend: listLabels now calls /api/sidecar/labels instead of /api/v1/labels
148 lines
4.9 KiB
Go
148 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// PpLabel mirrors the shape PhotoPrism's /api/v1/labels endpoint returns.
|
|
// We decode enough to filter + recalculate PhotoCount; fields the client
|
|
// doesn't render are skipped for token efficiency.
|
|
type PpLabel struct {
|
|
UID string `json:"UID"`
|
|
Name string `json:"Name"`
|
|
Slug string `json:"Slug"`
|
|
CustomSlug string `json:"CustomSlug"`
|
|
Priority int `json:"Priority"`
|
|
Favorite bool `json:"Favorite"`
|
|
PhotoCount int `json:"PhotoCount"`
|
|
Thumb string `json:"Thumb"`
|
|
CreatedAt string `json:"CreatedAt"`
|
|
UpdatedAt string `json:"UpdatedAt"`
|
|
}
|
|
|
|
// handleLabels proxies PhotoPrism's /api/v1/labels and then post-filters
|
|
// each label's PhotoCount (and removes labels with zero count) so they
|
|
// reflect only photos under the caller's BasePath.
|
|
//
|
|
// Route: GET /api/sidecar/labels (behind requireSession)
|
|
func handleLabels(pp *ppClient, ppDb *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
token := ctxToken(c)
|
|
basePath := ctxBasePath(c)
|
|
|
|
// Forward the query string (count, offset, q, all, …) to PhotoPrism.
|
|
query := c.Request.URL.RawQuery
|
|
|
|
// Call PhotoPrism's labels endpoint using the caller's token.
|
|
resp, err := pp.call(c.Request.Context(), http.MethodGet, "/api/v1/labels?"+query, token, nil)
|
|
if err != nil || !resp.OK {
|
|
c.JSON(http.StatusBadGateway, gin.H{"error": "upstream labels request failed"})
|
|
return
|
|
}
|
|
|
|
// Decode labels.
|
|
var labels []PpLabel
|
|
if err := json.Unmarshal(resp.Body, &labels); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to parse labels"})
|
|
return
|
|
}
|
|
|
|
// If the user has no BasePath (admin/empty), return labels as-is.
|
|
if basePath == "" || ppDb == nil {
|
|
c.JSON(http.StatusOK, labels)
|
|
return
|
|
}
|
|
|
|
// For each label, count how many photos under this user's BasePath
|
|
// have that label. Remove labels with zero count for this user.
|
|
filtered := make([]PpLabel, 0, len(labels))
|
|
prefix := basePath + "/%"
|
|
|
|
for _, l := range labels {
|
|
var cnt int64
|
|
if err := ppDb.Raw(
|
|
`SELECT COUNT(*) FROM photos_labels pl
|
|
JOIN photos p ON pl.photo_id = p.id
|
|
JOIN labels lb ON pl.label_id = lb.id
|
|
WHERE lb.label_uid = ?
|
|
AND (p.photo_path = ? OR p.photo_path LIKE ?)
|
|
AND p.deleted_at IS NULL`,
|
|
l.UID, basePath, prefix,
|
|
).Count(&cnt).Error; err != nil {
|
|
// On DB error, skip this label rather than failing the whole response.
|
|
continue
|
|
}
|
|
if cnt == 0 {
|
|
continue
|
|
}
|
|
l.PhotoCount = int(cnt)
|
|
filtered = append(filtered, l)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, filtered)
|
|
}
|
|
}
|
|
|
|
// Now also handle the session/config count scoping.
|
|
|
|
// PpCounts mirrors PhotoPrism's session config.count block that drives
|
|
// the sidebar badges (review, archive, all, etc.).
|
|
type PpCounts struct {
|
|
All int `json:"all"`
|
|
Photos int `json:"photos"`
|
|
Media int `json:"media"`
|
|
Videos int `json:"videos"`
|
|
Review int `json:"review"`
|
|
Archived int `json:"archived"`
|
|
Hidden int `json:"hidden"`
|
|
Private int `json:"private"`
|
|
Favorites int `json:"favorites"`
|
|
}
|
|
|
|
// handleScopedCounts returns user-scoped counts for review/archive/all
|
|
// so the sidebar badges match what the user actually sees.
|
|
//
|
|
// Route: GET /api/sidecar/counts (behind requireSession)
|
|
func handleScopedCounts(ppDb *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
basePath := ctxBasePath(c)
|
|
if basePath == "" || ppDb == nil {
|
|
// Admin or no DB — can't scope, return empty.
|
|
c.JSON(http.StatusOK, PpCounts{})
|
|
return
|
|
}
|
|
|
|
prefix := basePath + "/%"
|
|
pathCond := "(p.photo_path = ? OR p.photo_path LIKE ?)"
|
|
args := []any{basePath, prefix}
|
|
|
|
var counts PpCounts
|
|
|
|
// All non-deleted photos in this user's scope.
|
|
ppDb.Raw(`SELECT COUNT(*) FROM photos p WHERE p.deleted_at IS NULL AND `+pathCond, args...).Scan(&counts.All)
|
|
|
|
// Photos needing review (quality < 3).
|
|
ppDb.Raw(`SELECT COUNT(*) FROM photos p WHERE p.deleted_at IS NULL AND p.photo_quality < 3 AND `+pathCond, args...).Scan(&counts.Review)
|
|
|
|
// Archived (soft-deleted) photos.
|
|
ppDb.Raw(`SELECT COUNT(*) FROM photos p WHERE p.deleted_at IS NOT NULL AND `+pathCond, args...).Scan(&counts.Archived)
|
|
|
|
// Private photos.
|
|
ppDb.Raw(`SELECT COUNT(*) FROM photos p WHERE p.deleted_at IS NULL AND p.photo_private = 1 AND `+pathCond, args...).Scan(&counts.Private)
|
|
|
|
// Photos (type image).
|
|
ppDb.Raw(`SELECT COUNT(*) FROM photos p WHERE p.deleted_at IS NULL AND p.photo_type IN ('image','raw','live','animated') AND `+pathCond, args...).Scan(&counts.Photos)
|
|
|
|
// Videos.
|
|
ppDb.Raw(`SELECT COUNT(*) FROM photos p WHERE p.deleted_at IS NULL AND p.photo_type IN ('video','hdr','burst','live') AND `+pathCond, args...).Scan(&counts.Videos)
|
|
|
|
// Favorites.
|
|
ppDb.Raw(`SELECT COUNT(*) FROM photos p WHERE p.deleted_at IS NULL AND p.photo_favorite = 1 AND `+pathCond, args...).Scan(&counts.Favorites)
|
|
|
|
c.JSON(http.StatusOK, counts)
|
|
}
|
|
} |