Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -1826,6 +1826,12 @@ Outdoor Cinema",
"format": {},
"notes": "",
"occasions": {
"inConversation": {
"guests": [
"Emma Matthews",
"Louis Petit",
],
},
"panel": true,
},
"screen": "Cinema 3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,13 @@ exports[`Genesis Cinema retrieve and transform 1`] = `
"bookingUrl": "https://genesis.admit-one.co.uk/seats/?perfCode=11711",
"format": {},
"notes": "",
"occasions": {},
"occasions": {
"inConversation": {
"guests": [
"Nick Walker",
],
},
},
"status": {
"soldOut": false,
},
Expand Down
74 changes: 74 additions & 0 deletions cinemas/ibraaz.org/tests/transform.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const transform = require("../transform");

// Turang's film page, cut down to what the transform reads, in Ibraaz's own
// words from a November 2026 retrieve.
const TURANG_URL = "https://ibraaz.org/whats-on/turang";
const turangPage = `
<div class="hero__inner">
<h1 class="headline">Turang</h1>
<p class="line--lg">Bachtiar Siagian</p>
</div>
<div class="richtext">
<p>With an introduction from <strong>Bunga Siagian</strong>, cultural producer, researcher, and daughter of Bachtiar Siagian, the screening presents the restored film as an act of narrative reclamation and an archive of decolonisation, created at a time of radical political upheaval in the country of its creation. This screening of <i>Turang</i> is curated by <strong>May Adadol Ingawanij</strong> with Ibraaz in conversation with the final chapter of Taring Padi’s <i>Rakyat Pasti Menang</i> at Ibraaz London, which runs until 22 November, 2026.</p>
</div>
<div class="tt-widget-fallback">
<a href="https://tickets.ibraaz.org/events/ibraaz/2526567/select-date">Book</a>
</div>
`;

// The Ticket Tailor event says nothing of the introduction.
const sourcedEvents = {
"tickettailor.com": [
{
showingId: "tickettailor.com-2526567",
title: "Turang",
url: "https://www.tickettailor.com/events/ibraaz/2526567",
overview: {
duration: 7200000,
categories: [],
directors: [],
actors: [],
},
performances: [
{
time: new Date("2026-11-22T15:00:00Z").getTime(),
notes: "",
bookingUrl: "https://www.tickettailor.com/events/ibraaz/2526567",
status: {},
accessibility: {},
format: {},
occasions: {},
},
],
matchingHints: { overview: "Turang" },
},
],
};

