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 }