On the first evening, the page worked. The PS Plus catalogue, sorted by rating, with a nice genre filter. I searched for a game I knew. It showed up twice, with two different ratings. Another, which I knew was in the catalogue, was not there. A third was marked “removed” even though I had played it the night before.
What I believed
That the catalogue was a list. A list of titles that four public sources each describe in their own way, and that matching them by title would be enough to get a clean record: the rating from one source, the release date from another, catalogue presence from a third.
The first collection script was written in an hour. Matching by title, in ten minutes. That is where the three weeks began.
What it was
Two problems I was treating as one.
The first: a title is not an identifier. The same game is called “Game Name”, “Game Name™”, “Game Name — Director’s Cut”, “GAME NAME Remastered” or “Game Name (PS4 & PS5)” depending on the source. Two different games can share a name ten years apart. One source includes the year, another does not.
The second, more annoying: there is no single catalogue. There is one per country. A game available in France can be missing in Germany, or leave a month earlier. The source announcing removals talks about the American catalogue. The one listing the contents talks about whichever one its author checks. Nobody is lying; nobody is describing the same thing.
The answer has two parts. An aggressive but documented title normalisation:
function key(title: string): string {
return title
.normalize('NFD').replace(/[̀-ͯ]/g, '') // accents
.replace(/[™®©]/g, '')
.replace(/\s*[\(\[].*?[\)\]]/g, '') // (PS4 & PS5), [Remastered]
.replace(/\s*[—–:-]\s*(director'?s cut|remastered|definitive edition|edition)\b.*$/i, '')
.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
}
And a list of manual matches for the cases the rule cannot settle, with a report every morning of titles that found no match. In the first weeks there were forty. Today, two or three a month.
For countries, I stopped looking for the truth. Each game has one row per country, with the date it was last seen there. “Removed” is no longer something you read somewhere, it is a deduction: absent for three consecutive runs. “Leaving soon” comes from official announcements, and I say which country they are about.
What I take from it
The AI tool wrote the collection, the interface, the nightly job, without much for me to fix. It could do nothing for the matching, because matching is not a coding problem. It is a series of decisions: are these two titles the same game? Is this source authoritative for this country? Each decision is small, debatable, and there have been hundreds of them.
That is why the methodology is published on the site. Not out of abstract transparency: because the rating on screen is the result of choices, and someone who disagrees with a choice should be able to see it.
Merging data is not writing code. It is making three hundred small decisions, and writing them down somewhere.
Get the journal by email
About one entry a week. What I build, what breaks, what I learn. No promotion, one-click unsubscribe.
You can also follow the RSS feed.