Files
MeBox/internal/service/stats.go
T
ShukeBta 23d1fbfddb fix cloud library scale and playback compatibility
Root cause: display-only cloud library filtering was reused by scan jobs, merged cloud mounts could be skipped, and OpenList listing relied on WebDAV/first-page behavior that could cap huge directories around 100 items. Cloud scans also let new items consume probe budget before existing rows with missing track/artwork metadata.\n\nChanges:\n- split display filtering from scannable cloud filtering so merged cloud mounts still scan\n- add OpenList API pagination with WebDAV fallback\n- prioritize existing cloud media missing metadata before new imports\n- restrict automatic cloud sync to one successful 19:00-21:00 daily window and keep manual scan immediate\n- disable startup cloud full-scan by default, with an explicit opt-in setting\n- improve Emby/cloud playback compatibility and cache/search/test coverage from the continued work
2026-06-11 18:31:01 +08:00

142 lines
3.9 KiB
Go

// Package service — statistics aggregator.
//
// StatsService computes the dashboard numbers for the admin / home page:
// - total libraries, media items, users
// - total disk size and durations
// - top recently-watched media
// - process metadata (CPU / memory) via gopsutil
package service
import (
"context"
"runtime"
"time"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/mem"
"go.uber.org/zap"
"github.com/ShukeBta/MediaStationGo/internal/model"
"github.com/ShukeBta/MediaStationGo/internal/repository"
)
// StatsService computes aggregate stats.
type StatsService struct {
log *zap.Logger
repo *repository.Container
}
// NewStatsService is the constructor.
func NewStatsService(log *zap.Logger, repo *repository.Container) *StatsService {
return &StatsService{log: log, repo: repo}
}
// Snapshot is the JSON returned by /api/stats.
type Snapshot struct {
Libraries int64 `json:"libraries"`
MediaCount int64 `json:"media_count"`
UsersCount int64 `json:"users_count"`
TotalSizeBytes int64 `json:"total_size_bytes"`
TotalSeconds int64 `json:"total_seconds"`
RecentlyAdded []model.Media `json:"recently_added"`
Hardware Hardware `json:"hardware"`
GeneratedAt time.Time `json:"generated_at"`
}
// Hardware is the live CPU / memory / disk readings.
type Hardware struct {
CPUPercent float64 `json:"cpu_percent"`
MemoryUsed uint64 `json:"memory_used"`
MemoryTotal uint64 `json:"memory_total"`
DiskUsed uint64 `json:"disk_used"`
DiskTotal uint64 `json:"disk_total"`
GoVersion string `json:"go_version"`
Goroutines int `json:"goroutines"`
}
// Compute builds a fresh snapshot.
func (s *StatsService) Compute(ctx context.Context, dataDir string) (*Snapshot, error) {
snap := &Snapshot{GeneratedAt: time.Now()}
libs, err := s.repo.Library.List(ctx)
if err != nil {
return nil, err
}
libs = FilterDisplayCloudLibraries(ctx, s.repo, libs)
activeLibraryIDs := make([]string, 0, len(libs))
for _, lib := range libs {
if !lib.Enabled {
continue
}
activeLibraryIDs = append(activeLibraryIDs, lib.ID)
}
snap.Libraries = int64(len(activeLibraryIDs))
mediaQuery := s.repo.DB.Model(&model.Media{})
if len(activeLibraryIDs) == 0 {
mediaQuery = mediaQuery.Where("1 = 0")
} else {
mediaQuery = mediaQuery.Where("library_id IN ?", activeLibraryIDs)
}
if err := mediaQuery.Count(&snap.MediaCount).Error; err != nil {
return nil, err
}
if err := s.repo.DB.Model(&model.User{}).Count(&snap.UsersCount).Error; err != nil {
return nil, err
}
type sumRow struct {
Size int64
Seconds int64
}
var sum sumRow
sumQuery := s.repo.DB.Model(&model.Media{})
if len(activeLibraryIDs) == 0 {
sumQuery = sumQuery.Where("1 = 0")
} else {
sumQuery = sumQuery.Where("library_id IN ?", activeLibraryIDs)
}
if err := sumQuery.
Select("COALESCE(SUM(size_bytes),0) as size, COALESCE(SUM(duration_sec),0) as seconds").
Scan(&sum).Error; err != nil {
return nil, err
}
snap.TotalSizeBytes = sum.Size
snap.TotalSeconds = sum.Seconds
recentQuery := s.repo.DB.Model(&model.Media{})
if len(activeLibraryIDs) == 0 {
recentQuery = recentQuery.Where("1 = 0")
} else {
recentQuery = recentQuery.Where("library_id IN ?", activeLibraryIDs)
}
if err := recentQuery.
Order("created_at desc").Limit(12).
Find(&snap.RecentlyAdded).Error; err != nil {
return nil, err
}
snap.Hardware = readHardware(dataDir)
return snap, nil
}
func readHardware(dataDir string) Hardware {
hw := Hardware{
GoVersion: runtime.Version(),
Goroutines: runtime.NumGoroutine(),
}
if usage, err := cpu.Percent(0, false); err == nil && len(usage) > 0 {
hw.CPUPercent = usage[0]
}
if v, err := mem.VirtualMemory(); err == nil {
hw.MemoryUsed = v.Used
hw.MemoryTotal = v.Total
}
if dataDir == "" {
dataDir = "/"
}
if d, err := disk.Usage(dataDir); err == nil {
hw.DiskUsed = d.Used
hw.DiskTotal = d.Total
}
return hw
}