Repository navigation
feat(kick): fix campaign discoverability — open-campaign directory fallback + shell rows (#60) - #62
Open
steethebutcher wants to merge 4 commits into
Conversation
Most Kick campaigns are open (channels: []) — they accrue on any live channel in the category. ListEligibleChannels only considered the campaign's own channels, a category pool built exclusively from sibling campaigns that embed channels, and manual operator channels. When no sibling campaign carries channels (e.g. "WoW Forever", the only WoW campaign), the pool was empty, the watcher logged "no eligible streams live" forever, and the campaign was unmineable without registering a channel manually (aalejandrofer#60). Wire the public /stream/livestreams/{category} directory back in as the open-campaign fallback — behind probeLive's live + on-category verification, which makes the old failure mode (the generic feed's wrong-game junk picks) impossible to regress: a wrong slug now yields zero results, never junk. Restricted campaigns never consult the directory. - api: parse the campaign payload's authoritative category slug (game_slug/category_slug sibling or nested game.slug) into the new kickCampaign.GameSlug — previously discarded, so the directory was addressed with a name-derived slug. - backend: categorySlugs cache (game -> payload slug); the fallback resolves payload slug first, then gameslug.Slug(name). Directory results are viewer-sorted (probeLive's cap lands on the biggest, most stable streams) and merged into categoryChannels so later picks work even if the feed goes away. Tests: happy path, payload-slug precedence, wrong-category rejection, restricted-campaign exclusion, and pool persistence across picks.
Non-whitelisted Kick campaigns were dropped twice — inside the backend's ListActiveCampaigns (GameFilter continue) and again by the discovery scraper's re-filter — so they never reached the campaigns table or the /drops Discoverable tab. A user couldn't see a campaign like "WoW Forever" to whitelist it; the only way in was adding the game blind on /priority. Twitch deliberately avoids this deadlock by emitting shell rows; now Kick does too. - backend: GameFilter is now a should-map-benefits gate instead of a drop-the-campaign gate. Non-whitelisted campaigns keep ID/Game/Name/ Status/window/link-state/AllowedChannels but carry EMPTY Benefits and skip the channel-pool caching. The watcher's own AllowGame (applied after ListActiveCampaigns) plus its never-mine-empty-Benefits rule keep shells out of mining — the same mechanism that already makes Twitch shells safe. - discovery/kick: stop re-applying the whitelist; pass everything the backend returned (shells included) to the persister so they surface in Discoverable and the game picker. Also refresh the stale sidecar/Cloudflare header comments — Kick data has been pure-HTTP utls since the kick-breakthrough rework. Tests: MapsAndFilters updated to the shell contract (both campaigns emitted; non-whitelisted has empty Benefits + intact status), a new ShellRowsSkipChannelPools guard, and a scraper pass-through test.
Live-checking the public /stream/livestreams/{slug} endpoint (2026-10-03)
showed two things the dead-code implementation assumed wrong:
1. The feed IGNORES the {category} path segment — querying rust vs
world-of-warcraft returns the identical global list (and next_page_url
even leaks an unrelated default category). This is the same
wrong-game-junk failure mode that got the old generic feed removed
from channel selection.
2. It is paginated: per_page=5, ?page=N works, entries carry their own
categories[] (name+slug) alongside channel.slug.
So DiscoverChannelsForCategory now walks up to 4 pages and filters
client-side — an entry qualifies when any embedded category matches the
wanted slug or the campaign's display name (case/space-insensitive;
real feed even mixes case, e.g. "Project-Zomboid"). Correct whether or
not Kick restores per-category filtering, and probeLive still hard-
verifies every candidate afterwards, so junk picks remain impossible.
Live-verified by simulation against the production feed: 4-page scan
extracted categories correctly and the filter matched channels for a
category present in the window. The window is a slice of a global feed,
so discovery is best-effort progressive — successive watcher cycles
keep scanning — not exhaustive; noted in the PR.
api_live_test updated to the new signature with a note that zero
results may just mean the category wasn't live in the scanned window.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the Kick half of #60 ("even if I add kick campaigns like WoW Forever it won't mine it, and I must add whitelist channel like rewardstation to mine the wow drop").
Root causes (two independent defects)
1. Open campaigns had no channel candidates. Most Kick campaigns are open (
channels: []— accrue on any live channel in the category).ListEligibleChannelsonly considered the campaign's own channels, a category pool built exclusively from sibling campaigns that embed channels, and manual operator channels. WoW Forever is the only WoW campaign, so the pool was empty → "no eligible streams live" forever → unmineable without a manual channel registration.2. Non-whitelisted campaigns were invisible (deadlock). The backend dropped them (GameFilter
continue) and the scraper re-dropped them, so they never reached the campaigns table or the /drops Discoverable tab. A user couldn't see "WoW Forever" to whitelist it — the only way in was adding the game blind on /priority. Twitch deliberately avoids this deadlock with shell rows; Kick now does too.Commits
1.
feat(kick): directory fallback for open campaigns with no channel poolWhen an open campaign's pooled candidates yield nothing live, consult the public category directory, then merge + probe:
game_slug(previously discarded) so the directory is addressed correctly instead of re-deriving the slug from the display name (cf. the Twitch slug bugs in "watcher no eligible streams live" logged despite active Twitch streams with drops for campaigns #61).categoryChannelsso later picks work even if the feed goes away.probeLive's live + on-category verification — a wrong slug can only produce zero results, never the junk picks that got the old generic feed removed.channels[]) never consult the directory.2.
feat(kick): emit shell rows for non-whitelisted campaignsGameFilter becomes a should-map-benefits gate (Twitch parity, campaigns.go:236): non-whitelisted campaigns keep ID/Game/Name/Status/window/AllowedChannels but carry empty Benefits and skip channel-pool caching. The scraper passes shells through to the persister so they surface in Discoverable + the game picker. Safety is the same mechanism that already makes Twitch shells safe: the watcher's own AllowGame + never-mine-empty-Benefits.
3.
fix(kick): directory feed is global + paginated — filter client-sideI live-checked the public
/stream/livestreams/{slug}endpoint before wiring it in (2026-10-03) and found the dead-code assumptions wrong:?page=N), and each entry carries its owncategories[](name+slug) +channel.slug.So
DiscoverChannelsForCategorynow walks ≤4 pages and filters client-side by slug or display name (case/space-insensitive — the real feed mixes case, e.g."Project-Zomboid"). Live-verified by simulation against the production feed: a 4-page scan extracted categories correctly and the filter matched channels for a category present in the window. Combined with probeLive's independent verification, junk picks remain impossible whether or not Kick restores per-category filtering.Known limitations (deliberate)
/api/v1/drops/campaignshas no pagination handling — if Kick ever pages that endpoint, campaigns would silently truncate. Unverifiable without a live authed response; flagging for awareness.Verification
go build ./...,go vet ./...clean; full test suite green (24 packages).Out of scope
Twitch slug derivation (#61 — JourneyOver's fixes in flight), device-code login (#48), pipeline redesign (#50).