feat(countries): replace Map view with browse-by-country (like Tags)
Removes the maplibre-gl Map view and adds "Countries" as a sixth TagCategory, reusing the existing /tags/[category]/[[value]] browse machinery instead of a bespoke map UI. Backed by a new self-contained sidecar endpoint that aggregates photos.photo_country with BasePath scoping, mirroring handleLabels/handleScopedCounts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
70
sidecar/handlers_countries.go
Normal file
70
sidecar/handlers_countries.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PpCountry is the country aggregation row returned to the client: a
|
||||
// 2-letter ISO 3166-1 code, the user-scoped photo count, and a representative
|
||||
// thumb hash for the sidebar row.
|
||||
type PpCountry struct {
|
||||
Code string `json:"Code"`
|
||||
PhotoCount int `json:"PhotoCount"`
|
||||
Thumb string `json:"Thumb"`
|
||||
}
|
||||
|
||||
// handleCountries aggregates photos.photo_country directly against
|
||||
// PhotoPrism's DB (no upstream proxy needed — this is a simple GROUP BY)
|
||||
// and scopes the result to the caller's BasePath, mirroring handleLabels.
|
||||
//
|
||||
// Route: GET /api/sidecar/countries (behind requireSession, ppDb != nil)
|
||||
func handleCountries(ppDb *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
basePath := ctxBasePath(c)
|
||||
|
||||
type countryStat struct {
|
||||
Code string `gorm:"column:code"`
|
||||
Cnt int64 `gorm:"column:cnt"`
|
||||
ThumbHash string `gorm:"column:thumb_hash"`
|
||||
}
|
||||
var stats []countryStat
|
||||
|
||||
query := ppDb.Table("photos p").
|
||||
Select(`p.photo_country AS code,
|
||||
COUNT(DISTINCT p.id) AS cnt,
|
||||
COALESCE(MIN(f.file_hash), '') AS thumb_hash`).
|
||||
Joins(`LEFT JOIN files f ON f.photo_uid = p.photo_uid
|
||||
AND f.file_primary = 1
|
||||
AND f.file_missing = 0`).
|
||||
Where("p.deleted_at IS NULL").
|
||||
Where("p.photo_country != '' AND p.photo_country != 'zz'")
|
||||
|
||||
if basePath != "" {
|
||||
prefix := basePath + "/%"
|
||||
query = query.Where("(p.photo_path = ? OR p.photo_path LIKE ?)", basePath, prefix)
|
||||
}
|
||||
|
||||
if err := query.
|
||||
Group("p.photo_country").
|
||||
Having("cnt > 0").
|
||||
Order("cnt DESC").
|
||||
Scan(&stats).Error; err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "country stats query failed"})
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]PpCountry, 0, len(stats))
|
||||
for _, s := range stats {
|
||||
out = append(out, PpCountry{
|
||||
Code: s.Code,
|
||||
PhotoCount: int(s.Cnt),
|
||||
Thumb: s.ThumbHash,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,7 @@ func main() {
|
||||
if ppDb != nil {
|
||||
auth.GET("/labels", handleLabels(pp, ppDb))
|
||||
auth.GET("/counts", handleScopedCounts(ppDb))
|
||||
auth.GET("/countries", handleCountries(ppDb))
|
||||
}
|
||||
|
||||
// User-scoped photos — post-filters by BasePath so review/archive
|
||||
|
||||
Reference in New Issue
Block a user