mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
7095d9ebec
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.
156 lines
4.6 KiB
Go
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)
|
|
}
|
|
}
|