# cz.basketball Schedule Watcher (`zdenek_kuncar/cbf-cz-schedule-watcher`) Actor

Watches a cz.basketball team's fixtures for schedule changes (new/moved/cancelled games, time & venue changes) and optionally syncs them to Google Calendar. Sends email and webhook notifications on every change.

- **URL**: https://apify.com/zdenek\_kuncar/cbf-cz-schedule-watcher.md
- **Developed by:** [Zdeněk Kunčar](https://apify.com/zdenek_kuncar) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## CBF.cz Schedule Watcher

Watches a team's fixtures on [cz.basketball](https://cz.basketball) and tells you when the schedule
changes: a game gets moved, the venue changes, a game is postponed/cancelled, or a brand-new fixture
appears. Optionally mirrors the fixtures into a Google Calendar so your team's games just show up —
and stay correct — without anyone re-typing anything.

Built for **BASKET OPAVA 2010** by default, but works for any team in any Czech basketball league:
just change the `teamId` input.

### How it works

cz.basketball doesn't have a stable per-season schedule URL — the competition ID (`/soutez/5376`)
changes every year when the league restarts. What *is* stable is the **team ID** (`/tym/13093`),
with the season selected via a `?y=` query parameter (`?y=2026` = the 2026/27 season).

Each run, the Actor:

1. Fetches `https://cz.basketball/tym/{teamId}?y={year}` to get the list of this season's fixtures.
2. Fetches each fixture's detail page (`/zapas/{id}`) for the authoritative date, time, and venue.
3. Diffs the result against the snapshot saved from the previous run.
4. Reports anything that changed — new games, reschedules, venue changes, postponements/cancellations,
   and games dropped from the schedule.
5. Optionally emails you (via Gmail), POSTs a webhook, and/or creates/updates/deletes the matching
   Google Calendar events.
6. Saves the new snapshot for next time.

If `year` is left empty, the Actor auto-detects the current season (it rolls over every August), so
it keeps working next year and the year after without any changes to the input.

### Finding your team's ID

1. Go to [cz.basketball](https://cz.basketball) and open your team's page (via your league's
   standings/competition page, or search).
2. The team ID is the number in the URL: `https://cz.basketball/tym/13093` → `teamId = "13093"`.

### Input

| Field | Required | Default | Description |
|---|---|---|---|
| `teamId` | yes | `13093` (BASKET OPAVA 2010) | The cz.basketball team ID. |
| `teamName` | yes | `BASKET OPAVA 2010` | Display name used in calendar events and notifications. |
| `year` | no | auto-detected | Season start year (the `?y=` value). Leave empty so the Actor tracks the current season automatically, forever. |
| `skipPastGames` | no | `true` | Ignore fixtures already in the past. |
| `notifyEmail` | no | — | Get an email summary when something changes, sent via Gmail from your connected Google account (see setup below). Can be any address. |
| `webhookUrl` | no | — | POST a JSON payload of the changes to your own endpoint, Zapier, Make, a Slack incoming webhook, etc. |
| `notifyOnFirstRun` | no | `false` | On the very first run every fixture looks "new" (no prior snapshot exists). Leave off to seed silently. |
| `dryRun` | no | `false` | Detect and report changes but never write to Google Calendar. |

#### Google account (optional)

**Connecting a Google account is entirely opt-in.** If you leave the fields below empty, the Actor
works purely as a change detector: it still diffs the schedule and still sends webhook notifications.
The same Google account powers two independent, separately-toggled features:

- **Calendar sync** — mirrors fixtures into Google Calendar. Requires `enableCalendar` to be turned on.
- **Email notifications** — sends the change summary via Gmail. Requires `notifyEmail` to be set.

You only need to do the OAuth setup once even if you use both — a single refresh token authorizes both.

| Field | Description |
|---|---|
| `enableCalendar` | Turn on to mirror fixtures into Google Calendar. |
| `googleCalendarId` | `primary` for your main calendar, or a specific calendar's ID. Only used when Calendar sync is on. |
| `googleClientId` / `googleClientSecret` | OAuth 2.0 credentials from Google Cloud Console. |
| `googleRefreshToken` | A long-lived refresh token (see setup below). |
| `eventDurationMinutes` | How long each game event should last (default 120). Calendar sync only. |

If `enableCalendar` is on but any of the three Google credential fields is missing, the Actor logs a
warning and skips calendar sync for that run. Likewise, if `notifyEmail` is set without the Google
fields, it logs a warning and skips the email. Neither will crash the run.

##### One-time Google OAuth setup

1. In [Google Cloud Console](https://console.cloud.google.com/), create (or pick) a project and enable
   the **Google Calendar API** and the **Gmail API**.
2. Under **APIs & Services → Credentials**, create an **OAuth client ID** of type **Desktop app**.
3. Under **OAuth consent screen**, add your own Google account as a **Test user** (unless the app is
   published/verified).
4. Clone this repo locally, install dependencies (`npm install`), then run:

   ```bash
   GOOGLE_CLIENT_ID=<your client id> GOOGLE_CLIENT_SECRET=<your client secret> npm run get-token
   ```

   This opens a consent URL in your terminal — open it in a browser, approve access, and the script
   prints your `googleClientId`, `googleClientSecret`, and `googleRefreshToken`. The token is
   authorized for both Calendar and Gmail send access.
5. Paste those three values into the Actor's input (mark the secret and refresh token fields as secret
   in Apify Console), then turn on `enableCalendar` and/or set `notifyEmail` as needed.

The refresh token doesn't expire under normal use, so this is a one-time setup. If you'd previously
generated a refresh token before email notifications used Gmail, re-run `npm run get-token` to get a
new one that also covers the Gmail scope — the old one won't have permission to send email.

### Output

Every run that detects changes pushes one dataset row per change, with the change type
(`added`, `removed`, `rescheduled`, `venue_changed`, `opponent_changed`, `status_changed`), a
human-readable summary, the affected match's details, and (for updates) the previous values.

### Scheduling

Run this Actor on a schedule (Apify Console → Schedules) — a few times a day is plenty for a
weekly amateur league; tighten it up if your league tends to announce changes close to game time.

### Local development

```bash
npm install
apify run          # reads storage/key_value_stores/default/INPUT.json
npm test           # unit tests against saved real HTML fixtures — no network required
```

### Notes on the source site

- cz.basketball is server-rendered HTML with no public API, so this Actor scrapes it directly with
  Cheerio (no browser needed — fast and light).
- The site displays `00:00` as a placeholder when a game's kickoff time hasn't been announced yet;
  the Actor treats that as "time TBD" rather than a literal midnight game, and will correctly report
  it as a schedule update once a real time is announced.
- All crawled HTML is treated as untrusted input and parsed defensively — nothing scraped is ever
  executed or interpolated into commands.

# Actor input Schema

## `teamId` (type: `string`):

The cz.basketball team ID. Found in the team page URL: https://cz.basketball/tym/\<TEAM\_ID>. This ID is stable across seasons. Default is BASKET OPAVA 2010.

## `teamName` (type: `string`):

Human-readable team name, used in calendar event titles and notifications. Should match how the team is named on cz.basketball.

## `year` (type: `integer`):

The season's starting year, i.e. the ?y= value on the team page. Season 2026/27 = 2026. Leave empty to auto-detect the current season (rolls over in August), so the Actor keeps working every year without changes.

## `skipPastGames` (type: `boolean`):

Only track and sync games scheduled for today or later. Changes to games already played are ignored.

## `notifyEmail` (type: `string`):

Email address to receive a summary whenever the schedule changes, sent via Gmail from your own connected Google account — see the Google account fields below. Can be any address, not just your own. Leave empty to disable email.

## `webhookUrl` (type: `string`):

If set, the Actor POSTs a JSON payload with the detected changes to this URL (Zapier, Make, Slack Incoming Webhook, your own endpoint, ...). Leave empty to disable.

## `notifyOnFirstRun` (type: `boolean`):

On the very first run there is no previous snapshot, so every game looks 'new'. Keep this off to seed silently; turn on to receive the full initial list.

## `enableCalendar` (type: `boolean`):

OPTIONAL. Turn on if you want fixtures mirrored as Google Calendar events. If off, the Actor just detects changes and notifies you. Requires the Google account fields below — see the README for a 5-minute setup.

## `googleCalendarId` (type: `string`):

Which calendar to write to. 'primary' is your main calendar; otherwise the calendar's ID (looks like an email address), found in Google Calendar > Settings > Integrate calendar. Only used when Calendar sync is on.

## `googleClientId` (type: `string`):

OAuth 2.0 Client ID from Google Cloud Console. Needed for Calendar sync and/or Gmail email notifications.

## `googleClientSecret` (type: `string`):

OAuth 2.0 Client Secret from Google Cloud Console.

## `googleRefreshToken` (type: `string`):

Long-lived refresh token obtained via the one-time consent flow (run `npm run get-token` locally, see README). Must be authorized for both Calendar and Gmail send access if you use both features.

## `eventDurationMinutes` (type: `integer`):

How long each game event should last in the calendar.

## `dryRun` (type: `boolean`):

Detect and report changes but do NOT write to Google Calendar. Useful for testing.

## Actor input object example

```json
{
  "teamId": "13093",
  "teamName": "BASKET OPAVA 2010",
  "skipPastGames": true,
  "notifyOnFirstRun": false,
  "enableCalendar": false,
  "googleCalendarId": "primary",
  "eventDurationMinutes": 120,
  "dryRun": false
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "teamId": "13093",
    "teamName": "BASKET OPAVA 2010",
    "googleCalendarId": "primary"
};

// Run the Actor and wait for it to finish
const run = await client.actor("zdenek_kuncar/cbf-cz-schedule-watcher").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "teamId": "13093",
    "teamName": "BASKET OPAVA 2010",
    "googleCalendarId": "primary",
}

# Run the Actor and wait for it to finish
run = client.actor("zdenek_kuncar/cbf-cz-schedule-watcher").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "teamId": "13093",
  "teamName": "BASKET OPAVA 2010",
  "googleCalendarId": "primary"
}' |
apify call zdenek_kuncar/cbf-cz-schedule-watcher --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zdenek_kuncar/cbf-cz-schedule-watcher"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/80lJHRxkgvgHXcdZ6/builds/3NgwyS8LHEwSbhBoc/openapi.json