describe("Ibraaz transform", () => {
it("reads the occasions the film page describes", async () => {
const [movie] = await transform(
{ moviePages: { [TURANG_URL]: turangPage } },
sourcedEvents,
);

expect(movie.performances[0].occasions).toEqual({
intro: { guests: ["Bunga Siagian"] },
});
});

it("keeps the occasions the listing found", async () => {
const listed = structuredClone(sourcedEvents);
listed["tickettailor.com"][0].performances[0].occasions = { qAndA: true };

const [movie] = await transform(
{ moviePages: { [TURANG_URL]: turangPage } },
listed,
);

expect(movie.performances[0].occasions).toEqual({
intro: { guests: ["Bunga Siagian"] },
qAndA: true,
});
});
});
15 changes: 13 additions & 2 deletions cinemas/ibraaz.org/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const cheerio = require("cheerio");
const {
getText,
createOverview,
createOccasions,
mergeOccasions,
generateShowingId,
} = require("../../common/utils");
const attributes = require("./attributes");
Expand Down Expand Up @@ -103,11 +105,20 @@ async function transform({ moviePages }, sourcedEvents) {
duration: sourcedEvent.overview.duration,
},
// The event's performances already carry the booking URL, status and the
// accessibility and format read off the listing; the room they screen in
// is the one thing only the venue's own page knows.
// accessibility, format and occasions read off the listing. The room
// they screen in is only on the venue's own page, and so is the copy
// that names who is introducing - "With an introduction from Bunga
// Siagian" is in Turang's page and not its Ticket Tailor event - so the
// page's occasions are added beneath the listing's.
performances: sourcedEvent.performances.map((performance) => ({
...performance,
screen: screen || undefined,
occasions: mergeOccasions(
createOccasions("", {}, overview, {
date: new Date(performance.time),
}),
performance.occasions,
),
})),
matchingHints: { overview },
});
Expand Down
12 changes: 12 additions & 0 deletions cinemas/ica.art/tests/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,12 @@ exports[`Institute of Contemporary Arts retrieve and transform 1`] = `
"format": {},
"notes": "",
"occasions": {
"inConversation": {
"guests": [
"Michael Witt",
],
},
"intro": true,
"qAndA": true,
},
"screen": "Cinema 1",
Expand Down Expand Up @@ -1452,6 +1458,12 @@ exports[`Institute of Contemporary Arts retrieve and transform 1`] = `
"format": {},
"notes": "",
"occasions": {
"inConversation": {
"guests": [
"Alexander Carey-Morgan",
"Laurence Hills",
],
},
"qAndA": true,
},
"screen": "Cinema 1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ exports[`Phoenix Cinema retrieve and transform 1`] = `
"format": {},
"notes": "This screening will be followed by a Q&A",
"occasions": {
"intro": true,
"qAndA": true,
},
"screen": "1",
Expand Down Expand Up @@ -107,6 +108,7 @@ exports[`Phoenix Cinema retrieve and transform 1`] = `
"format": {},
"notes": "This screening will be followed by a Q&A",
"occasions": {
"intro": true,
"qAndA": true,
},
"screen": "1",
Expand Down Expand Up @@ -789,7 +791,11 @@ exports[`Phoenix Cinema retrieve and transform 1`] = `
"format": {},
"notes": "This screening will be followed by a Q&A",
"occasions": {
"qAndA": true,
"qAndA": {
"guests": [
"Aengus Collins O'Malley",
],
},
},
"screen": "1",
"status": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4023,7 +4023,9 @@ Q&A",
"dimension": "2d",
},
"notes": "Ad and Trailer Free",
"occasions": {},
"occasions": {
"intro": true,
},
"screen": "1",
"status": {
"soldOut": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,13 @@ No Ads or Trailers",
Special Event
No Ads or Trailers",
"occasions": {
"qAndA": true,
"qAndA": {
"guests": [
"Rabbi Herschel Gluck",
"Winstan Whitter",
"Elin Moe",
],
},
},
"screen": "1",
"status": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,14 @@ The screening will be introduced.",
"bookingUrl": "https://bookings.thegardencinema.co.uk/TheGardenCinema.dll/TSelectItems.waSelectItemsPrompt.TcsWebMenuItem_0.TcsWebTab_0.TcsPerformance_5958172.TcsSection_3060731",
"format": {},
"notes": "The screening will be followed by a discussion.",
"occasions": {},
"occasions": {
"inConversation": {
"guests": [
"Dan Dooner",
"Ben Mills",
],
},
},
"status": {
"soldOut": false,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1187,7 +1187,7 @@ exports[`The Lexi Cinema retrieve and transform 1`] = `
},
"bookingUrl": "https://thelexicinema.co.uk/TheLexiCinema.dll/Booking?Booking=TSelectItems.waSelectItemsPrompt.TcsWebMenuItem_0.TcsWebTab_0.TcsPerformance_9991762.TcsSection_5209705",
"format": {},
"notes": "",
"notes": "Presented in collaboration with Cinema Mentiré",
"occasions": {},
"screen": "1",
"status": {
Expand All @@ -1201,7 +1201,7 @@ exports[`The Lexi Cinema retrieve and transform 1`] = `
},
"bookingUrl": "https://thelexicinema.co.uk/TheLexiCinema.dll/Booking?Booking=TSelectItems.waSelectItemsPrompt.TcsWebMenuItem_0.TcsWebTab_0.TcsPerformance_9991779.TcsSection_5209705",
"format": {},
"notes": "",
"notes": "Presented in collaboration with Cinema Mentiré",
"occasions": {},
"screen": "1",
"status": {
Expand Down Expand Up @@ -1232,7 +1232,7 @@ exports[`The Lexi Cinema retrieve and transform 1`] = `
},
"bookingUrl": "https://thelexicinema.co.uk/TheLexiCinema.dll/Booking?Booking=TSelectItems.waSelectItemsPrompt.TcsWebMenuItem_0.TcsWebTab_0.TcsPerformance_9991803.TcsSection_5209705",
"format": {},
"notes": "",
"notes": "Presented in collaboration with Cinema Mentiré",
"occasions": {},
"screen": "1",
"status": {
Expand All @@ -1246,7 +1246,7 @@ exports[`The Lexi Cinema retrieve and transform 1`] = `
},
"bookingUrl": "https://thelexicinema.co.uk/TheLexiCinema.dll/Booking?Booking=TSelectItems.waSelectItemsPrompt.TcsWebMenuItem_0.TcsWebTab_0.TcsPerformance_9991814.TcsSection_5209705",
"format": {},
"notes": "",
"notes": "Presented in collaboration with Cinema Mentiré",
"occasions": {},
"screen": "1",
"status": {
Expand Down Expand Up @@ -1277,7 +1277,7 @@ exports[`The Lexi Cinema retrieve and transform 1`] = `
},
"bookingUrl": "https://thelexicinema.co.uk/TheLexiCinema.dll/Booking?Booking=TSelectItems.waSelectItemsPrompt.TcsWebMenuItem_0.TcsWebTab_0.TcsPerformance_9991784.TcsSection_5209705",
"format": {},
"notes": "",
"notes": "Presented in collaboration with Cinema Mentiré",
"occasions": {},
"screen": "1",
"status": {
Expand All @@ -1291,7 +1291,7 @@ exports[`The Lexi Cinema retrieve and transform 1`] = `
},
"bookingUrl": "https://thelexicinema.co.uk/TheLexiCinema.dll/Booking?Booking=TSelectItems.waSelectItemsPrompt.TcsWebMenuItem_0.TcsWebTab_0.TcsPerformance_9991801.TcsSection_5209705",
"format": {},
"notes": "",
"notes": "Presented in collaboration with Cinema Mentiré",
"occasions": {},
"screen": "1",
"status": {
Expand Down Expand Up @@ -2210,7 +2210,9 @@ exports[`The Lexi Cinema retrieve and transform 1`] = `
"bookingUrl": "https://thelexicinema.co.uk/TheLexiCinema.dll/Booking?Booking=TSelectItems.waSelectItemsPrompt.TcsWebMenuItem_0.TcsWebTab_0.TcsPerformance_9987157.TcsSection_5209807",
"format": {},
"notes": "Accessible screening",
"occasions": {},
"occasions": {
"intro": true,
},
"screen": "2",
"status": {
"soldOut": false,
Expand Down
58 changes: 52 additions & 6 deletions common/occasions.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,23 @@ const routinePreview =

const qAndA = String.raw`(?:q\s*&\s*a|q\s*and\s*a|screen\s?talk)`;

// "Switches Screening + Director Conversation" - a conversation billed by who
// is in it rather than with "in". Only the roles that are someone in the room
// for the film, so a "Film Conversation" (a strand name) isn't read as one.
const conversationRole = String.raw`(?:director|filmmaker|film-maker|writer|actor|actress|producer|artist|curator|composer|author|cast|creator)s?(?:['’]s?)?`;

// Curators put works "in conversation with" one another: Ibraaz's Turang "is
// curated by May Adadol Ingawanij with Ibraaz in conversation with the final
// chapter of Taring Padi's Rakyat Pasti Menang", an exhibition. That is a
// dialogue between artworks, not someone in the room.
const conversationWithWork =
/\bin conversation with\s+(?:(?:the|an?|our|its|their|this|these)\s+)?(?:[\p{L}'’-]+\s+){0,3}?(?:exhibitions?|chapters?|installations?|works?|programmes?|seasons?|series|collections?|archives?|shows?)(?![\p{L}'’])/iu;

const notInTheRoomOrAWork = new RegExp(
`${notInTheRoom.source}|${conversationWithWork.source}`,
"iu",
);

// Each rule finds one key. `names` names the detail its trailing phrase fills;
// rules for the same key are tried in order and the first to match wins, so a
// named rule is listed before the bare one it refines.
Expand All @@ -257,7 +274,8 @@ const rules = [
{
key: "qAndA",
pattern: new RegExp(
String.raw`\b${qAndA}\s+(?:with|w\/|from|featuring|by)\s+`,
// "a Q&A discussion with Rabbi Herschel Gluck" (the Rio) is a Q&A
String.raw`\b${qAndA}(?:\s+(?:discussion|session))?\s+(?:with|w\/|from|featuring|by)\s+`,
"i",
),
names: "guests",
Expand All @@ -271,15 +289,36 @@ const rules = [

{
key: "inConversation",
pattern: /\bin conversation with\s+/i,
pattern: new RegExp(
String.raw`\b(?:in|${conversationRole})\s+conversation\s+with\s+`,
"i",
),
names: "guests",
unless: notInTheRoom,
unless: notInTheRoomOrAWork,
},
// A discussion only counts with someone named in it: "lead a discussion
// following this screening with director Nadine White" (the Lexi). A bare
// one is as often the audience talking - "A post-film discussion where every
// voice is welcome", the Lexi's "friendly film discussion group for
// seniors" - and a panel or Q&A discussion is a panel or a Q&A, as is
// Birkbeck's "post-screening discussion panel with Michal Oleszczyk".
{
key: "inConversation",
pattern: /\bin conversation\b/i,
pattern: new RegExp(
String.raw`(?<!\b(?:panel|${qAndA})\s)\bdiscussion\s+(?:(?!panel\b)[\p{L}'’-]+\s+){0,4}?with\s+`,
"iu",
),
names: "guests",
unless: notInTheRoom,
},
{
key: "inConversation",
pattern: new RegExp(
String.raw`\bin conversation\b|\b${conversationRole}\s+conversation\b|(?:^|\s)\+\s*conversation\b`,
"i",
),
unless: notInTheRoomOrAWork,
},

{
key: "panel",
Expand All @@ -301,7 +340,14 @@ const rules = [
},
{
key: "intro",
pattern: /\bintroduc(?:tion|tions|ed)\b|(?:^|[^\p{L}])intros?(?![\p{L}])/iu,
// "introduce" only with the film or the screening as its object -
// "Charmaine Simpson ... will introduce the films" (the Lexi) - since
// programme copy introduces audiences to things ("This season introduces
// audiences to ...", "The second selection of films will introduce films
// about the war"). Not "introducing", which is as often a title -
// "Introducing the Dwights".
pattern:
/\bintroduc(?:tion|tions|ed)\b|\bintroduces?\s+(?:the|this|these|each|tonight's|tonight’s)\s+(?:films?|screenings?|evening|night|programme|feature|double bill)\b|(?:^|[^\p{L}])intros?(?![\p{L}])/iu,
unless: notInTheRoom,
},

Expand Down Expand Up @@ -348,7 +394,7 @@ const rules = [
// One cheap test every rule implies, so text that says nothing but a film's
// name costs one pass rather than a dozen.
const anySignal =
/q\s*&\s*a|q\s*and\s*a|screen\s?talk|intro|live|premi[eè]re|preview|conversation|in person|attendance|panel|masterclass|special guest/i;
/q\s*&\s*a|q\s*and\s*a|screen\s?talk|intro|live|premi[eè]re|preview|conversation|discussion|in person|attendance|panel|masterclass|special guest/i;

// The occasions one piece of text describes, as an occasions object. A
// sentence of a description is read with `fromDescription`, which leaves out
Expand Down
Loading
Loading