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.
60 lines
1.7 KiB
Go
60 lines
1.7 KiB
Go
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
|
|
}
|