Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions cli-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3100,6 +3100,176 @@
"modulePath": "booking/search.js",
"sourceFile": "booking/search.js"
},
{
"site": "bookmyshow",
"name": "cinemas",
"description": "BookMyShow cinemas in a city",
"access": "read",
"example": "webcmd bookmyshow cinemas --city mumbai --limit 5",
"domain": "in.bookmyshow.com",
"strategy": "ui",
"browser": true,
"args": [
{
"name": "city",
"type": "string",
"default": "mumbai",
"required": false,
"help": "BookMyShow city name or slug"
},
{
"name": "limit",
"type": "int",
"default": 10,
"required": false,
"help": "Number of cinemas to return (max 20)"
}
],
"columns": [
"rank",
"name",
"address",
"city",
"url"
],
"type": "js",
"modulePath": "bookmyshow/cinemas.js",
"sourceFile": "bookmyshow/cinemas.js",
"navigateBefore": true
},
{
"site": "bookmyshow",
"name": "events",
"description": "BookMyShow events in a city",
"access": "read",
"example": "webcmd bookmyshow events --city mumbai --limit 5",
"domain": "in.bookmyshow.com",
"strategy": "ui",
"browser": true,
"args": [
{
"name": "city",
"type": "string",
"default": "mumbai",
"required": false,
"help": "BookMyShow city name or slug"
},
{
"name": "limit",
"type": "int",
"default": 10,
"required": false,
"help": "Number of events to return (max 20)"
}
],
"columns": [
"rank",
"eventCode",
"title",
"venue",
"category",
"price",
"city",
"url"
],
"type": "js",
"modulePath": "bookmyshow/events.js",
"sourceFile": "bookmyshow/events.js",
"navigateBefore": true
},
{
"site": "bookmyshow",
"name": "movies",
"description": "BookMyShow movies running in a city",
"access": "read",
"example": "webcmd bookmyshow movies --city mumbai --limit 5",
"domain": "in.bookmyshow.com",
"strategy": "ui",
"browser": true,
"args": [
{
"name": "city",
"type": "string",
"default": "mumbai",
"required": false,
"help": "BookMyShow city name or slug"
},
{
"name": "limit",
"type": "int",
"default": 10,
"required": false,
"help": "Number of movies to return (max 20)"
}
],
"columns": [
"rank",
"eventCode",
"title",
"genres",
"city",
"url",
"image"
],
"type": "js",
"modulePath": "bookmyshow/movies.js",
"sourceFile": "bookmyshow/movies.js",
"navigateBefore": true
},
{
"site": "bookmyshow",
"name": "shows",
"description": "BookMyShow movie showtimes in a city",
"access": "read",
"example": "webcmd bookmyshow shows alpha --city mumbai --limit 5",
"domain": "in.bookmyshow.com",
"strategy": "ui",
"browser": true,
"args": [
{
"name": "movie",
"type": "string",
"required": true,
"positional": true,
"help": "Movie title, event code, or BookMyShow movie URL"
},
{
"name": "city",
"type": "string",
"default": "mumbai",
"required": false,
"help": "BookMyShow city name or slug"
},
{
"name": "date",
"type": "string",
"required": false,
"help": "Show date as YYYYMMDD; defaults to today"
},
{
"name": "limit",
"type": "int",
"default": 10,
"required": false,
"help": "Number of showtimes to return (max 20)"
}
],
"columns": [
"rank",
"eventCode",
"movie",
"cinema",
"showTime",
"format",
"status",
"city",
"url"
],
"type": "js",
"modulePath": "bookmyshow/shows.js",
"sourceFile": "bookmyshow/shows.js",
"navigateBefore": true
},
{
"site": "brave",
"name": "search",
Expand Down
115 changes: 115 additions & 0 deletions clis/bookmyshow/bookmyshow.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { describe, expect, it } from 'vitest';
import { ArgumentError, EmptyResultError } from '@agentrhq/webcmd/errors';
import { createPageMock } from '../test-utils.js';
import { __test__ as utils } from './utils.js';
import { __test__ as movies } from './movies.js';
import { __test__ as events } from './events.js';
import { __test__ as cinemas } from './cinemas.js';
import { __test__ as shows } from './shows.js';

const movieRows = [
{
title: 'Alpha',
genres: 'Action/Thriller',
eventCode: 'ET00403805',
url: 'https://in.bookmyshow.com/movies/mumbai/alpha/ET00403805',
image: 'https://assets.example/alpha.jpg',
},
];

describe('bookmyshow utils', () => {
it('normalizes common city names and validates limits', () => {
expect(utils.resolveCity('Delhi NCR')).toMatchObject({ slug: 'national-capital-region-ncr', regionCode: 'NCR' });
expect(utils.resolveCity('Mumbai')).toMatchObject({ slug: 'mumbai', regionCode: 'MUMBAI' });
expect(utils.parseLimit(5, 'bookmyshow movies')).toBe(5);
expect(() => utils.parseLimit(0, 'bookmyshow movies')).toThrow(ArgumentError);
});

it('parses event code and slug from BookMyShow movie URLs', () => {
expect(utils.parseMovieRef('https://in.bookmyshow.com/movies/mumbai/alpha/ET00403805')).toEqual({
eventCode: 'ET00403805',
slug: 'alpha',
});
});
});

describe('bookmyshow movies', () => {
it('returns visible movie cards with stable columns', async () => {
const page = createPageMock([{ ok: true, rows: movieRows }]);
const rows = await movies.command.func(page, { city: 'mumbai', limit: 1 });
expect(page.goto).toHaveBeenCalledWith('https://in.bookmyshow.com/explore/home/mumbai');
expect(rows).toEqual([{
rank: 1,
eventCode: 'ET00403805',
title: 'Alpha',
genres: 'Action/Thriller',
city: 'mumbai',
url: 'https://in.bookmyshow.com/movies/mumbai/alpha/ET00403805',
image: 'https://assets.example/alpha.jpg',
}]);
});

it('throws EmptyResultError when no movie cards are visible', async () => {
const page = createPageMock([{ ok: true, rows: [] }]);
await expect(movies.command.func(page, { city: 'mumbai', limit: 5 })).rejects.toBeInstanceOf(EmptyResultError);
});
});

describe('bookmyshow events', () => {
it('returns visible event cards', async () => {
const page = createPageMock([{ ok: true, rows: [{
title: 'Sunburn Festival',
venue: 'Mahalaxmi Race Course: Mumbai',
category: 'Concerts',
price: '3500 onwards',
eventCode: 'ET00498558',
url: 'https://in.bookmyshow.com/events/sunburn-festival-2026/ET00498558',
}] }]);
const rows = await events.command.func(page, { city: 'mumbai', limit: 1 });
expect(page.goto).toHaveBeenCalledWith('https://in.bookmyshow.com/explore/events-mumbai');
expect(rows[0]).toMatchObject({ rank: 1, title: 'Sunburn Festival', city: 'mumbai' });
});
});

describe('bookmyshow cinemas', () => {
it('pairs visible cinema names with addresses', async () => {
const page = createPageMock([{ ok: true, rows: [{
name: 'Cinepolis: Nexus Seawoods',
address: 'Nerul, Navi Mumbai, Maharashtra 400706, India',
}] }]);
const rows = await cinemas.command.func(page, { city: 'mumbai', limit: 1 });
expect(page.goto).toHaveBeenCalledWith('https://in.bookmyshow.com/mumbai/cinemas');
expect(rows[0]).toEqual({
rank: 1,
name: 'Cinepolis: Nexus Seawoods',
address: 'Nerul, Navi Mumbai, Maharashtra 400706, India',
city: 'mumbai',
url: 'https://in.bookmyshow.com/mumbai/cinemas',
});
});
});

describe('bookmyshow shows', () => {
it('resolves a movie title before extracting visible showtimes', async () => {
const page = createPageMock([
{ ok: true, rows: movieRows },
{ ok: true, rows: [{
movie: 'Alpha',
cinema: 'Ajanta Cinema Cinex: Borivali (W) Newly Renovated',
showTime: '04:15 PM',
format: 'DOLBY 9.5',
status: 'AVAILABLE',
}] },
]);
const rows = await shows.command.func(page, { city: 'mumbai', movie: 'alpha', limit: 1 });
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://in.bookmyshow.com/explore/home/mumbai');
expect(page.goto.mock.calls[1][0]).toMatch('/movies/mumbai/alpha/buytickets/ET00403805/');
expect(rows[0]).toMatchObject({
rank: 1,
eventCode: 'ET00403805',
movie: 'Alpha',
cinema: 'Ajanta Cinema Cinex: Borivali (W) Newly Renovated',
showTime: '04:15 PM',
});
});
});
56 changes: 56 additions & 0 deletions clis/bookmyshow/cinemas.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { cli, Strategy } from '@agentrhq/webcmd/registry';
import { HOST, SITE, addRankAndLimit, openAndExtract, parseLimit, resolveCity } from './utils.js';

export function buildCinemasExtractScript() {
return `(() => {
const clean = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
const root = document.querySelector('[role="grid"]') || document.body;
const leaves = Array.from(root.querySelectorAll('div'))
.filter((el) => !el.children.length)
.map((el) => clean(el.textContent))
.filter(Boolean);
const rows = [];
const seen = new Set();
for (let i = 0; i < leaves.length - 1; i += 1) {
const name = leaves[i];
const address = leaves[i + 1];
if (!name || !address || seen.has(name)) continue;
if (!/\\bIndia\\b|\\bMaharashtra\\b|\\bDelhi\\b|\\bKarnataka\\b|\\bTamil Nadu\\b|\\bTelangana\\b|\\bWest Bengal\\b|\\bGujarat\\b|\\bKerala\\b/i.test(address)) continue;
seen.add(name);
rows.push({ name, address });
i += 1;
}
return { ok: true, rows };
})()`;
}

export const command = cli({
site: SITE,
name: 'cinemas',
access: 'read',
description: 'BookMyShow cinemas in a city',
example: 'webcmd bookmyshow cinemas --city mumbai --limit 5',
domain: 'in.bookmyshow.com',
strategy: Strategy.UI,
browser: true,
args: [
{ name: 'city', type: 'string', default: 'mumbai', help: 'BookMyShow city name or slug' },
{ name: 'limit', type: 'int', default: 10, help: 'Number of cinemas to return (max 20)' },
],
columns: ['rank', 'name', 'address', 'city', 'url'],
func: async (page, kwargs) => {
const city = resolveCity(kwargs.city);
const limit = parseLimit(kwargs.limit, 'bookmyshow cinemas');
const url = `${HOST}/${city.slug}/cinemas`;
const rows = await openAndExtract(page, url, buildCinemasExtractScript(), 'bookmyshow cinemas');
return addRankAndLimit(rows, limit, city.slug, url, (row) => ({
name: row.name || '',
address: row.address || '',
}));
},
});

export const __test__ = {
command,
buildCinemasExtractScript,
};
Loading
Loading