DEV Community
Ticketing feeds lie a little: cleaning concert listings from six partner APIs
events.lat is a calendar of concerts, festivals and cultural events in Latin America: more than 3,000 upcoming events in over 100 cities, in English, Spanish and Portuguese. Almost none of it is typed in by hand. It comes from partner feeds: Ticketmaster, Eventbrite, Sympla, Bandsintown, Songkick and Luma. Each feed is fine for what it was built for: selling tickets. Put them all on one browse page, though, and you see things that make no sense to someone looking for something to do on Saturday. Here are the three problems we hit, and the small, boring functions that fixed them. 1. Add-ons that look like events Partners publish ticket upsells as their own events. A "Hi-Touch" photo session sold with the note boleto de evento no incluido (event ticket not included). A pre-signed poster. A parking pass for a festival. In the feed they have a title, a date and a venue, just like the concert. The fix is a pattern on the title: const ADDON_PATTERN =
/(boleto de evento no in?cluido|no incluye boleto|entrada no incluida|ticket not included|hi[\s-]?(touch|bye)|pre[\s-]?signed|meet ?& ?greet|soundcheck|^\sestacionamiento\b|\b(estacionamiento|parking)\s$|\bupgrade\b)/i;
const isTicketAddon = (e: { title?: string | null }) =>
ADDON_PATTERN.test(String(e.title ?? ""));
The hard part is what to leave out. An Abono … is a season or multi-day pass, and a Grada 15, Formula 1 … is a grandstand tier. Both look like add-ons, but they are real tickets to a real event, so filtering them would hide things a visitor can actually buy. Parking had a bug of its own. The first version matched the word at the start of a title, and that hid an event called "Parking Lot Party". Now English "parking" only counts as the last word ("Festival Vaivén 2026 Estacionamiento" is a car park; "Parking Lot Party" is a party). Spanish estacionamiento is safe at either end. Each of those cases is now a test. 2. One show, ten listings A theatre run comes in once per performance. El Fantasma de la Ópera playing ten nights at one theatre filled ten slots on the page, and pushed everything else off it. So we group performances into one production with a key built from the title and the venue: const fold = (v: unknown) =>
String(v ?? "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
function productionKey(e: { title?: string | null; venue?: string | null; city?: string | null }) {
const title = fold(e.title)
.replace(/\b(19|20)\d{2}\b/g, "") // "… 2026" on some nights, not others
.replace(/[^a-z0-9]+/g, " ")
.trim();
return ${title}::${fold(e.venue ?? e.city).trim()};
}
Accents, case, punctuation and the year all go, because partner titles for the same run vary from night to night, for example "Ópera" on one and "Opera" on another, with or without "2026". The venue stays in the key, so the same touring show in two cities is still two listings. Then we keep only the soonest performance of each production, in the order the rows came in: type Listing = { title?: string | null; venue?: string | null; city?: string | null; event_date?: string | null };
function dedupeListing(events: T[] | null | undefined): T[] {
const seen = new Set();
const out: T[] = [];
for (const e of events ?? []) {
if (isTicketAddon(e) || isImplausibleEvent(e)) continue;
const key = productionKey(e);
if (seen.has(key)) continue;
seen.add(key);
out.push(e);
}
return out;
}
One rule we hold to: these helpers only remove noise. They never reorder by "quality", because someone looking for a specific date has to find that date where they expect it. The list stays in date order. Since the list can shrink, callers ask for a few extra rows. 3. Rows that can't be real Some feeds leave out the location and the importer fills in a placeholder. We found an event in a city called "Nocity", dated 31 December 2039. const PLACEHOLDER_CITIES = new Set(["nocity", "no city", "n/a", "na", "unknown", "tba", "tbd", "-"...
Powered by Readybot.io
External Link
Platform
You are about to visit:
https://example.comThis will open in a new tab. Make sure you trust this link.