0.3.49
This commit is contained in:
@@ -180,6 +180,147 @@ func (s *Store) AllMediaRequests(ctx context.Context, limit int) ([]OwnedMediaRe
|
||||
// MediaRequestSweepLimit caps what one pass of the ready sweep will look at.
|
||||
const MediaRequestSweepLimit = 500
|
||||
|
||||
// RequestReadySurfaceWindow is how long a freshly arrived request stays pinned to the front
|
||||
// of Continue Watching with a REQUEST READY tag if the viewer never plays it. Past this the
|
||||
// age-out clears it: an arrival nobody has acted on in a fortnight is no longer news, and a
|
||||
// pin that outstays its welcome is worse than one that lapses.
|
||||
const RequestReadySurfaceWindow = 14 * 24 * time.Hour
|
||||
|
||||
// SurfacedReadyRequest is one arrival still worth pinning: which title, and the Emby item id
|
||||
// the card opens and dedupes against.
|
||||
type SurfacedReadyRequest struct {
|
||||
MediaType string
|
||||
ForeignID int
|
||||
Title string
|
||||
Year int
|
||||
PosterURL string
|
||||
ItemID string
|
||||
SurfacedAt time.Time
|
||||
}
|
||||
|
||||
// MarkRequestReadySurfaced records that a request has become watchable and should be pinned.
|
||||
//
|
||||
// The guard is the whole of the idempotency: the ready sweep calls this on every observed
|
||||
// transition to "available", and only the first one — before anything has cleared it — takes
|
||||
// effect. A request cleared by playback or the age-out is never re-pinned here; a genuinely
|
||||
// new ask is a fresh row with both timestamps null.
|
||||
func (s *Store) MarkRequestReadySurfaced(
|
||||
ctx context.Context, userID, mediaType string, foreignID int, itemID string,
|
||||
) error {
|
||||
itemID = strings.TrimSpace(itemID)
|
||||
if itemID == "" {
|
||||
return fmt.Errorf("store: request ready needs an item id")
|
||||
}
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE media_requests
|
||||
SET ready_surfaced_at = now(), ready_item_id = $4
|
||||
WHERE emby_user_id = $1 AND media_type = $2 AND foreign_id = $3
|
||||
AND ready_surfaced_at IS NULL AND ready_cleared_at IS NULL`,
|
||||
strings.TrimSpace(userID), mediaType, foreignID, itemID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("store: mark request ready surfaced: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SurfacedReadyRequests reads one viewer's still-pinned arrivals, newest first.
|
||||
//
|
||||
// The window and the cleared check are both in the query so the caller never has to think
|
||||
// about either: a row past RequestReadySurfaceWindow, or one playback has cleared, simply
|
||||
// does not come back. This is the read Home makes beside its Emby fan-out.
|
||||
func (s *Store) SurfacedReadyRequests(
|
||||
ctx context.Context, userID string,
|
||||
) ([]SurfacedReadyRequest, error) {
|
||||
cutoff := time.Now().Add(-RequestReadySurfaceWindow)
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT media_type, foreign_id, title, year, poster_url, ready_item_id, ready_surfaced_at
|
||||
FROM media_requests
|
||||
WHERE emby_user_id = $1
|
||||
AND ready_surfaced_at IS NOT NULL
|
||||
AND ready_cleared_at IS NULL
|
||||
AND ready_surfaced_at > $2
|
||||
AND ready_item_id <> ''
|
||||
ORDER BY ready_surfaced_at DESC`,
|
||||
strings.TrimSpace(userID), cutoff)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: read surfaced ready requests: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := []SurfacedReadyRequest{}
|
||||
for rows.Next() {
|
||||
var req SurfacedReadyRequest
|
||||
if err := rows.Scan(
|
||||
&req.MediaType, &req.ForeignID, &req.Title, &req.Year,
|
||||
&req.PosterURL, &req.ItemID, &req.SurfacedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("store: scan surfaced ready request: %w", err)
|
||||
}
|
||||
out = append(out, req)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// ClearRequestReadySurfaced retires the pin for whichever of a viewer's requests point at
|
||||
// the given Emby item id — the film itself, or the series an episode belongs to. It returns
|
||||
// how many rows it touched so the caller can skip a cache invalidation that would change
|
||||
// nothing.
|
||||
func (s *Store) ClearRequestReadySurfaced(
|
||||
ctx context.Context, userID string, itemIDs ...string,
|
||||
) (int64, error) {
|
||||
ids := make([]string, 0, len(itemIDs))
|
||||
for _, id := range itemIDs {
|
||||
if id = strings.TrimSpace(id); id != "" {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
tag, err := s.pool.Exec(ctx, `
|
||||
UPDATE media_requests
|
||||
SET ready_cleared_at = now()
|
||||
WHERE emby_user_id = $1 AND ready_item_id = ANY($2)
|
||||
AND ready_surfaced_at IS NOT NULL AND ready_cleared_at IS NULL`,
|
||||
strings.TrimSpace(userID), ids)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("store: clear request ready surfaced: %w", err)
|
||||
}
|
||||
return tag.RowsAffected(), nil
|
||||
}
|
||||
|
||||
// ExpireStaleReadyRequests clears every pin past RequestReadySurfaceWindow across the whole
|
||||
// household in one statement, and returns the users whose Home cache is now stale. The ready
|
||||
// sweep runs this so SurfacedReadyRequests never has to lean on its own window clause to
|
||||
// hide a row that should have been retired days ago.
|
||||
func (s *Store) ExpireStaleReadyRequests(ctx context.Context) ([]string, error) {
|
||||
cutoff := time.Now().Add(-RequestReadySurfaceWindow)
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
UPDATE media_requests
|
||||
SET ready_cleared_at = now()
|
||||
WHERE ready_surfaced_at IS NOT NULL AND ready_cleared_at IS NULL
|
||||
AND ready_surfaced_at <= $1
|
||||
RETURNING emby_user_id`, cutoff)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: expire stale ready requests: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
seen := map[string]bool{}
|
||||
users := []string{}
|
||||
for rows.Next() {
|
||||
var userID string
|
||||
if err := rows.Scan(&userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !seen[userID] {
|
||||
seen[userID] = true
|
||||
users = append(users, userID)
|
||||
}
|
||||
}
|
||||
return users, rows.Err()
|
||||
}
|
||||
|
||||
// SetMediaRequestStatus records what a request was last seen doing.
|
||||
//
|
||||
// Written only when the state actually moved, so a sweep over a household where nothing has
|
||||
|
||||
Reference in New Issue
Block a user