Files
MeBox/internal/handler/stats_extra.go
T
Kiro Agent 7095d9ebec feat: port remaining Vue UI surfaces with full backend support
Backend additions:
- New GORM models: NotifyChannel, PlayProfile (with PIN hashing).
- NotifyChannelService: multi-channel CRUD + unified dispatcher
  supporting Telegram / Bark / WeChat / Webhook with optional
  per-channel event filtering.
- PlayProfileService: per-user 'viewing personas' with
  content-rating gates, library scoping, PIN protection, and
  player defaults (autoplay, skip-intro, audio/subtitle prefs).
- New endpoints:
    GET    /admin/notify/channels           list
    POST   /admin/notify/channels           create
    PUT    /admin/notify/channels/:id       update
    DELETE /admin/notify/channels/:id       delete
    POST   /admin/notify/channels/:id/test  send test
    GET    /play-profiles                   list (admin: ?all=true)
    POST   /play-profiles                   create
    PUT    /play-profiles/:id               update
    DELETE /play-profiles/:id               delete
    GET    /media/recent                    home page rail
    GET    /media/stats                     library composition
    GET    /watch-history                   list
    GET    /watch-history/stats             aggregate
    GET    /watch-history/continue          continue-watching rail
    DELETE /watch-history[?media_id=]       clear
    DELETE /watch-history/:id               remove one
    GET    /discover/sections               available rails
    GET    /discover/feed?sections=a,b      multi-section TMDb fetch
    GET    /system/info                     name/version/runtime
    GET    /system/status                   uptime/cpu/mem/disk
    GET    /system/scheduler                read-only scheduler view
    GET    /stats/overview                  counts only
    GET    /stats/trend                     daily plays
    GET    /stats/top-content               most played
    GET    /stats/libraries                 per-library size
    GET    /stats/monitor                   live hardware

Frontend additions:
- New API helpers: notify_channels, play_profiles, history,
  stats_extra, media_extra, discover_extra, system.
- ProfileManagementPage (/play-profiles): full CRUD form with
  PIN management, library scoping, language preferences.
- NotifyChannelsPage (/notify-channels, admin): typed config
  forms per channel type + per-channel test action.
- SettingsPage (/settings, admin): grouped key/value editor
  covering General, Organize/Scrape, Adult, qBittorrent.
- Layout sidebar links and App.tsx routes wired for all three.
2026-05-16 09:01:09 +00:00

156 lines
4.6 KiB
Go

// Package handler — richer dashboard statistics endpoints.
//
// /api/stats already returns the basic snapshot. The Vue admin
// dashboard also uses:
//
// /api/stats/overview — counts + total size + total seconds
// /api/stats/trend — daily play count over last N days
// /api/stats/top-content — top played media (by play count)
// /api/stats/libraries — per-library item count + size
// /api/stats/monitor — live CPU/mem/disk
package handler
import (
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MediaStationGo/internal/model"
"github.com/ShukeBta/MediaStationGo/internal/service"
)
func statsOverviewHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
snap, err := svc.Stats.Compute(c.Request.Context(), svc.Cfg.App.DataDir)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"libraries": snap.Libraries,
"media_count": snap.MediaCount,
"users_count": snap.UsersCount,
"total_size": snap.TotalSizeBytes,
"total_seconds": snap.TotalSeconds,
"generated_at": snap.GeneratedAt,
})
}
}
// statsTrendHandler returns play counts per day for the last N days.
func statsTrendHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
days, _ := strconv.Atoi(c.DefaultQuery("days", "14"))
if days <= 0 || days > 90 {
days = 14
}
// Use the playback_history table; one row per (user, media)
// per day if we group by date(watched_at).
type bucket struct {
Day string `json:"day"`
Count int64 `json:"count"`
}
out := make([]bucket, 0, days)
now := time.Now().UTC()
for i := days - 1; i >= 0; i-- {
start := now.AddDate(0, 0, -i).Truncate(24 * time.Hour)
end := start.Add(24 * time.Hour)
var n int64
_ = svc.Repo.DB.Model(&model.PlaybackHistory{}).
Where("watched_at >= ? AND watched_at < ?", start, end).
Count(&n).Error
out = append(out, bucket{
Day: start.Format("2006-01-02"),
Count: n,
})
}
c.JSON(http.StatusOK, gin.H{"trend": out, "days": days})
}
}
// statsTopContentHandler returns the most-watched media items.
func statsTopContentHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
if limit <= 0 || limit > 50 {
limit = 10
}
type row struct {
MediaID string `json:"media_id"`
PlayCount int64 `json:"play_count"`
LastPlayed time.Time `json:"last_played"`
}
var rows []row
_ = svc.Repo.DB.Table("playback_histories").
Select("media_id, COUNT(*) as play_count, MAX(watched_at) as last_played").
Group("media_id").
Order("play_count desc").
Limit(limit).
Scan(&rows).Error
// Hydrate media titles in a single query.
ids := make([]string, 0, len(rows))
for _, r := range rows {
ids = append(ids, r.MediaID)
}
mIdx := map[string]model.Media{}
if len(ids) > 0 {
var media []model.Media
_ = svc.Repo.DB.Where("id IN ?", ids).Find(&media).Error
for _, m := range media {
mIdx[m.ID] = m
}
}
out := make([]gin.H, 0, len(rows))
for _, r := range rows {
out = append(out, gin.H{
"media": mIdx[r.MediaID],
"play_count": r.PlayCount,
"last_played": r.LastPlayed,
})
}
c.JSON(http.StatusOK, gin.H{"items": out})
}
}
// statsLibrariesHandler returns per-library counts + size.
func statsLibrariesHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
var libs []model.Library
if err := svc.Repo.DB.Find(&libs).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
out := make([]gin.H, 0, len(libs))
for _, l := range libs {
var count int64
var size int64
_ = svc.Repo.DB.Model(&model.Media{}).
Where("library_id = ?", l.ID).Count(&count).Error
_ = svc.Repo.DB.Model(&model.Media{}).
Where("library_id = ?", l.ID).
Select("COALESCE(SUM(size_bytes),0)").Row().Scan(&size)
out = append(out, gin.H{
"library": l,
"item_count": count,
"total_size": size,
})
}
c.JSON(http.StatusOK, gin.H{"libraries": out})
}
}
// statsMonitorHandler returns live system resource usage; this is just
// the Hardware portion of the snapshot but with a snappy schema.
func statsMonitorHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
snap, err := svc.Stats.Compute(c.Request.Context(), svc.Cfg.App.DataDir)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, snap.Hardware)
}
}