tixbit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +237 -0
- package/dist/cli.js +646 -0
- package/dist/index.d.ts +267 -0
- package/dist/index.js +317 -0
- package/package.json +68 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/client.ts
|
|
7
|
+
var DEFAULT_BASE_URL = "https://tixbit.com";
|
|
8
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
9
|
+
var USER_AGENT = "@tixbit/sdk";
|
|
10
|
+
var TixBitClient = class {
|
|
11
|
+
baseUrl;
|
|
12
|
+
timeoutMs;
|
|
13
|
+
apiKey;
|
|
14
|
+
constructor(config = {}) {
|
|
15
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
16
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
17
|
+
this.apiKey = config.apiKey;
|
|
18
|
+
}
|
|
19
|
+
// ── HTTP helpers ──────────────────────────────────────────────────────────
|
|
20
|
+
async request(path, init) {
|
|
21
|
+
const url = `${this.baseUrl}${path}`;
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
24
|
+
const headers = {
|
|
25
|
+
Accept: "application/json",
|
|
26
|
+
"User-Agent": USER_AGENT,
|
|
27
|
+
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch(url, {
|
|
31
|
+
...init,
|
|
32
|
+
headers: { ...headers, ...init?.headers },
|
|
33
|
+
signal: controller.signal
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const text = await res.text().catch(() => "");
|
|
37
|
+
throw new TixBitApiError(
|
|
38
|
+
`${res.status} ${res.statusText}: ${text.slice(0, 300)}`,
|
|
39
|
+
res.status,
|
|
40
|
+
url
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return await res.json();
|
|
44
|
+
} finally {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
qs(params) {
|
|
49
|
+
const entries = Object.entries(params).filter(([, v]) => v !== void 0 && v !== null && v !== "").map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
|
50
|
+
return entries.length ? `?${entries.join("&")}` : "";
|
|
51
|
+
}
|
|
52
|
+
// ── Search Events ─────────────────────────────────────────────────────────
|
|
53
|
+
/**
|
|
54
|
+
* Search for events by keyword, city, state, category, or date range.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* const results = await client.searchEvents({ query: "Hawks", state: "GA" });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
async searchEvents(params = {}) {
|
|
62
|
+
const query = this.qs({
|
|
63
|
+
q: params.query,
|
|
64
|
+
city: params.city,
|
|
65
|
+
state: params.state,
|
|
66
|
+
category: params.category,
|
|
67
|
+
startDate: params.startDate,
|
|
68
|
+
endDate: params.endDate,
|
|
69
|
+
page: params.page,
|
|
70
|
+
size: params.size ?? 25
|
|
71
|
+
});
|
|
72
|
+
const data = await this.request(`/api/events/search${query}`);
|
|
73
|
+
return {
|
|
74
|
+
events: (data.events ?? []).map(normalizeEvent),
|
|
75
|
+
pagination: data.pagination
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// ── Browse (Location-Aware) ───────────────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* Browse upcoming events near a location (homepage-style).
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* const nearby = await client.browse({ city: "Atlanta", state: "GA" });
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
async browse(params = {}) {
|
|
88
|
+
const query = this.qs({
|
|
89
|
+
size: params.size ?? 18,
|
|
90
|
+
context: "homepage",
|
|
91
|
+
recommendation: "upcoming",
|
|
92
|
+
nearLat: params.latitude,
|
|
93
|
+
nearLng: params.longitude,
|
|
94
|
+
preferCity: params.city,
|
|
95
|
+
preferState: params.state,
|
|
96
|
+
categoryEventType: params.categoryEventType
|
|
97
|
+
});
|
|
98
|
+
const data = await this.request(`/api/events${query}`);
|
|
99
|
+
return {
|
|
100
|
+
events: (data.events ?? []).map(normalizeEvent),
|
|
101
|
+
total: data.total
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// ── Listings ──────────────────────────────────────────────────────────────
|
|
105
|
+
/**
|
|
106
|
+
* Get available ticket listings for an event.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* const listings = await client.getListings({ eventId: "abc123" });
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
async getListings(params) {
|
|
114
|
+
const query = this.qs({
|
|
115
|
+
size: params.size ?? 50,
|
|
116
|
+
page: params.page ?? 1,
|
|
117
|
+
order_by_direction: params.orderByDirection ?? "asc"
|
|
118
|
+
});
|
|
119
|
+
const data = await this.request(`/api/events/${encodeURIComponent(params.eventId)}/listings${query}`);
|
|
120
|
+
const listings = (data.data ?? []).map(normalizeListing);
|
|
121
|
+
return {
|
|
122
|
+
listings,
|
|
123
|
+
meta: {
|
|
124
|
+
total: data.meta?.total ?? listings.length,
|
|
125
|
+
page: data.meta?.page ?? params.page ?? 1,
|
|
126
|
+
size: data.meta?.size ?? params.size ?? 50,
|
|
127
|
+
cacheSource: data.meta?.cacheSource
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// ── Checkout Link ──────────────────────────────────────────────────────────
|
|
132
|
+
/**
|
|
133
|
+
* Create a checkout link for a listing.
|
|
134
|
+
*
|
|
135
|
+
* Returns a URL to the TixBit checkout page where the user can
|
|
136
|
+
* sign in and complete their purchase (card, crypto, etc.).
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* const link = client.createCheckoutLink({
|
|
141
|
+
* listingId: "P2JO5OBX",
|
|
142
|
+
* quantity: 2,
|
|
143
|
+
* });
|
|
144
|
+
*
|
|
145
|
+
* console.log(link.url);
|
|
146
|
+
* // → "https://tixbit.com/checkout/process?listing=P2JO5OBX&quantity=2"
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
createCheckoutLink(params) {
|
|
150
|
+
const quantity = Math.max(1, Math.min(8, Math.round(params.quantity)));
|
|
151
|
+
const url = `${this.baseUrl}/checkout/process?listing=${encodeURIComponent(params.listingId)}&quantity=${quantity}`;
|
|
152
|
+
return {
|
|
153
|
+
url,
|
|
154
|
+
listingId: params.listingId,
|
|
155
|
+
quantity
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
// ── Event URL helper ──────────────────────────────────────────────────────
|
|
159
|
+
/**
|
|
160
|
+
* Build the direct URL to an event page on tixbit.com.
|
|
161
|
+
*/
|
|
162
|
+
eventUrl(slugOrId) {
|
|
163
|
+
return `${this.baseUrl}/events/${slugOrId}`;
|
|
164
|
+
}
|
|
165
|
+
// ── Seatmap ───────────────────────────────────────────────────────────────
|
|
166
|
+
/**
|
|
167
|
+
* Get the seating chart for an event's venue.
|
|
168
|
+
*
|
|
169
|
+
* Returns section-level data including section names, positions, and
|
|
170
|
+
* the venue background image URL. Use this to help users understand
|
|
171
|
+
* where their tickets are located.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```ts
|
|
175
|
+
* const seatmap = await client.getSeatmap({ eventId: "4BKJMDZ" });
|
|
176
|
+
* console.log(seatmap.venue_name); // "State Farm Arena"
|
|
177
|
+
* console.log(seatmap.section_names); // ["101", "102", ...]
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
async getSeatmap(params) {
|
|
181
|
+
const data = await this.request(`/api/events/${encodeURIComponent(params.eventId)}/seating-chart`);
|
|
182
|
+
let zones = [];
|
|
183
|
+
let sectionNames = [];
|
|
184
|
+
if (data.has_coordinates && data.coordinates) {
|
|
185
|
+
try {
|
|
186
|
+
zones = await this.fetchAndParseCoordinates(data.coordinates);
|
|
187
|
+
sectionNames = zones.flatMap(
|
|
188
|
+
(z) => z.sections.map((s) => s.name)
|
|
189
|
+
);
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
success: data.success,
|
|
195
|
+
event_id: data.event_id,
|
|
196
|
+
venue_id: data.venue_id,
|
|
197
|
+
venue_name: data.venue_name,
|
|
198
|
+
configuration_id: data.configuration_id,
|
|
199
|
+
configuration_name: data.configuration_name,
|
|
200
|
+
background_image: data.background_image ?? null,
|
|
201
|
+
coordinates_url: data.coordinates ?? null,
|
|
202
|
+
has_coordinates: data.has_coordinates,
|
|
203
|
+
capacity: data.capacity ?? null,
|
|
204
|
+
venue: data.venue_data,
|
|
205
|
+
zones,
|
|
206
|
+
section_names: sectionNames
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Fetch the coordinates JSON and parse it into zones/sections.
|
|
211
|
+
*/
|
|
212
|
+
async fetchAndParseCoordinates(coordinatesUrl) {
|
|
213
|
+
const fullUrl = coordinatesUrl.startsWith("http") ? coordinatesUrl : `${this.baseUrl}${coordinatesUrl}`;
|
|
214
|
+
const controller = new AbortController();
|
|
215
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
216
|
+
try {
|
|
217
|
+
const res = await fetch(fullUrl, {
|
|
218
|
+
headers: {
|
|
219
|
+
Accept: "application/json",
|
|
220
|
+
"User-Agent": USER_AGENT
|
|
221
|
+
},
|
|
222
|
+
signal: controller.signal
|
|
223
|
+
});
|
|
224
|
+
if (!res.ok) return [];
|
|
225
|
+
const coords = await res.json();
|
|
226
|
+
if (!coords.zones) return [];
|
|
227
|
+
return coords.zones.map((zone) => ({
|
|
228
|
+
id: zone.id,
|
|
229
|
+
name: zone.name,
|
|
230
|
+
sections: (zone.sections ?? []).map((section) => {
|
|
231
|
+
const label = section.labels?.[0];
|
|
232
|
+
return {
|
|
233
|
+
id: section.id,
|
|
234
|
+
name: section.name,
|
|
235
|
+
x: label?.x ?? 0,
|
|
236
|
+
y: label?.y ?? 0
|
|
237
|
+
};
|
|
238
|
+
})
|
|
239
|
+
}));
|
|
240
|
+
} finally {
|
|
241
|
+
clearTimeout(timer);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
function normalizeEvent(raw) {
|
|
246
|
+
const e = raw;
|
|
247
|
+
return {
|
|
248
|
+
id: str(e.id) ?? str(e.external_event_id) ?? "",
|
|
249
|
+
external_event_id: str(e.external_event_id) ?? str(e.externalEventId) ?? str(e.id) ?? "",
|
|
250
|
+
slug: str(e.slug) ?? str(e.external_event_id) ?? "",
|
|
251
|
+
name: str(e.name) ?? str(e.performer) ?? "",
|
|
252
|
+
date: resolveDate(e),
|
|
253
|
+
venue_name: str(e.venue_name) ?? str(e.venueName) ?? null,
|
|
254
|
+
venue_city: str(e.venue_city) ?? str(e.venueCity) ?? null,
|
|
255
|
+
venue_state: str(e.venue_state) ?? str(e.venueState) ?? null,
|
|
256
|
+
image_url: str(e.image_url) ?? str(e.imageUrl) ?? null,
|
|
257
|
+
category_name: str(e.category_name) ?? str(e.categoryName) ?? null,
|
|
258
|
+
category_event_type: str(e.category_event_type) ?? str(e.categoryEventType) ?? null,
|
|
259
|
+
has_listings: Boolean(e.has_listings),
|
|
260
|
+
inventory: normalizeInventory(e.inventory)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function normalizeInventory(raw) {
|
|
264
|
+
if (!raw || typeof raw !== "object") {
|
|
265
|
+
return { total_available: 0, min_price: 0, max_price: 0 };
|
|
266
|
+
}
|
|
267
|
+
const inv = raw;
|
|
268
|
+
return {
|
|
269
|
+
total_available: num(inv.total_available) ?? 0,
|
|
270
|
+
min_price: num(inv.min_price) ?? 0,
|
|
271
|
+
max_price: num(inv.max_price) ?? 0
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function normalizeListing(raw) {
|
|
275
|
+
const outer = raw;
|
|
276
|
+
const attrs = outer.attributes ?? outer;
|
|
277
|
+
return {
|
|
278
|
+
id: str(outer.id) ?? str(attrs.id) ?? "",
|
|
279
|
+
// price_per_ticket is the fee-inclusive per-ticket price (in dollars, not cents)
|
|
280
|
+
price: num(attrs.price_per_ticket) ?? num(attrs.price) ?? 0,
|
|
281
|
+
quantity: num(attrs.quantity) ?? num(attrs.available_quantity) ?? 0,
|
|
282
|
+
quantities_list: Array.isArray(attrs.quantities_list) ? attrs.quantities_list : [],
|
|
283
|
+
section: str(attrs.section) ?? null,
|
|
284
|
+
row: str(attrs.row) ?? null,
|
|
285
|
+
seat_numbers: str(attrs.seat_numbers) ?? null,
|
|
286
|
+
listing_hash: str(attrs.listing_hash) ?? "",
|
|
287
|
+
notes: str(attrs.notes) ?? null,
|
|
288
|
+
delivery_method: str(attrs.delivery_method) ?? str(attrs.delivery_type) ?? null,
|
|
289
|
+
splits: Array.isArray(attrs.splits) ? attrs.splits : [],
|
|
290
|
+
raw: outer
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function resolveDate(e) {
|
|
294
|
+
if (typeof e.date === "string") return e.date;
|
|
295
|
+
if (typeof e.date === "number") return new Date(e.date).toISOString();
|
|
296
|
+
if (e.date && typeof e.date === "object") {
|
|
297
|
+
const d = e.date;
|
|
298
|
+
if (d.month && d.day && d.year) {
|
|
299
|
+
return `${d.month} ${d.day}, ${d.year}`;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (typeof e.date_ms === "number") return new Date(e.date_ms).toISOString();
|
|
303
|
+
return "";
|
|
304
|
+
}
|
|
305
|
+
function str(v) {
|
|
306
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
307
|
+
}
|
|
308
|
+
function num(v) {
|
|
309
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
310
|
+
}
|
|
311
|
+
var TixBitApiError = class extends Error {
|
|
312
|
+
constructor(message, status, url) {
|
|
313
|
+
super(message);
|
|
314
|
+
this.status = status;
|
|
315
|
+
this.url = url;
|
|
316
|
+
this.name = "TixBitApiError";
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// src/cli.ts
|
|
321
|
+
var client = new TixBitClient({
|
|
322
|
+
baseUrl: process.env.TIXBIT_BASE_URL,
|
|
323
|
+
apiKey: process.env.TIXBIT_API_KEY
|
|
324
|
+
});
|
|
325
|
+
function output(data, json) {
|
|
326
|
+
if (json) {
|
|
327
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (Array.isArray(data)) {
|
|
331
|
+
for (const item of data) {
|
|
332
|
+
process.stdout.write(formatItem(item) + "\n");
|
|
333
|
+
}
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
337
|
+
}
|
|
338
|
+
function formatItem(item) {
|
|
339
|
+
if (!item || typeof item !== "object") return String(item);
|
|
340
|
+
const e = item;
|
|
341
|
+
if (e.name && e.external_event_id) {
|
|
342
|
+
const parts = [
|
|
343
|
+
` ${e.name}`,
|
|
344
|
+
` ID: ${e.external_event_id}`
|
|
345
|
+
];
|
|
346
|
+
if (e.date) parts.push(` Date: ${e.date}`);
|
|
347
|
+
const location = [e.venue_city, e.venue_state].filter(Boolean).join(", ");
|
|
348
|
+
if (location) parts.push(` Location: ${location}`);
|
|
349
|
+
if (e.venue_name) parts.push(` Venue: ${e.venue_name}`);
|
|
350
|
+
if (e.has_listings) {
|
|
351
|
+
const inv = e.inventory;
|
|
352
|
+
if (inv?.min_price) {
|
|
353
|
+
parts.push(` From: $${inv.min_price.toFixed(2)} (${inv.total_available} available)`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return parts.join("\n") + "\n";
|
|
357
|
+
}
|
|
358
|
+
if (e.listing_hash !== void 0) {
|
|
359
|
+
const parts = [
|
|
360
|
+
` $${e.price.toFixed(2)} \xD7 ${e.quantity} ticket(s)`,
|
|
361
|
+
` ID: ${e.id}`
|
|
362
|
+
];
|
|
363
|
+
if (e.section) parts.push(` Section: ${e.section}${e.row ? ` Row ${e.row}` : ""}`);
|
|
364
|
+
if (e.delivery_method) parts.push(` Delivery: ${e.delivery_method}`);
|
|
365
|
+
if (e.quantities_list?.length > 0) {
|
|
366
|
+
parts.push(` Qty options: ${e.quantities_list.join(", ")}`);
|
|
367
|
+
}
|
|
368
|
+
return parts.join("\n") + "\n";
|
|
369
|
+
}
|
|
370
|
+
return JSON.stringify(item, null, 2);
|
|
371
|
+
}
|
|
372
|
+
function handleError(err) {
|
|
373
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
374
|
+
process.stderr.write(`Error: ${msg}
|
|
375
|
+
`);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
var program = new Command().name("tixbit").description("Search events, browse listings, and buy tickets on TixBit").version("0.1.0");
|
|
379
|
+
program.command("search [query]").description("Search for events by keyword, city, state, or category").option("--city <city>", "Filter by city").option("--state <state>", "Filter by state (2-letter code)").option("--category <slug>", "Filter by category slug (e.g. nba-basketball)").option("--start-date <date>", "Events on or after this date (ISO)").option("--end-date <date>", "Events on or before this date (ISO)").option("--page <n>", "Page number", "1").option("--size <n>", "Results per page", "10").option("--json", "Output raw JSON (for agents)", false).action(async (query, opts) => {
|
|
380
|
+
try {
|
|
381
|
+
const params = {
|
|
382
|
+
query,
|
|
383
|
+
city: opts.city,
|
|
384
|
+
state: opts.state,
|
|
385
|
+
category: opts.category,
|
|
386
|
+
startDate: opts.startDate,
|
|
387
|
+
endDate: opts.endDate,
|
|
388
|
+
page: parseInt(opts.page, 10),
|
|
389
|
+
size: parseInt(opts.size, 10)
|
|
390
|
+
};
|
|
391
|
+
const result = await client.searchEvents(params);
|
|
392
|
+
const isJson = opts.json === true || opts.json === "true";
|
|
393
|
+
if (isJson) {
|
|
394
|
+
output(result, true);
|
|
395
|
+
} else {
|
|
396
|
+
const { pagination } = result;
|
|
397
|
+
process.stdout.write(
|
|
398
|
+
`
|
|
399
|
+
Found ${pagination.total} event(s) \u2014 page ${pagination.page}/${pagination.totalPages}
|
|
400
|
+
|
|
401
|
+
`
|
|
402
|
+
);
|
|
403
|
+
output(result.events, false);
|
|
404
|
+
}
|
|
405
|
+
} catch (err) {
|
|
406
|
+
handleError(err);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
program.command("browse").description("Browse upcoming events near a location").option("--city <city>", "Preferred city").option("--state <state>", "Preferred state (2-letter code)").option("--lat <lat>", "Latitude").option("--lng <lng>", "Longitude").option("--category <type>", "SPORT, CONCERT, THEATER, or ALL", "ALL").option("--size <n>", "Number of results", "10").option("--json", "Output raw JSON (for agents)", false).action(async (opts) => {
|
|
410
|
+
try {
|
|
411
|
+
const params = {
|
|
412
|
+
city: opts.city,
|
|
413
|
+
state: opts.state,
|
|
414
|
+
latitude: opts.lat ? parseFloat(opts.lat) : void 0,
|
|
415
|
+
longitude: opts.lng ? parseFloat(opts.lng) : void 0,
|
|
416
|
+
categoryEventType: opts.category ?? "ALL",
|
|
417
|
+
size: parseInt(opts.size, 10)
|
|
418
|
+
};
|
|
419
|
+
const result = await client.browse(params);
|
|
420
|
+
const isJson = opts.json === true || opts.json === "true";
|
|
421
|
+
if (isJson) {
|
|
422
|
+
output(result, true);
|
|
423
|
+
} else {
|
|
424
|
+
process.stdout.write(
|
|
425
|
+
`
|
|
426
|
+
${result.events.length} upcoming event(s) near ${opts.city ?? "you"}
|
|
427
|
+
|
|
428
|
+
`
|
|
429
|
+
);
|
|
430
|
+
output(result.events, false);
|
|
431
|
+
}
|
|
432
|
+
} catch (err) {
|
|
433
|
+
handleError(err);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
program.command("listings <eventId>").description("Get available ticket listings for an event").option("--size <n>", "Results per page", "20").option("--page <n>", "Page number", "1").option("--sort <dir>", "Price sort: asc or desc", "asc").option("--json", "Output raw JSON (for agents)", false).action(async (eventId, opts) => {
|
|
437
|
+
try {
|
|
438
|
+
const params = {
|
|
439
|
+
eventId,
|
|
440
|
+
size: parseInt(opts.size, 10),
|
|
441
|
+
page: parseInt(opts.page, 10),
|
|
442
|
+
orderByDirection: opts.sort
|
|
443
|
+
};
|
|
444
|
+
const result = await client.getListings(params);
|
|
445
|
+
const isJson = opts.json === true || opts.json === "true";
|
|
446
|
+
if (isJson) {
|
|
447
|
+
output(result, true);
|
|
448
|
+
} else {
|
|
449
|
+
process.stdout.write(
|
|
450
|
+
`
|
|
451
|
+
${result.listings.length} listing(s) for event ${eventId}
|
|
452
|
+
|
|
453
|
+
`
|
|
454
|
+
);
|
|
455
|
+
output(result.listings, false);
|
|
456
|
+
}
|
|
457
|
+
} catch (err) {
|
|
458
|
+
handleError(err);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
program.command("checkout <listingId>").description("Get a checkout link to buy tickets for a listing").requiredOption("--quantity <n>", "Number of tickets to buy").option("--json", "Output raw JSON", false).action(async (listingId, opts) => {
|
|
462
|
+
try {
|
|
463
|
+
const quantity = parseInt(opts.quantity, 10);
|
|
464
|
+
if (!Number.isFinite(quantity) || quantity < 1) {
|
|
465
|
+
process.stderr.write("Error: --quantity must be a positive integer\n");
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
const isJson = opts.json === true || opts.json === "true";
|
|
469
|
+
let listingInfo;
|
|
470
|
+
try {
|
|
471
|
+
const result = await client.getListings({ eventId: listingId, size: 1 });
|
|
472
|
+
if (result.listings.length > 0) {
|
|
473
|
+
listingInfo = result.listings.find((l) => l.id === listingId);
|
|
474
|
+
}
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
const link = client.createCheckoutLink({ listingId, quantity });
|
|
478
|
+
if (isJson) {
|
|
479
|
+
output({ ...link, listing: listingInfo ?? null }, true);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
process.stdout.write("\n\u{1F39F} Checkout Link\n\n");
|
|
483
|
+
if (listingInfo) {
|
|
484
|
+
process.stdout.write(` Listing: ${listingInfo.id}
|
|
485
|
+
`);
|
|
486
|
+
if (listingInfo.section) {
|
|
487
|
+
process.stdout.write(
|
|
488
|
+
` Section: ${listingInfo.section}${listingInfo.row ? ` Row ${listingInfo.row}` : ""}
|
|
489
|
+
`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
process.stdout.write(
|
|
493
|
+
` Price: $${listingInfo.price.toFixed(2)} \xD7 ${quantity} = $${(listingInfo.price * quantity).toFixed(2)}
|
|
494
|
+
`
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
process.stdout.write(` Quantity: ${quantity}
|
|
498
|
+
|
|
499
|
+
`);
|
|
500
|
+
process.stdout.write(` ${link.url}
|
|
501
|
+
|
|
502
|
+
`);
|
|
503
|
+
process.stdout.write(
|
|
504
|
+
" Open the link above in your browser to complete checkout.\n\n"
|
|
505
|
+
);
|
|
506
|
+
} catch (err) {
|
|
507
|
+
handleError(err);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
program.command("seatmap <eventId>").description("Show the seating chart / section map for an event's venue").option("--section <name>", "Highlight a specific section (case-insensitive)").option("--json", "Output raw JSON (for agents)", false).action(async (eventId, opts) => {
|
|
511
|
+
try {
|
|
512
|
+
const result = await client.getSeatmap({ eventId });
|
|
513
|
+
const isJson = opts.json === true || opts.json === "true";
|
|
514
|
+
if (isJson) {
|
|
515
|
+
output(result, true);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (!result.success) {
|
|
519
|
+
process.stderr.write("Seatmap not available for this event.\n");
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
process.stdout.write(`
|
|
523
|
+
\u{1F3DF} ${result.venue_name}
|
|
524
|
+
`);
|
|
525
|
+
process.stdout.write(` ${result.configuration_name}
|
|
526
|
+
`);
|
|
527
|
+
if (result.venue.address) {
|
|
528
|
+
process.stdout.write(` ${result.venue.address}, ${result.venue.city}, ${result.venue.region}
|
|
529
|
+
`);
|
|
530
|
+
}
|
|
531
|
+
if (result.capacity) {
|
|
532
|
+
process.stdout.write(` Capacity: ${result.capacity.toLocaleString()}
|
|
533
|
+
`);
|
|
534
|
+
}
|
|
535
|
+
process.stdout.write("\n");
|
|
536
|
+
if (!result.has_coordinates || result.zones.length === 0) {
|
|
537
|
+
process.stdout.write(" No section-level seating data available for this venue.\n\n");
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const groups = categorizeSections(result.zones.flatMap((z) => z.sections));
|
|
541
|
+
const highlightSection = opts.section?.toUpperCase();
|
|
542
|
+
for (const [groupName, sections] of Object.entries(groups)) {
|
|
543
|
+
process.stdout.write(` \u2500\u2500 ${groupName} \u2500\u2500
|
|
544
|
+
`);
|
|
545
|
+
const names = sections.map((s) => s.name);
|
|
546
|
+
for (let i = 0; i < names.length; i += 8) {
|
|
547
|
+
const row = names.slice(i, i + 8);
|
|
548
|
+
const formatted = row.map((name) => {
|
|
549
|
+
if (highlightSection && name.toUpperCase() === highlightSection) {
|
|
550
|
+
return ` \u25B8${name}\u25C2 `;
|
|
551
|
+
}
|
|
552
|
+
return ` ${name} `;
|
|
553
|
+
}).join(" ");
|
|
554
|
+
process.stdout.write(` ${formatted}
|
|
555
|
+
`);
|
|
556
|
+
}
|
|
557
|
+
process.stdout.write("\n");
|
|
558
|
+
}
|
|
559
|
+
if (highlightSection) {
|
|
560
|
+
const allSections = result.zones.flatMap((z) => z.sections);
|
|
561
|
+
const match = allSections.find(
|
|
562
|
+
(s) => s.name.toUpperCase() === highlightSection
|
|
563
|
+
);
|
|
564
|
+
if (match) {
|
|
565
|
+
const pos = describePosition(match, allSections);
|
|
566
|
+
process.stdout.write(` \u{1F4CD} Section ${match.name}: ${pos}
|
|
567
|
+
|
|
568
|
+
`);
|
|
569
|
+
} else {
|
|
570
|
+
process.stdout.write(` \u26A0 Section "${opts.section}" not found in this venue.
|
|
571
|
+
`);
|
|
572
|
+
process.stdout.write(` Available: ${result.section_names.slice(0, 20).join(", ")}${result.section_names.length > 20 ? "..." : ""}
|
|
573
|
+
|
|
574
|
+
`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
process.stdout.write(
|
|
578
|
+
` Total sections: ${result.section_names.length}
|
|
579
|
+
|
|
580
|
+
`
|
|
581
|
+
);
|
|
582
|
+
} catch (err) {
|
|
583
|
+
handleError(err);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
program.command("url <slug>").description("Print the TixBit event page URL for a slug or ID").action((slug) => {
|
|
587
|
+
process.stdout.write(client.eventUrl(slug) + "\n");
|
|
588
|
+
});
|
|
589
|
+
function categorizeSections(sections) {
|
|
590
|
+
const groups = {};
|
|
591
|
+
const addToGroup = (groupName, section) => {
|
|
592
|
+
if (!groups[groupName]) groups[groupName] = [];
|
|
593
|
+
groups[groupName].push(section);
|
|
594
|
+
};
|
|
595
|
+
const hasLowerLevel = sections.some((s) => /^1\d{2}/.test(s.name));
|
|
596
|
+
const hasSmallNumbers = sections.some((s) => /^\d{1,2}$/.test(s.name));
|
|
597
|
+
for (const section of sections) {
|
|
598
|
+
const name = section.name.toUpperCase();
|
|
599
|
+
if (/^FLOOR\d/.test(name) || /^FLR\d/.test(name)) {
|
|
600
|
+
addToGroup("\u{1F3C0} Floor", section);
|
|
601
|
+
} else if (/^1\d{2}/.test(name)) {
|
|
602
|
+
addToGroup("\u2B07 Lower Level (100s)", section);
|
|
603
|
+
} else if (/^2\d{2}/.test(name)) {
|
|
604
|
+
addToGroup("\u2B06 Upper Level (200s)", section);
|
|
605
|
+
} else if (/^3\d{2}/.test(name)) {
|
|
606
|
+
addToGroup("\u{1F535} 300 Level", section);
|
|
607
|
+
} else if (/^4\d{2}/.test(name)) {
|
|
608
|
+
addToGroup("\u{1F7E3} 400 Level", section);
|
|
609
|
+
} else if (/^\d{1,2}$/.test(name) && hasSmallNumbers) {
|
|
610
|
+
addToGroup(hasLowerLevel ? "\u{1F3DF} Field Level" : "\u2B07 Lower Level", section);
|
|
611
|
+
} else if (/^L\d/.test(name)) {
|
|
612
|
+
addToGroup("\u{1FAB5} Loge", section);
|
|
613
|
+
} else if (/^S\d/.test(name)) {
|
|
614
|
+
addToGroup("\u2601 Sky", section);
|
|
615
|
+
} else if (/^T\d/.test(name)) {
|
|
616
|
+
addToGroup("\u{1F307} Terrace", section);
|
|
617
|
+
} else if (/^V\d/.test(name)) {
|
|
618
|
+
addToGroup("\u{1F440} Vista", section);
|
|
619
|
+
} else if (/^STE/.test(name)) {
|
|
620
|
+
addToGroup("\u{1F3E2} Suites", section);
|
|
621
|
+
} else if (name.includes("SUITE") || name.includes("SUITES")) {
|
|
622
|
+
addToGroup("\u{1F3E2} Suites", section);
|
|
623
|
+
} else if (name.includes("STANDING") || name === "SRO" || name === "UPPER") {
|
|
624
|
+
addToGroup("\u{1F9CD} Standing Room", section);
|
|
625
|
+
} else if (name === "DECK" || name === "ROOF" || name === "GA" || name === "HAT") {
|
|
626
|
+
addToGroup("\u{1F3AA} General / Special", section);
|
|
627
|
+
} else {
|
|
628
|
+
addToGroup("\u{1F4CD} Other", section);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return groups;
|
|
632
|
+
}
|
|
633
|
+
function describePosition(section, allSections) {
|
|
634
|
+
if (!allSections.length) return "position unknown";
|
|
635
|
+
const centerX = allSections.reduce((sum, s) => sum + s.x, 0) / allSections.length;
|
|
636
|
+
const centerY = allSections.reduce((sum, s) => sum + s.y, 0) / allSections.length;
|
|
637
|
+
const dx = section.x - centerX;
|
|
638
|
+
const dy = section.y - centerY;
|
|
639
|
+
const horizontal = Math.abs(dx) < 50 ? "center" : dx < 0 ? "left side" : "right side";
|
|
640
|
+
const vertical = Math.abs(dy) < 50 ? "center" : dy < 0 ? "near side (closer to stage/court)" : "far side";
|
|
641
|
+
if (horizontal === "center" && vertical === "center") {
|
|
642
|
+
return "center of venue";
|
|
643
|
+
}
|
|
644
|
+
return [vertical, horizontal].filter((p) => p !== "center").join(", ");
|
|
645
|
+
}
|
|
646
|
+
program.parse();
|