From 4e709c847029e8a68520bb2436b85e38fca63dcd Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Fri, 6 Mar 2026 18:01:08 -0500 Subject: [PATCH] feat(race): add `supports_countdown` to `ContestListResult` Problem: `:CP race` on platforms without future contests (CSES, USACO) wastes time fetching the full contest list only to discover there is no `start_time`. The error message is also uninformative. Solution: Add `supports_countdown` bool to `ContestListResult` (default `True`). CSES and USACO set it to `False`. Cache the flag per-platform so subsequent calls skip the fetch entirely. `race.lua` checks the cached value first, then the scraper result, and shows `" does not support :CP race"` instead of a generic error. --- lua/cp/cache.lua | 16 +++++++++++++++- lua/cp/pickers/init.lua | 6 ++++-- lua/cp/race.lua | 28 ++++++++++++++++++++++------ lua/cp/scraper.lua | 7 +++++-- scrapers/cses.py | 8 ++++++-- scrapers/models.py | 1 + scrapers/usaco.py | 12 +++++++++--- 7 files changed, 62 insertions(+), 16 deletions(-) diff --git a/lua/cp/cache.lua b/lua/cp/cache.lua index 987caee..5dac48d 100644 --- a/lua/cp/cache.lua +++ b/lua/cp/cache.lua @@ -392,7 +392,8 @@ end ---@param platform string ---@param contests ContestSummary[] -function M.set_contest_summaries(platform, contests) +---@param opts? { supports_countdown?: boolean } +function M.set_contest_summaries(platform, contests, opts) cache_data[platform] = cache_data[platform] or {} for _, contest in ipairs(contests) do cache_data[platform][contest.id] = cache_data[platform][contest.id] or {} @@ -405,9 +406,22 @@ function M.set_contest_summaries(platform, contests) end end + if opts and opts.supports_countdown ~= nil then + cache_data[platform].supports_countdown = opts.supports_countdown + end + M.save() end +---@param platform string +---@return boolean? +function M.get_supports_countdown(platform) + if not cache_data[platform] then + return nil + end + return cache_data[platform].supports_countdown +end + ---@param platform string ---@param contest_id string ---@return integer? diff --git a/lua/cp/pickers/init.lua b/lua/cp/pickers/init.lua index 562c274..3eb975f 100644 --- a/lua/cp/pickers/init.lua +++ b/lua/cp/pickers/init.lua @@ -48,8 +48,10 @@ function M.get_platform_contests(platform, refresh) { level = vim.log.levels.INFO, override = true, sync = true } ) - local contests = scraper.scrape_contest_list(platform) - cache.set_contest_summaries(platform, contests) + local result = scraper.scrape_contest_list(platform) + local contests = result and result.contests or {} + local sc = result and result.supports_countdown + cache.set_contest_summaries(platform, contests, { supports_countdown = sc }) picker_contests = cache.get_contest_summaries(platform) logger.log( diff --git a/lua/cp/race.lua b/lua/cp/race.lua index b44b9ab..3e84830 100644 --- a/lua/cp/race.lua +++ b/lua/cp/race.lua @@ -35,20 +35,36 @@ function M.start(platform, contest_id, language) end cache.load() + + local display = constants.PLATFORM_DISPLAY_NAMES[platform] or platform + local cached_countdown = cache.get_supports_countdown(platform) + if cached_countdown == false then + logger.log(('%s does not support :CP race'):format(display), { level = vim.log.levels.ERROR }) + return + end + local start_time = cache.get_contest_start_time(platform, contest_id) if not start_time then - logger.log('Fetching contest list...', { level = vim.log.levels.INFO, override = true }) - local contests = scraper.scrape_contest_list(platform) - if contests and #contests > 0 then - cache.set_contest_summaries(platform, contests) - start_time = cache.get_contest_start_time(platform, contest_id) + logger.log('Fetching contest list...', { level = vim.log.levels.INFO, override = true, sync = true }) + local result = scraper.scrape_contest_list(platform) + if result then + local sc = result.supports_countdown + if sc == false then + cache.set_contest_summaries(platform, result.contests or {}, { supports_countdown = false }) + logger.log(('%s does not support :CP race'):format(display), { level = vim.log.levels.ERROR }) + return + end + if result.contests and #result.contests > 0 then + cache.set_contest_summaries(platform, result.contests, { supports_countdown = sc }) + start_time = cache.get_contest_start_time(platform, contest_id) + end end end if not start_time then logger.log( - ('No start time found for %s contest %s'):format( + ('No start time found for %s contest "%s"'):format( constants.PLATFORM_DISPLAY_NAMES[platform] or platform, contest_id ), diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index 28705df..e488cc5 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -257,9 +257,12 @@ function M.scrape_contest_list(platform) ), { level = vim.log.levels.ERROR } ) - return {} + return nil end - return result.data.contests + return { + contests = result.data.contests, + supports_countdown = result.data.supports_countdown ~= false, + } end ---@param platform string diff --git a/scrapers/cses.py b/scrapers/cses.py index bf1edbd..d0ff3d8 100644 --- a/scrapers/cses.py +++ b/scrapers/cses.py @@ -228,9 +228,13 @@ class CSESScraper(BaseScraper): cats = parse_categories(html) if not cats: return ContestListResult( - success=False, error=f"{self.platform_name}: No contests found" + success=False, + error=f"{self.platform_name}: No contests found", + supports_countdown=False, ) - return ContestListResult(success=True, error="", contests=cats) + return ContestListResult( + success=True, error="", contests=cats, supports_countdown=False + ) async def login(self, credentials: dict[str, str]) -> LoginResult: username = credentials.get("username", "") diff --git a/scrapers/models.py b/scrapers/models.py index 2d579cb..4560c3e 100644 --- a/scrapers/models.py +++ b/scrapers/models.py @@ -50,6 +50,7 @@ class MetadataResult(ScrapingResult): class ContestListResult(ScrapingResult): contests: list[ContestSummary] = Field(default_factory=list) + supports_countdown: bool = True model_config = ConfigDict(extra="forbid") diff --git a/scrapers/usaco.py b/scrapers/usaco.py index 53f92be..b3970db 100644 --- a/scrapers/usaco.py +++ b/scrapers/usaco.py @@ -337,10 +337,16 @@ class USACOScraper(BaseScraper): contests.extend(await coro) if not contests: - return self._contests_error("No contests found") - return ContestListResult(success=True, error="", contests=contests) + return ContestListResult( + success=False, error="No contests found", supports_countdown=False + ) + return ContestListResult( + success=True, error="", contests=contests, supports_countdown=False + ) except Exception as e: - return self._contests_error(str(e)) + return ContestListResult( + success=False, error=str(e), supports_countdown=False + ) async def stream_tests_for_category_async(self, category_id: str) -> None: month_year, division = _parse_contest_id(category_id)