Skip to content

feat(watch): theatre/venue prefer-filter on /watch - #85

Merged
Ishannaik merged 1 commit into
Ishannaik:mainfrom
dchaudhari7177:feat/theatre-filter
Aug 31, 2026
Merged

feat(watch): theatre/venue prefer-filter on /watch#85
Ishannaik merged 1 commit into
Ishannaik:mainfrom
dchaudhari7177:feat/theatre-filter

Conversation

@dchaudhari7177

@dchaudhari7177 dchaudhari7177 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Closes #27.

What

theatre: alongside the existing format: and days:, following the same shape end to end — normalise on input, persist in its own column (ALTER TABLE + catch), surface in /list and in the DM filter line.

/watch link:<bms-url> theatre:PVR,INOX
/watch link:<bms-url> date:2026-08-15 theatre:IMOB

matchesTheatre is a case-insensitive substring against the venue name and the venue code, so theatre:PVR catches PVR: Phoenix Palladium, Lower Parel and someone who'd rather paste IMOB still matches. Whitespace is collapsed on both sides, so IMAX Wadala matches IMAX WADALA.

One deliberate guard: an empty filter entry never matches. A stray trailing comma (theatre:PVR,) would otherwise produce an "" needle, and haystack.includes("") is always true — the filter would silently become a wildcard that matches every cinema. There's a test pinning it.

The interesting bit — new-cinema alerts

format: and days: are deliberately not applied to the new-cinema half of a subscription; the code comments and the README both explain that filtering it would need a showtimes fetch per venue plus careful coalescing.

theatre: doesn't have that problem. A fresh venue already carries its name and code from the very response the venue diff is computed from — so filtering it there costs no extra request and needs none of the coalescing. So theatre:PVR does stay silent when an INOX starts listing the film, which is what the issue's "only announce dates/cinemas that match" asks for.

I updated the code comment and the README caveat section to say exactly this, rather than leaving the docs claiming all filters behave alike.

On the fresh-dates path the theatre check rides the existing per-date fetchShowtimes instead of adding a second pass, so combining format: and theatre: doesn't double the requests.

Acceptance

  • Slash option + persistence + /list display
  • One-shot path honours the filter (checkWatch)
  • Subscription path honours it — both the fresh-dates and the fresh-venues halves

Docs updated: README intro, feature table, commands table, the format:/days: caveat block, and the /help embed.

Checks run locally

gate before after
bun test 85 pass, 0 fail 98 pass, 0 fail (13 new)
bunx tsc --noEmit 2 errors in db.test.ts:145-146 same 2, unchanged

Those two TS2532s are pre-existing on main and untouched here; my new db.test.ts cases use optional chaining and add none.

One thing worth knowing

While building this I briefly had a genuine bug — the INSERT column list and the bound args got out of sync. addWatch's catch { return null } swallowed SQLite query expected 10 values, received 11 and reported it as "already watching that movie", which is what a UNIQUE violation looks like. Every db.test.ts case failed with a misleading addWatch failed and no cause.

Not fixing that here since it's out of scope, but a catch (e) that re-throws anything which isn't a constraint violation would have turned a confusing ten minutes into an obvious one. Happy to open a separate issue.

Summary by CodeRabbit

  • New Features

    • Added theatre filtering to watch notifications using cinema names or venue codes.
    • Supports case-insensitive, comma-separated theatre filters with flexible spacing.
    • Theatre filters apply to new-cinema alerts and showtime notifications alongside existing filters.
    • /watch confirmations and watch lists now display configured theatre filters.
  • Bug Fixes

    • Existing watches continue working with no theatre filter configured.
  • Documentation

    • Updated help text and README with theatre-filter usage and matching behavior.

Greptile Summary

This PR adds a normalized, persisted theatre: preference and applies it to fixed-date watches, new-date checks, and new-cinema subscription alerts.

  • Adds theatre-filter normalization and name/code substring matching.
  • Persists and displays theatre preferences in watch confirmations and lists.
  • Updates command registration, help text, documentation, and focused tests.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/index.ts Threads the theatre preference through watch creation and applies it across the fixed-date, new-date, and new-venue polling paths.
src/filters.ts Adds theatre normalization, whitespace-insensitive name/code matching, and filter-summary rendering.
src/db.ts Adds a backward-compatible theatre-filter column and keeps the inserted column/value ordering aligned.
src/register.ts Registers the optional theatre slash-command argument.
src/messages.ts Displays theatre preferences in watch lists and documents their alert scope in help output.
src/theatre-filter.test.ts Covers normalization, matching by venue name or code, whitespace handling, empty entries, and summaries.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Discord /watch theatre filter] --> B[Normalize theatre values]
  B --> C[(Persist on watch)]
  C --> D{Monitoring path}
  D -->|Fixed date| E[Filter fetched shows by venue name or code]
  D -->|New subscription date| F[Fetch shows and filter by venue]
  D -->|New cinema| G[Filter venue snapshot by name or code]
  E --> H[Send matching alert]
  F --> H
  G --> H
