diff --git a/common/known-removable-phrases.js b/common/known-removable-phrases.js index c851b59b..c2e1b78a 100644 --- a/common/known-removable-phrases.js +++ b/common/known-removable-phrases.js @@ -1091,6 +1091,7 @@ const knownRemovablePhrases = [ "Taste Film presents", "Tatau to Tattoo:", "TFFF:", + "- TFFF", "Tibet Film Festival London:", "Tim Burton's ", "Times+:", diff --git a/common/normalize-title.js b/common/normalize-title.js index ef069155..1429a329 100644 --- a/common/normalize-title.js +++ b/common/normalize-title.js @@ -1007,6 +1007,16 @@ function normalizeTitle(title, options) { title = hasBrackets[1]; } + // Collapse multiple years + title = title.replace(/\((\d{4}),\s+(\d{4})\)/i, "($1/$2)"); + + // Remove diretor or other notes from bracketted years + // E.g. Convert "(1964, Glauber Rocha)" to "(1964)" + const hasYearWithComment = title.match(/^(.*?)\s+\((\d{4}),\s[^)]+\)/i); + if (hasYearWithComment) { + title = `${hasYearWithComment[1]} (${hasYearWithComment[2]})`; + } + const hasSlavicPremier = title.match(/Кинопремиера на "([^"]+)" /i); if (hasSlavicPremier) { title = hasSlavicPremier[1]; diff --git a/common/standardize-prefixing-for-theatre-performances.js b/common/standardize-prefixing-for-theatre-performances.js index 82b09963..3904a2a2 100644 --- a/common/standardize-prefixing-for-theatre-performances.js +++ b/common/standardize-prefixing-for-theatre-performances.js @@ -4,7 +4,7 @@ const yearRangeMatcher = /(\d{2})(\d{2})[-|/](\d{2})/; const shortYearRangeMatcher = /(\d{2})[-|/](\d{2})/; const yearSuffixMatcher = /\(\d{4}\)$/; const ownerMatcher = /:\s+[^\s]+['|’]s/; -const baselineYear = new Date().getFullYear() - 1; // Keep the year at 2025 +const getBaselineYear = () => new Date().getFullYear(); // Les Miserables function standardizePrefixingForLesMiserablesPerformances(title) { @@ -90,7 +90,7 @@ function standardizePrefixingForMetropolitanOperaPerformances(title, options) { updatedPrefixTitle = updatedPrefixTitle.replace(ownerMatcher, ":"); - let year = baselineYear; + let year = getBaselineYear(); const fullYearRangeMatch = updatedPrefixTitle.match(fullYearRangeMatcher); if (fullYearRangeMatch) { @@ -117,7 +117,7 @@ function standardizePrefixingForMetropolitanOperaPerformances(title, options) { } const yearNumber = parseInt(year, 10); - const isFutureYear = yearNumber > baselineYear; + const isFutureYear = yearNumber > getBaselineYear(); year = isFutureYear ? `${yearNumber - 1}` : year; updatedPrefixTitle = `${updatedPrefixTitle} (${year})`; @@ -144,7 +144,9 @@ const rboPrefixes = [ /Royal Opera Live[:|\s]/i, /Royal Ballet Live[:|\s]/i, /RBO[:|\s]/i, + /Royal Ballet and Opera Live[:|\s]/i, /Royal Ballet and Opera[:|\s]/i, + /Royal Ballet & Opera Live[:|\s]/i, /Royal Ballet & Opera[:|\s]/i, /Royal Opera House[:|\s]/i, /^The Royal Ballet[:|\s]/i, @@ -176,7 +178,7 @@ function standardizePrefixingForRoyalBalletOperaPerformances(title) { ":", ); - let year = baselineYear; + let year = getBaselineYear(); const fullYearRangeMatch = updatedPrefixTitle.match(fullYearRangeMatcher); if (fullYearRangeMatch) { @@ -200,7 +202,7 @@ function standardizePrefixingForRoyalBalletOperaPerformances(title) { if (yearSuffixMatch) { const yearSuffix = yearSuffixMatch[0].replaceAll(/[()]/g, ""); const yearNumber = parseInt(yearSuffix, 10); - const isFutureYear = yearNumber > baselineYear; + const isFutureYear = yearNumber > getBaselineYear(); year = isFutureYear ? `${yearNumber - 1}` : yearSuffix; updatedPrefixTitle = updatedPrefixTitle.replace(yearMatcher, ""); } diff --git a/common/tests/normalize-title.test.js b/common/tests/normalize-title.test.js index 35e922bd..e7d2bc8d 100644 --- a/common/tests/normalize-title.test.js +++ b/common/tests/normalize-title.test.js @@ -2,6 +2,8 @@ const normalizeTitle = require("../normalize-title"); const testTitles = require("./test-titles.json"); describe("Normalise Title", () => { + jest.useFakeTimers().setSystemTime(new Date("2025-06-15")); + test.each(testTitles)( "normalizes the title '$input'", ({ input, output }) => { diff --git a/common/tests/test-titles.json b/common/tests/test-titles.json index 32326a11..d0d1ac5b 100644 --- a/common/tests/test-titles.json +++ b/common/tests/test-titles.json @@ -27893,7 +27893,7 @@ }, { "input": "The Gothique Film Society presents The Flying Serpent (1946) And It’s Alive (1974, 35mm)", - "output": "flying serpent (1946) its alive" + "output": "flying serpent (1946) its alive (1974)" }, { "input": "Honey Don't! Unlimited Screening", @@ -71505,7 +71505,7 @@ }, { "input": "New York Ninja (1984, 2021) vs Miami Connection (1987) - Pick Your Trash!", - "output": "new york ninja (1984 2021) vs miami connection (1987)" + "output": "new york ninja (1984/2021) vs miami connection (1987)" }, { "input": "Cheese & Port Afternoon (October)", @@ -79005,11 +79005,11 @@ }, { "input": "Hi, Mom! (1970, Brian De Palma) 4K Restoration - TFFF", - "output": "hi mom" + "output": "hi mom (1970)" }, { "input": "The Dancing Hawk (1978, Grzegorz Królikiewicz) UK Theatrical Premiere - TFFF", - "output": "dancing hawk" + "output": "dancing hawk (1978)" }, { "input": "All Power to the People: The Black Panther Party at 60", @@ -79293,7 +79293,7 @@ }, { "input": "Black God, White Devil (1964, Glauber Rocha) 4k Restoration - TFFF", - "output": "black god white devil" + "output": "black god white devil (1964)" }, { "input": "Faces of Death (1978) world theatrical premiere + Video intro by Daniel Goldhaber - TFFF", @@ -79862,5 +79862,93 @@ { "input": "THE EXCHANGE CINEMA CLUB PRESENTS - CHEF", "output": "chef" + }, + { + "input": "ROYAL BALLET & OPERA LIVE: ALICE’S ADVENTURES IN WONDERLAND", + "output": "royal ballet opera 2025 alices adventures in wonderland" + }, + { + "input": "ROYAL BALLET & OPERA LIVE: CARMEN", + "output": "royal ballet opera 2025 carmen" + }, + { + "input": "ROYAL BALLET & OPERA LIVE: COSÌ FAN TUTTE", + "output": "royal ballet opera 2025 cosi fan tutte" + }, + { + "input": "ROYAL BALLET & OPERA LIVE: GÖTTERDÄMMERUNG", + "output": "royal ballet opera 2025 gotterdammerung" + }, + { + "input": "ROYAL BALLET & OPERA LIVE: LA FANCIULLA DEL WEST", + "output": "royal ballet opera 2025 la fanciulla del west" + }, + { + "input": "ROYAL BALLET & OPERA LIVE: MACBETH", + "output": "royal ballet opera 2025 macbeth" + }, + { + "input": "ROYAL BALLET & OPERA LIVE: MANON", + "output": "royal ballet opera 2025 manon" + }, + { + "input": "ROYAL BALLET & OPERA LIVE: SWAN LAKE", + "output": "royal ballet opera 2025 swan lake" + }, + { + "input": "ROYAL BALLET & OPERA LIVE: THE NUTCRACKER", + "output": "royal ballet opera 2025 the nutcracker" + }, + { + "input": "ROYAL BALLET & OPERA LIVE: TOSCA", + "output": "royal ballet opera 2025 tosca" + }, + { + "input": "Moana (Live Action) (2026) Sing-along", + "output": "moana (2026)" + }, + { + "input": "Black God, White Devil (1964, Glauber Rocha)- TFFF", + "output": "black god white devil (1964)" + }, + { + "input": "Hi, Mom! (1970, Brian De Palma) - TFFF", + "output": "hi mom (1970)" + }, + { + "input": "All I Ever Wanted", + "output": "all i ever wanted" + }, + { + "input": "Harbinger", + "output": "harbinger" + }, + { + "input": "Rise of the Footsoldier: All Day Marathon", + "output": "rise of the footsoldier all day marathon" + }, + { + "input": "Rise of the Footsoldier: Retribution", + "output": "rise of the footsoldier retribution" + }, + { + "input": "Shaun the Sheep Movie (2015)", + "output": "shaun the sheep movie (2015)" + }, + { + "input": "Shaun The Sheep Movie: Farmageddon (2019)", + "output": "shaun the sheep movie farmageddon (2019)" + }, + { + "input": "The Pirates! In an Adventure with Scientists! (2012)", + "output": "pirates in an adventure with scientists (2012)" + }, + { + "input": "Wallace & Gromit: A Close Shave/A Matter of Loaf and Death", + "output": "wallace gromit a close shave/a matter of loaf death" + }, + { + "input": "Cine-Real presents: One Flew Over The Cuckoo's Nest", + "output": "one flew over the cuckoos nest" } ]