// backend\tray_stats.go package main import ( "fmt" "strconv" "strings" "time" godisk "github.com/shirou/gopsutil/v3/disk" ) type TrayStats struct { ActiveDownloads int DownloadLimitEnabled bool DownloadLimitMax int DownloadLimitReached bool PostworkWaiting int PostworkRunning int EnrichWaiting int EnrichRunning int AutostartPaused bool AutostartPausedByUser bool AutostartPausedByDisk bool DiskFreeBytes uint64 HasDiskInfo bool DiskEmergency bool Uptime time.Duration } func buildTrayStats() TrayStats { active := activeRecordingJobsCount() limitEnabled, limitMax, _, limitReached := concurrentDownloadsLimitState() if limitMax < 1 { limitMax = 1 } pwWaiting, pwRunning, _ := postWorkQ.Stats() enWaiting, enRunning, _ := enrichQ.Stats() auto := getAutostartStatePayload() diskFree, hasDisk := getTrayDiskFreeBytes() return TrayStats{ ActiveDownloads: active, DownloadLimitEnabled: limitEnabled, DownloadLimitMax: limitMax, DownloadLimitReached: limitReached, PostworkWaiting: pwWaiting, PostworkRunning: pwRunning, EnrichWaiting: enWaiting, EnrichRunning: enRunning, AutostartPaused: auto.Paused, AutostartPausedByUser: auto.PausedByUser, AutostartPausedByDisk: auto.PausedByDisk, DiskFreeBytes: diskFree, HasDiskInfo: hasDisk, DiskEmergency: diskEmergencyActive(), Uptime: time.Since(serverStartedAt).Round(time.Second), } } func getTrayDiskFreeBytes() (uint64, bool) { s := getSettings() recordDir, err := resolvePathRelativeToApp(s.RecordDir) if err != nil || strings.TrimSpace(recordDir) == "" { recordDir = strings.TrimSpace(s.RecordDir) } if strings.TrimSpace(recordDir) == "" { return 0, false } u, err := godisk.Usage(recordDir) if err != nil || u == nil { return 0, false } return u.Free, true } func diskEmergencyActive() bool { return isAutostartPaused() && getAutostartStatePayload().PausedByDisk } func (s TrayStats) MenuTitle() string { return fmt.Sprintf( "Aktiv: %d • PW: %d/%d • Enrich: %d/%d", s.ActiveDownloads, s.PostworkRunning, s.PostworkWaiting, s.EnrichRunning, s.EnrichWaiting, ) } func (s TrayStats) Tooltip() string { return fmt.Sprintf( "Recorder | Downloads %s | PW %d/%d | Enrich %d/%d", s.downloadLimitText(), s.PostworkRunning, s.PostworkWaiting, s.EnrichRunning, s.EnrichWaiting, ) } func (s TrayStats) AutostartLine() string { switch { case s.AutostartPausedByDisk: return "Autostart: Pausiert (Disk-Notbremse)" case s.AutostartPausedByUser: return "Autostart: Pausiert" default: return "Autostart: Aktiv" } } func (s TrayStats) DownloadsLine() string { if s.DownloadLimitEnabled { if s.DownloadLimitReached { return fmt.Sprintf( "Downloads: %d/%d aktiv (Limit erreicht)", s.ActiveDownloads, s.DownloadLimitMax, ) } return fmt.Sprintf( "Downloads: %d/%d aktiv", s.ActiveDownloads, s.DownloadLimitMax, ) } return fmt.Sprintf("Downloads: %d aktiv (kein Limit)", s.ActiveDownloads) } func (s TrayStats) PostworkLine() string { return fmt.Sprintf( "Postwork: %d läuft • %d wartend", s.PostworkRunning, s.PostworkWaiting, ) } func (s TrayStats) EnrichLine() string { return fmt.Sprintf( "Enrich: %d läuft • %d wartend", s.EnrichRunning, s.EnrichWaiting, ) } func (s TrayStats) DiskLine() string { if !s.HasDiskInfo { return "Freier Speicher: unbekannt" } if s.DiskEmergency { return fmt.Sprintf( "Freier Speicher: %s (kritisch)", formatTrayBytes(s.DiskFreeBytes), ) } return fmt.Sprintf( "Freier Speicher: %s", formatTrayBytes(s.DiskFreeBytes), ) } func (s TrayStats) UptimeLine() string { return "Uptime: " + formatTrayDuration(s.Uptime) } func (s TrayStats) ToggleAutostartTitle() string { if s.AutostartPausedByDisk { return "Autostart fortsetzen (gesperrt: Disk)" } if s.AutostartPaused { return "Autostart fortsetzen" } return "Autostart pausieren" } func (s TrayStats) downloadLimitText() string { if s.DownloadLimitEnabled { return fmt.Sprintf("%d/%d", s.ActiveDownloads, s.DownloadLimitMax) } return strconv.Itoa(s.ActiveDownloads) } func formatTrayDuration(d time.Duration) string { if d < 0 { d = 0 } total := int(d.Seconds()) h := total / 3600 m := (total % 3600) / 60 s := total % 60 return fmt.Sprintf("%02d:%02d:%02d", h, m, s) } func formatTrayBytes(b uint64) string { const unit = 1000 if b < unit { return fmt.Sprintf("%d B", b) } div, exp := uint64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp]) }