74 lines
2.7 KiB
Go
74 lines
2.7 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// DeviceActivityRetention is how long the per-day marks are kept. They are not history
|
|
// anybody reads — the notification they produced is the record, and that is itself pruned
|
|
// at thirty days — so this only has to be long enough that a clock correction or a
|
|
// database restored from a backup cannot make yesterday look like a day that never
|
|
// happened.
|
|
const DeviceActivityRetention = 90 * 24 * time.Hour
|
|
|
|
// MarkDeviceDay records that a television was in use on a household-local day, and
|
|
// reports whether this is the first time it has been seen today.
|
|
//
|
|
// The answer comes from the insert rather than from a read followed by a write, because
|
|
// every set in the house can reach this at once and two of them racing on the same row
|
|
// must not both be told they were first. `ON CONFLICT DO NOTHING` makes the primary key
|
|
// the arbiter: exactly one insert affects a row.
|
|
//
|
|
// It is keyed on the viewer as well as the television because a household set that two
|
|
// people sign into is two people opening Memby, and an operator reading the feed wants to
|
|
// know which of them it was.
|
|
func (s *Store) MarkDeviceDay(
|
|
ctx context.Context, deviceID, userID string, day time.Time,
|
|
) (bool, error) {
|
|
if deviceID == "" {
|
|
return false, nil
|
|
}
|
|
tag, err := s.pool.Exec(ctx, `
|
|
INSERT INTO device_activity_days (device_id, emby_user_id, day)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (device_id, emby_user_id, day) DO NOTHING`,
|
|
deviceID, userID, day.Format("2006-01-02"))
|
|
if err != nil {
|
|
return false, fmt.Errorf("store: mark device day: %w", err)
|
|
}
|
|
return tag.RowsAffected() > 0, nil
|
|
}
|
|
|
|
// PruneDeviceActivityDays drops marks older than the retention window.
|
|
func (s *Store) PruneDeviceActivityDays(
|
|
ctx context.Context, retention time.Duration,
|
|
) (int64, error) {
|
|
if retention <= 0 {
|
|
retention = DeviceActivityRetention
|
|
}
|
|
tag, err := s.pool.Exec(ctx,
|
|
`DELETE FROM device_activity_days WHERE first_seen_at < now() - $1::interval`,
|
|
fmt.Sprintf("%d seconds", int64(retention.Seconds())))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("store: prune device activity days: %w", err)
|
|
}
|
|
return tag.RowsAffected(), nil
|
|
}
|
|
|
|
// DeleteDeviceActivityDays retires a television's marks along with the television, the
|
|
// way its build history is retired: a set that is gone must not be able to announce a
|
|
// first use it can no longer have.
|
|
func (s *Store) DeleteDeviceActivityDays(ctx context.Context, deviceIDs ...string) error {
|
|
if len(deviceIDs) == 0 {
|
|
return nil
|
|
}
|
|
_, err := s.pool.Exec(ctx,
|
|
`DELETE FROM device_activity_days WHERE device_id = ANY($1::text[])`, deviceIDs)
|
|
if err != nil {
|
|
return fmt.Errorf("store: delete device activity days: %w", err)
|
|
}
|
|
return nil
|
|
}
|