Loading

Reviews (2): Last reviewed commit: "feat(watch): theatre/venue prefer-filter..." | Re-trigger Greptile

Context used (5)

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 80330efd-a60d-49ec-a852-f8a13f972622

📝 Walkthrough

Walkthrough

The /watch command now accepts an optional theatre filter. Watches persist the filter, display it in summaries, and apply it to subscription and single-date alerts using case-insensitive venue-name or venue-code matching.

Changes

Theatre filtering

Layer / File(s) Summary
Watch persistence contract
src/db.ts, src/db.test.ts
Watch and the database schema now include nullable theatre_filter values. Tests cover persistence and the omitted-filter default.
Command input and filter display
src/register.ts, src/index.ts, src/messages.ts, README.md
The /watch command accepts theatre values. The application persists and displays normalized filters. Help text and README documentation describe matching behavior and alert scope.
Subscription and single-date filtering
src/index.ts, src/theatre-filter.test.ts
Subscription polling filters venues, dates, and shows by theatre. Single-date alerts exclude non-matching theatres. Tests cover matching, normalization, edge cases, and summaries.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: ishannaik, slegarraga

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: adding a theatre or venue preference filter to /watch.
Linked Issues check ✅ Passed The changes satisfy issue #27 by implementing theatre filtering across commands, persistence, display, alerts, tests, and documentation.
Out of Scope Changes check ✅ Passed All changes support the theatre-filter objective and its required persistence, matching, alerting, testing, and documentation updates.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/db.ts`:
- Line 63: Update the migration around the ALTER TABLE statement in src/db.ts to
ignore only the duplicate-column error for theatre_filter. Inspect the caught
error’s code/message for that specific case, and re-throw all other migration
failures so addWatch does not continue after locked, read-only, or otherwise
invalid database errors.

In `@src/index.ts`:
- Around line 51-58: Update the one-shot watch creation validation around the
open.length branch to filter open shows using the requested formatFilter,
theatreFilter, and dayFilter predicates before deciding tickets are already on
sale. Ensure the watch is saved when no show matches all requested filters,
while preserving the existing already-on-sale response for matching live shows.
- Around line 224-235: Add a per-cycle shared promise cache for showtime lookups
keyed by city, event code, and date, and update the fresh-date filtering logic
around fetchShowtimes to reuse the cached promise instead of calling
fetchShowtimes directly. Ensure watches with identical lookup keys share one
request while distinct keys remain independent.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b8fa8cc9-31d0-40d5-b6b4-6133774c7a8b

📥 Commits

Reviewing files that changed from the base of the PR and between 036f520 and f6cec3e.

📒 Files selected for processing (8)
  • README.md
  • src/db.test.ts
  • src/db.ts
  • src/filters.ts
  • src/index.ts
  • src/messages.ts
  • src/register.ts
  • src/theatre-filter.test.ts
Comment thread src/db.ts
// Migration: add filter columns if the table predates them.
try { db.exec("ALTER TABLE watches ADD COLUMN format_filter TEXT"); } catch { /* already exists */ }
try { db.exec("ALTER TABLE watches ADD COLUMN day_filter TEXT"); } catch { /* already exists */ }
try { db.exec("ALTER TABLE watches ADD COLUMN theatre_filter TEXT"); } catch { /* already exists */ }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Do not suppress unexpected migration errors.

Line 63 treats every ALTER TABLE failure as an existing column. If the database is locked or read-only, the migration is skipped. The later insert then fails and addWatch returns the duplicate-watch message.

Ignore only the duplicate-column case. Re-throw every other error.

Proposed fix
-try { db.exec("ALTER TABLE watches ADD COLUMN theatre_filter TEXT"); } catch { /* already exists */ }
+const columns = db.query("PRAGMA table_info(watches)").all() as { name: string }[];
+if (!columns.some((column) => column.name === "theatre_filter")) {
+  db.exec("ALTER TABLE watches ADD COLUMN theatre_filter TEXT");
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try { db.exec("ALTER TABLE watches ADD COLUMN theatre_filter TEXT"); } catch { /* already exists */ }
const columns = db.query("PRAGMA table_info(watches)").all() as { name: string }[];
if (!columns.some((column) => column.name === "theatre_filter")) {
db.exec("ALTER TABLE watches ADD COLUMN theatre_filter TEXT");
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/db.ts` at line 63, Update the migration around the ALTER TABLE statement
in src/db.ts to ignore only the duplicate-column error for theatre_filter.
Inspect the caught error’s code/message for that specific case, and re-throw all
other migration failures so addWatch does not continue after locked, read-only,
or otherwise invalid database errors.
Comment thread src/index.ts Outdated
Comment on lines +51 to +58
const theatreFilter = normaliseTheatres(i.options.getString("theatre"));
try {
const parsed = parseWatchUrl(i.options.getString("link", true));
const dateOpt = i.options.getString("date")?.trim();
// "any" (or a link with no date and no date option) subscribes to the movie:
// ping me every time a NEW date unlocks, rather than watching one date.
const wantsAny = dateOpt ? /^(any|all|every|new)$/i.test(dateOpt) : !parsed.date;
if (wantsAny) return void (await subscribeToMovie(i, parsed, formatFilter, dayFilter));
if (wantsAny) return void (await subscribeToMovie(i, parsed, formatFilter, dayFilter, theatreFilter));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Apply filters during one-shot creation validation.

Line 51 accepts theatreFilter, but the open.length branch at Lines 95-98 checks unfiltered shows. If only INOX has shows and the user requests theatre:PVR, the command reports that tickets are already on sale and does not save the watch.

Filter open with the requested format, theatre, and day predicates before the already-on-sale response. Save the watch when no matching show is live.

Proposed fix
-  if (open.length) {
+  const matchingOpen = open.filter((show) =>
+    (!formatFilter || matchesFormat(show.attributes, formatFilter))
+    && (!dayFilter || matchesDay(show.showDateCode, dayFilter))
+    && (!theatreFilter || matchesTheatre(show.venueName, show.venueCode, theatreFilter)),
+  );
+
+  if (matchingOpen.length) {
     return void i.editReply(
-      msg.alreadyOnSale({ title, city: target.city, date: target.date, shows: open, url: showtimesUrl(target) }),
+      msg.alreadyOnSale({ title, city: target.city, date: target.date, shows: matchingOpen, url: showtimesUrl(target) }),
     );
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.ts` around lines 51 - 58, Update the one-shot watch creation
validation around the open.length branch to filter open shows using the
requested formatFilter, theatreFilter, and dayFilter predicates before deciding
tickets are already on sale. Ensure the watch is saved when no show matches all
requested filters, while preserving the existing already-on-sale response for
matching live shows.
Comment thread src/index.ts Outdated
Comment on lines +224 to +235
// Format / theatre filters: only announce dates that actually have matching shows.
// Costs an extra fetchShowtimes per fresh date — only spent when one of them is set,
// and a single fetch covers both so adding a theatre filter never doubles the requests.
let matchedFormats: string[] = [];
if (w.format_filter && freshDates.length) {
if ((w.format_filter || w.theatre_filter) && freshDates.length) {
const kept: string[] = [];
for (const d of freshDates) {
try {
const shows = showsOnDate((await fetchShowtimes({ city: w.city, slug: w.slug, eventCode: w.event_code, date: d })).shows, d);
const hits = shows.filter((s) => matchesFormat(s.attributes, w.format_filter!));
const hits = shows.filter((s) =>
(!w.format_filter || matchesFormat(s.attributes, w.format_filter))
&& (!w.theatre_filter || matchesTheatre(s.venueName, s.venueCode, w.theatre_filter)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant polling and BookMyShow request functions.
ast-grep outline src/index.ts --items all --match 'checkSubscription|bookableDatesCached|fetchShowtimes'
ast-grep outline src/bms.ts --items all --match 'fetchShowtimes|fetchPage'

# Inspect whether fetchShowtimes or fetchPage already deduplicates in-flight requests.
rg -n -C 5 'checkSubscription|bookableDatesCached|fetchShowtimes|fetchPage|Promise<|new Map' \
  src/index.ts src/bms.ts

Repository: Ishannaik/seatsniper

Length of output: 11338


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the complete cycle-cache implementation and the poll scheduling.
sed -n '360,410p' src/bms.ts
sed -n '175,255p' src/index.ts
sed -n '430,490p' src/index.ts
rg -n -C 4 'beginCycle\(|Promise\.all|allWatches\(|checkWatch\(' src/index.ts src/*.test.ts tests 2>/dev/null || true

# Check whether any shared cache covers date-specific fetchShowtimes calls.
rg -n -C 3 'fetchShowtimes\(|cycle\.|Map<string, Promise|Cached' src/index.ts src/bms.ts

Repository: Ishannaik/seatsniper

Length of output: 12913


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Use a small read-only verifier to model the request keys used by the current
# implementation for two watches that share a movie but have filter-specific
# fresh dates.
python3 - <<'PY'
from pathlib import Path
src = Path("src/index.ts").read_text()
bms = Path("src/bms.ts").read_text()

print("direct_date_fetches_in_index", src.count("await fetchShowtimes({ city: w.city, slug: w.slug, eventCode: w.event_code, date: d })"))
print("date_fetch_cache_key_present", "date: d" in bms and "cycle.get" in bms)
print("begin_cycle_calls", src.count("beginCycle()"))
PY

Repository: Ishannaik/seatsniper

Length of output: 244


Coalesce fresh-date showtime lookups across watches.

Line 232 calls fetchShowtimes directly and bypasses the existing per-cycle cache. Multiple watches for the same movie and date can therefore issue duplicate BookMyShow requests. Add a shared promise cache keyed by city, event code, and date.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.ts` around lines 224 - 235, Add a per-cycle shared promise cache
for showtime lookups keyed by city, event code, and date, and update the
fresh-date filtering logic around fetchShowtimes to reuse the cached promise
instead of calling fetchShowtimes directly. Ensure watches with identical lookup
keys share one request while distinct keys remain independent.

Source: Coding guidelines

@Ishannaik Ishannaik left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theatre filter is a real user need and the semantics are documented well (substring on name or code; new-cinema alerts are theatre-filtered unlike format). Tests for empty comma entries (no wildcard) are the important ones.

Watch:

  • gh shows src/filters.ts as a binary diff (0/0). That usually means CRLF or encoding. Please make it a normal text patch so review/rebase with #84 is possible.

  • Substring theatre:IMAX will match any venue whose name contains IMAX even when the user meant the IMAX format. Worth a README note.

  • Conflicts with #84 on the same addWatch / poll paths — rebase after whichever lands first.

@Ishannaik Ishannaik left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor src/filters.ts shows as a binary diff (0/0). That is CRLF or encoding. Make it a text patch so rebase with #84 is possible.

minor theatre:IMAX is a substring on venue name, so it also matches IMAX-named cinemas when the user meant the format. One README line is enough.

VERDICT: APPROVE
TESTS: empty comma entries (no wildcard) and name/code match. CI green.

@Ishannaik

Copy link
Copy Markdown
Owner

minor rebase needed: #84 just landed on main. Same �ddWatch / ilterSummary / subscribeToMovie paths.

"Notify me when PVR Phoenix opens" was the one filter SeatSniper could not
express. It already parsed venueCode/venueName and alerted when a new cinema
appeared, but there was no way to say "only these cinemas".

Adds `theatre:` alongside `format:` and `days:`, following the same shape:
normalise on input, persist in its own column (ALTER TABLE + catch), show in
/list and in the DM filter line.

matchesTheatre is a case-insensitive substring against the venue name *and*
the venue code, so `theatre:PVR` catches "PVR: Phoenix Palladium" and a user
who pasted `IMOB` still matches. Whitespace is collapsed on both sides. An
empty filter entry (a stray trailing comma) never matches, so it cannot
silently become a wildcard.

Unlike format:/days:, this one also filters the **new-cinema** half of a
subscription. That half is a deliberate gap for format:/days: because checking
them needs a showtimes fetch per venue plus coalescing; a venue's name and code
are already in hand from the response the diff is computed from, so filtering
it there costs nothing. README's caveat section is updated to say so.

On the fresh-dates path the theatre check rides the existing per-date
fetchShowtimes rather than adding a second one, so combining format: and
theatre: does not double the requests.
@dchaudhari7177

Copy link
Copy Markdown
Contributor Author

Rebased onto current main#84's time-of-day window and this theatre filter now coexist rather than replacing each other.

The union was mechanical in most places (Watch fields, the ALTER TABLE migration, addWatch, both armed* summaries, subscribeToMovie's signature), but two spots were real merges rather than concatenation:

  • checkOne's fresh-date pass — feat(watch): time-of-day filter with after and before #84 had renamed the format-only gate to needsShowtimes, so theatre_filter joins that same boolean instead of reintroducing a second condition. All three filters still share one fetchShowtimes per fresh date; adding theatre: does not add a request.
  • The single-date show filter now applies the time window and the venue check in sequence.

Also fixed something that was my own bug, not a conflict: matchesTheatre joined the venue name and code with a literal NUL byte in the source, which made git classify src/filters.ts as binary — that's why this rebase reported "Cannot merge binary files" and dropped the file wholesale. It's now the \u0000 escape, same runtime behaviour, and the file diffs as text again.

bun test: 110 pass, 0 fail. tsc --noEmit reports only the two pre-existing db.test.ts possibly 'undefined' errors that are already on main (from 402550b) — nothing new from this branch.

@Ishannaik
Ishannaik merged commit e903877 into Ishannaik:main Aug 31, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants