This commit is contained in:
ponzischeme89
2026-08-17 07:34:23 +12:00
parent 93fb0fd728
commit 36cb1324fd
56 changed files with 1177 additions and 168 deletions
+1 -1
View File
@@ -39,7 +39,7 @@ func (s *Store) HouseholdCompletionScores(
AND coalesce(nullif(emby_series_id, ''), emby_item_id) <> ''
GROUP BY item_id`, since)
if err != nil {
return nil, fmt.Errorf("store: household completion scores: %w", err)
return nil, fmt.Errorf("store: library-wide completion scores: %w", err)
}
defer rows.Close()
out := map[string]float64{}
+98
View File
@@ -15,6 +15,9 @@ import (
// MaintenanceKey is the app_settings row backing maintenance mode.
const MaintenanceKey = "maintenance"
// QuietTimeKey is the app_settings row backing the daily server quiet-time window.
const QuietTimeKey = "quiet_time"
// RequestPolicyKey controls which Emby users may send titles to Sonarr/Radarr.
const RequestPolicyKey = "request_policy"
@@ -742,6 +745,101 @@ func (s *Store) SetMaintenance(ctx context.Context, state Maintenance) error {
return nil
}
// QuietTime is a daily window in the household timezone during which Memby's data plane
// and background work stand down. The admin control plane and health checks remain live so
// an operator can change a bad schedule without restarting the container.
type QuietTime struct {
Enabled bool `json:"enabled"`
StartTime string `json:"startTime"`
EndTime string `json:"endTime"`
Message string `json:"message"`
UpdatedAt time.Time `json:"updatedAt"`
}
const DefaultQuietTimeMessage = "Memby is in quiet time. Try again later."
func DefaultQuietTime() QuietTime {
return QuietTime{StartTime: "23:00", EndTime: "07:00", Message: DefaultQuietTimeMessage}
}
// QuietTimeActive reports whether now falls in the configured local-clock window. A
// window crossing midnight includes late evening and the following morning. Equal or
// malformed endpoints are treated as inactive; the admin handler refuses both.
func QuietTimeActive(policy QuietTime, now time.Time, location *time.Location) bool {
if !policy.Enabled {
return false
}
start, startErr := time.Parse("15:04", policy.StartTime)
end, endErr := time.Parse("15:04", policy.EndTime)
if startErr != nil || endErr != nil || policy.StartTime == policy.EndTime {
return false
}
if location == nil {
location = time.UTC
}
local := now.In(location)
minute := local.Hour()*60 + local.Minute()
startMinute := start.Hour()*60 + start.Minute()
endMinute := end.Hour()*60 + end.Minute()
if startMinute < endMinute {
return minute >= startMinute && minute < endMinute
}
return minute >= startMinute || minute < endMinute
}
func normaliseQuietTime(policy QuietTime) QuietTime {
defaults := DefaultQuietTime()
if parsed, err := time.Parse("15:04", strings.TrimSpace(policy.StartTime)); err == nil {
policy.StartTime = parsed.Format("15:04")
} else {
policy.StartTime = defaults.StartTime
}
if parsed, err := time.Parse("15:04", strings.TrimSpace(policy.EndTime)); err == nil {
policy.EndTime = parsed.Format("15:04")
} else {
policy.EndTime = defaults.EndTime
}
policy.Message = strings.TrimSpace(policy.Message)
if policy.Message == "" {
policy.Message = DefaultQuietTimeMessage
}
return policy
}
func (s *Store) QuietTime(ctx context.Context) (QuietTime, error) {
var raw []byte
err := s.pool.QueryRow(ctx, `SELECT value FROM app_settings WHERE key = $1`, QuietTimeKey).Scan(&raw)
if errors.Is(err, pgx.ErrNoRows) {
return DefaultQuietTime(), nil
}
if err != nil {
return DefaultQuietTime(), fmt.Errorf("store: read quiet time: %w", err)
}
var policy QuietTime
if err := json.Unmarshal(raw, &policy); err != nil {
return DefaultQuietTime(), fmt.Errorf("store: decode quiet time: %w", err)
}
return normaliseQuietTime(policy), nil
}
func (s *Store) SetQuietTime(ctx context.Context, policy QuietTime) error {
policy = normaliseQuietTime(policy)
policy.UpdatedAt = time.Now().UTC()
raw, err := json.Marshal(policy)
if err != nil {
return err
}
_, err = s.pool.Exec(ctx, `
INSERT INTO app_settings (key, value, updated_at)
VALUES ($1, $2::jsonb, now())
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`,
QuietTimeKey, string(raw))
if err != nil {
return fmt.Errorf("store: write quiet time: %w", err)
}
return nil
}
// UpdatePolicyKey is the app_settings row backing the client update policy.
const UpdatePolicyKey = "update_policy"
+28
View File
@@ -5,6 +5,34 @@ import (
"time"
)
func TestQuietTimeActiveHandlesDaytimeAndOvernightWindows(t *testing.T) {
location := time.FixedZone("NZST", 12*60*60)
at := func(hour, minute int) time.Time {
return time.Date(2026, time.August, 17, hour, minute, 0, 0, location)
}
tests := []struct {
name string
policy QuietTime
now time.Time
want bool
}{
{"disabled", QuietTime{StartTime: "23:00", EndTime: "07:00"}, at(23, 30), false},
{"overnight evening", QuietTime{Enabled: true, StartTime: "23:00", EndTime: "07:00"}, at(23, 0), true},
{"overnight morning", QuietTime{Enabled: true, StartTime: "23:00", EndTime: "07:00"}, at(6, 59), true},
{"overnight end exclusive", QuietTime{Enabled: true, StartTime: "23:00", EndTime: "07:00"}, at(7, 0), false},
{"daytime inside", QuietTime{Enabled: true, StartTime: "09:00", EndTime: "17:00"}, at(12, 0), true},
{"daytime outside", QuietTime{Enabled: true, StartTime: "09:00", EndTime: "17:00"}, at(18, 0), false},
{"equal endpoints are safe", QuietTime{Enabled: true, StartTime: "09:00", EndTime: "09:00"}, at(9, 0), false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := QuietTimeActive(test.policy, test.now, location); got != test.want {
t.Fatalf("QuietTimeActive() = %v, want %v", got, test.want)
}
})
}
}
func TestRequestPolicyAllowsOnlyListedUsers(t *testing.T) {
policy := RequestPolicy{AllowedUserIDs: []string{"user-2"}}
if policy.Allows("user-1") {