feat(store,runner,web): channel unavailability notice (migration 013)
YouTube channels that 404 on playlist discovery (deleted/private) are now: 1. Wrapped in domain.ErrChannelUnavailable by the YouTube adapter (instead of a generic error), so the runner can identify them without string-matching. 2. Stored per-user in channel_errors (migration 013, RLS-guarded) via runner's new UpsertChannelError path — removed from the generic Errors counter, counted separately as ChannelUnavailable. 3. Shown on the account page under "Unavailable channels" with name, chip-warn badge, and first-seen date, so users know why some subscribed channels produce no videos.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// ChannelError is a channel that returned HTTP 404 on a discovery pass.
|
||||
type ChannelError struct {
|
||||
ChannelID string
|
||||
ChannelName string
|
||||
FirstSeen time.Time
|
||||
LastSeen time.Time
|
||||
}
|
||||
|
||||
// UpsertChannelError records (or refreshes) a 404 channel for the current user.
|
||||
// Called by the runner inside a withUser scope; RLS guards user isolation.
|
||||
func (s *Store) UpsertChannelError(ctx context.Context, userID, channelID, channelName string) error {
|
||||
return s.withUser(ctx, userID, func(tx pgx.Tx) error {
|
||||
_, err := tx.Exec(ctx, `
|
||||
INSERT INTO channel_errors (user_id, channel_id, channel_name)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (user_id, channel_id)
|
||||
DO UPDATE SET channel_name = EXCLUDED.channel_name, last_seen = now()`,
|
||||
userID, channelID, channelName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("store: upsert channel error: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// ListChannelErrors returns all 404-flagged channels for the user, newest first.
|
||||
func (s *Store) ListChannelErrors(ctx context.Context, userID string) ([]ChannelError, error) {
|
||||
var out []ChannelError
|
||||
err := s.withUser(ctx, userID, func(tx pgx.Tx) error {
|
||||
rows, err := tx.Query(ctx, `
|
||||
SELECT channel_id, channel_name, first_seen, last_seen
|
||||
FROM channel_errors
|
||||
WHERE user_id = $1
|
||||
ORDER BY last_seen DESC`, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("store: list channel errors: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var ce ChannelError
|
||||
if err := rows.Scan(&ce.ChannelID, &ce.ChannelName, &ce.FirstSeen, &ce.LastSeen); err != nil {
|
||||
return fmt.Errorf("store: scan channel error: %w", err)
|
||||
}
|
||||
out = append(out, ce)
|
||||
}
|
||||
return rows.Err()
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
Reference in New Issue
Block a user