From c68acb318ed0e384577d37d657de26de5200dd75 Mon Sep 17 00:00:00 2001 From: Elijah Lopez Date: Fri, 29 Aug 2025 20:02:37 -0400 Subject: [PATCH] feat(route): add eventbrite (#19007) --- lib/routes/eventbrite/events.ts | 152 +++++++++++++++++++++++++++++ lib/routes/eventbrite/namespace.ts | 7 ++ 2 files changed, 159 insertions(+) create mode 100644 lib/routes/eventbrite/events.ts create mode 100644 lib/routes/eventbrite/namespace.ts diff --git a/lib/routes/eventbrite/events.ts b/lib/routes/eventbrite/events.ts new file mode 100644 index 0000000000..152eb39bab --- /dev/null +++ b/lib/routes/eventbrite/events.ts @@ -0,0 +1,152 @@ +import type { Data, DataItem, Route } from '@/types'; +import { toTitleCase } from '@/utils/common-utils'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { load } from 'cheerio'; +import type { Context } from 'hono'; + +export const route: Route = { + path: '/:region/:eventType?/:includePromoted?', + categories: ['other'], + example: '/eventbrite/canada--toronto/all-events', + parameters: { eventType: 'category of events for filtering', region: 'Region or scope of events' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['eventbrite.com/d/:region/:eventType'], + target: '/:region/:eventType', + }, + { + source: ['eventbrite.ca/d/:region/:eventType'], + target: '/:region/:eventType', + }, + ], + name: 'Events', + maintainers: ['elibroftw'], + handler: async (ctx: Context): Promise => { + const params = ctx.req.param(); + const { region, includePromoted = 'false' } = params; + let eventType = params.eventType || 'all-events'; + if (eventType === 'events') { + eventType = 'all-events'; + } + const link = `https://www.eventbrite.com/d/${region}/${eventType}/`; + const response = await ofetch(link); + const $ = load(response); + const eventsApiUrl = new URL('https://www.eventbrite.com/api/v3/destination/events'); + // exclude promoted events + const includePromotedBool = includePromoted !== 'false'; + const eventListEl = $('div.search-results-panel-content section>ul>li') + .toArray() + .filter((item) => includePromotedBool || !$(item).text().includes('Promoted')); + const eventIds = eventListEl.map((item) => $(item).find('a.event-card-link').first().attr('data-event-id')); + eventsApiUrl.searchParams.append('event_ids', eventIds.join(',')); + eventsApiUrl.searchParams.append('page_size', eventIds.length.toString()); + eventsApiUrl.searchParams.append('expand', 'image,primary_venue,ticket_availability,primary_organizer'); + const eventsData: EventbriteEvent[] = (await ofetch(eventsApiUrl.href)).events; + const items = eventListEl.map((item, index): DataItem => { + const el = $(item); + const a = el.find('.event-card-details a.event-card-link').first(); + const fallbackTitle = a.attr('aria-label') || a.text() || a.toString(); + + const eventData = eventsData[index]; + if (eventData === undefined) { + const pElements = el.find('p'); + return { + title: fallbackTitle, + author: pElements.length > 1 ? $(pElements[1]).text() : undefined, + }; + } + const isPromoted = el.text().includes('Promoted'); + return { + title: isPromoted ? `${eventData.name} (Promoted)` : eventData.name, + link: eventData.url, + pubDate: parseDate(eventData.published), + author: eventData.primary_organizer === undefined ? JSON.stringify(eventData) : eventData.primary_organizer.name, + category: eventData.tags.map((tag) => tag.display_name), + image: eventData.image.original.url, + description: eventData.summary, + id: eventData.eventbrite_event_id, + content: { + html: `${eventDate(eventData)}
${getAuthor(eventData)}
${getTicketPriceRange(eventData)}`, + text: '', + }, + }; + }); + + const formattedEventType = toTitleCase(eventType.replaceAll('-', ' ')); + const formattedRegion = toTitleCase(region.replaceAll('-', ' ')); + + return { + title: `${formattedEventType} in ${formattedRegion}`, + link, + item: items, + }; + }, +}; + +function getAuthor(event: EventbriteEvent) { + return `${event.primary_organizer.name}`; +} + +function getTicketPriceRange(event: EventbriteEvent) { + const minPrice = event.ticket_availability.minimum_ticket_price.display; + const maxPrice = event.ticket_availability.maximum_ticket_price.display; + if (minPrice === maxPrice) { + return minPrice; + } + return `${minPrice} - ${maxPrice}`; +} + +function eventDate(event: EventbriteEvent): string { + const endDate = event.end_date; + const startDate = event.start_date; + const startTime = event.start_time; + const endTime = event.end_time; + if (startDate === endDate) { + return `${startDate} ${startTime} - ${endTime}`; + } + return `${startDate} ${startTime} - ${endDate} ${endTime}`; +} + +interface EventbriteEvent { + name: string; + url: string; + published: string; + primary_organizer: { + name: string; + url: string; + }; + summary: string; + tags: { + display_name: string; + }[]; + image: { + url: string; + original: { + url: string; + }; + }; + eventbrite_event_id: string; + ticket_availability: { + maximum_ticket_price: { + display: string; + }; + minimum_ticket_price: { + display: string; + }; + is_free: boolean; + }; + timezone: string; + start_date: string; + start_time: string; + end_date: string; + end_time: string; +} diff --git a/lib/routes/eventbrite/namespace.ts b/lib/routes/eventbrite/namespace.ts new file mode 100644 index 0000000000..b4d37e5af7 --- /dev/null +++ b/lib/routes/eventbrite/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Eventbrite', + url: 'eventbrite.com', + lang: 'en', +};