feat(web): real channel filter — multi-select of the user's channels
The free-text 'channel' filter was dead: it exact-matched SummaryRow.Channel,
which is just the provider ('youtube'), because videos never stored their source
channel. Now they do.
- migration 014: videos.channel_title (nullable; existing rows backfill on the
next discovery pass, pasted videos immediately).
- discovery (NewVideos) + paste (VideoByID) populate channel_title; UpsertVideo
persists it, preserving an existing title when an update arrives empty.
- store.DistinctChannels lists a user's channels (RLS-scoped); SummaryRow carries
ChannelTitle via the shared projection.
- Filter: single Channel -> Channels []string, matching on ChannelTitle; the feed
renders a multi-select of DistinctChannels (hidden until channels exist).
- migrate tests: 014 reversibility + fixed the relative-step counts in the 010/011
up/down tests (014 shifted the topology).
TDD throughout: channel persist + distinct, adapter channel wiring, multi-channel
filter match, handler channel filter, migration up/down.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ func TestVideoByID(t *testing.T) {
|
||||
if got := r.URL.Query().Get("part"); got != "snippet" {
|
||||
t.Errorf("expected part=snippet, got %q", got)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"items":[{"snippet":{"title":"Never Gonna Give You Up","publishedAt":"2026-05-20T09:00:00Z"}}]}`))
|
||||
_, _ = w.Write([]byte(`{"items":[{"snippet":{"title":"Never Gonna Give You Up","channelTitle":"Rick Astley","publishedAt":"2026-05-20T09:00:00Z"}}]}`))
|
||||
})
|
||||
|
||||
v, err := a.VideoByID(context.Background(), "u1", id)
|
||||
@@ -34,6 +34,9 @@ func TestVideoByID(t *testing.T) {
|
||||
if v.ProviderVideoID != id || v.Title != "Never Gonna Give You Up" {
|
||||
t.Errorf("unexpected video: %+v", v)
|
||||
}
|
||||
if v.ChannelTitle != "Rick Astley" {
|
||||
t.Errorf("ChannelTitle = %q, want Rick Astley", v.ChannelTitle)
|
||||
}
|
||||
if v.Provider != domain.ProviderYouTube || v.URL != "https://www.youtube.com/watch?v="+id {
|
||||
t.Errorf("video not wired correctly: %+v", v)
|
||||
}
|
||||
|
||||
@@ -234,6 +234,7 @@ func (a *Adapter) NewVideos(ctx context.Context, sub domain.Subscription) ([]dom
|
||||
Provider: domain.ProviderYouTube,
|
||||
ProviderVideoID: vid,
|
||||
Title: item.Snippet.Title,
|
||||
ChannelTitle: sub.ChannelTitle,
|
||||
URL: "https://www.youtube.com/watch?v=" + vid,
|
||||
PublishedAt: item.Snippet.PublishedAt,
|
||||
})
|
||||
@@ -270,6 +271,7 @@ func (a *Adapter) VideoByID(ctx context.Context, userID, videoID string) (domain
|
||||
Provider: domain.ProviderYouTube,
|
||||
ProviderVideoID: videoID,
|
||||
Title: it.Snippet.Title,
|
||||
ChannelTitle: it.Snippet.ChannelTitle,
|
||||
URL: "https://www.youtube.com/watch?v=" + videoID,
|
||||
PublishedAt: it.Snippet.PublishedAt,
|
||||
}, nil
|
||||
@@ -376,8 +378,9 @@ type playlistItemListResponse struct {
|
||||
type videoListResponse struct {
|
||||
Items []struct {
|
||||
Snippet struct {
|
||||
Title string `json:"title"`
|
||||
PublishedAt time.Time `json:"publishedAt"`
|
||||
Title string `json:"title"`
|
||||
ChannelTitle string `json:"channelTitle"`
|
||||
PublishedAt time.Time `json:"publishedAt"`
|
||||
} `json:"snippet"`
|
||||
} `json:"items"`
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ func TestNewVideos(t *testing.T) {
|
||||
}`))
|
||||
})
|
||||
|
||||
sub := domain.Subscription{ID: "s1", UserID: "u1", ChannelID: "UC_acme"}
|
||||
sub := domain.Subscription{ID: "s1", UserID: "u1", ChannelID: "UC_acme", ChannelTitle: "Acme Channel"}
|
||||
vids, err := a.NewVideos(context.Background(), sub)
|
||||
if err != nil {
|
||||
t.Fatalf("NewVideos: %v", err)
|
||||
@@ -143,6 +143,9 @@ func TestNewVideos(t *testing.T) {
|
||||
if v.ProviderVideoID != "vid1" || v.Title != "Designing for Attention" {
|
||||
t.Errorf("unexpected video: %+v", v)
|
||||
}
|
||||
if v.ChannelTitle != "Acme Channel" {
|
||||
t.Errorf("ChannelTitle = %q, want Acme Channel", v.ChannelTitle)
|
||||
}
|
||||
if v.Provider != domain.ProviderYouTube || v.URL != "https://www.youtube.com/watch?v=vid1" {
|
||||
t.Errorf("video not wired correctly: %+v", v)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user