Skip to content

Instantly share code, notes, and snippets.

@Rovack
Last active October 5, 2025 23:35
Show Gist options
  • Save Rovack/51e0fb558ee0fa4ce0e2cd5f0ab17cb1 to your computer and use it in GitHub Desktop.
Save Rovack/51e0fb558ee0fa4ce0e2cd5f0ab17cb1 to your computer and use it in GitHub Desktop.
A simple script that waits for tickets to become available for the Harry Potter Studio Tour in London, and grabs them
// Note that this has some limitations, such as looking specifically for adult tickets,
// looking for the given days only in the nearest month that has availability,
// and always choosing the earliest time if several are found within the desired dates.
function setAdultTickets(adultTicketsWanted) {
const adultTicketsCount = parseInt($('.quantity-control.row > input')[0].value, 10);
const ticketChangeIterations = Math.abs(adultTicketsWanted - adultTicketsCount);
const ticketChangeButton = $(`.quantity-control.row > button.typcn-${adultTicketsCount < adultTicketsWanted ? 'plus' : 'minus'}`)[0];
for (let i = 0; i < ticketChangeIterations; i++) {
ticketChangeButton.click();
}
}
function playSound(src) {
return new Promise((resolve) => {
const audio = new Audio(src);
audio.onended = resolve;
audio.play();
});
}
function repeatHeyListen() {
playSound('https://www.myinstants.com/media/sounds/hey_listen.mp3')
.then(repeatHeyListen);
}
function waitForAvailability() {
return new Promise((resolve) => {
setTimeout(() => {
const availableEls = $('.calendar>.row:not(.blankLoader) .calendar-body .day.available'), isLoading = $('.calendar-modal[data-component=eventTimeModal] .modal-content > .loading-mask.hide').length === 0;
if (isLoading) {
return waitForAvailability()
.then((res) => resolve(res));
}
resolve(availableEls);
}, 1000);
});
}
function playSounds() {
playSound('https://www.myinstants.com/media/sounds/mlg-airhorn.mp3')
.then(() => playSound('https://www.myinstants.com/media/sounds/sound-9______.mp3'))
.then(() => playSound('https://www.myinstants.com/media/sounds/ps_1.mp3'))
.then(() => playSound('https://www.myinstants.com/media/sounds/wrong-answer-sound-effect.mp3'))
.then(() => playSound('https://www.myinstants.com/media/sounds/lalalalala.swf.mp3'))
.then(() => playSound('https://www.myinstants.com/media/sounds/tuturu_1.mp3'))
.then(() => playSound('https://www.myinstants.com/media/sounds/hallelujahshort.swf.mp3'))
.then(repeatHeyListen);
}
function addTicketsToBasket(dayElement) {
dayElement.click();
setTimeout(() => waitForAvailability()
.then(() => {
$('.ui-control.button.select-time')[0].click();
setTimeout(() => {
$('.typcn.typcn-shopping-cart.ng-binding')[0].click();
}, 2000);
}), 2000);
}
function checkForTickets(datesWanted=[6, 7, 8], adultTicketsWanted=2, checkFrequency=15) {
setAdultTickets(adultTicketsWanted);
function check() {
$('.shared-calendar-button').click();
waitForAvailability()
.then(availableEls => {
console.log(new Date(), 'Availability loaded. Checking for relevant dates...');
for (let i = 0; i < availableEls.length; i++) {
const day = parseInt(availableEls[i].innerText, 10);
console.log('Day', day, 'is available...');
if (datesWanted.includes(day)) {
console.log('Found tickets!!!!!');
playSounds();
addTicketsToBasket(availableEls[i]);
return;
}
}
console.log(`Relevant dates not yet available. Will check again in ${checkFrequency} seconds.`);
$('#page > div:nth-child(11) > div.modal.info-modal.w-auto-c > div > div.close').click();
setTimeout(check, checkFrequency * 1000);
});
};
check();
}
@paterson37
Copy link

How weird. I've tried it on different browsers, incognito/private windows and different networks and getting the same.

Will try again in the morning and update.

@Rovack
Copy link
Author

Rovack commented Oct 5, 2025

Hey @paterson37. It occurred to me this morning to also try in incognito, and I do see the same error you shared.
While I haven't looked into why it's happening, it does look like this link works even then (which is the same, just with a couple of params that they auto-add for me but I guess not consistently).
Perhaps with this one the page will load for you too?

@paterson37
Copy link

@Rovack That URL is working. Thanks!

However got a little bit of a problem with the script. This is probably user error.

I'm using this script.

function setAdultTickets(adultTicketsWanted) {
const adultTicketsCount = parseInt($('.quantity-control.row > input')[0].value, 4);
const ticketChangeIterations = Math.abs(adultTicketsWanted - adultTicketsCount);
const ticketChangeButton = $(.quantity-control.row > button.typcn-${adultTicketsCount < adultTicketsWanted ? 'plus' : 'minus'})[0];

for (let i = 0; i < ticketChangeIterations; i++) {
	ticketChangeButton.click();
}

}

function playSound(src) {
return new Promise((resolve) => {
const audio = new Audio(src);
audio.onended = resolve;
audio.play();
});
}

function repeatHeyListen() {
playSound('https://www.myinstants.com/media/sounds/hey_listen.mp3')
.then(repeatHeyListen);
}

function waitForAvailability(monthWanted) {
return new Promise((resolve) => {
setTimeout(() => {
const availableEls = $('.calendar&gt;.row:not(.blankLoader) .calendar-body .day.available'), isLoading = $('.calendar-modal[data-component=eventTimeModal] .modal-content > .loading-mask.hide').length === 0;
if (isLoading) {
return waitForAvailability(monthWanted)
.then((res) => resolve(res));
}

		if (monthWanted == null) {
			resolve({ availableEls });
		}

		const monthValue = $('[name="ctl00$ContentPlaceHolder$SalesChannelDetailControl$EventsDateTimeSelectorModal$EventsDateTimeSelector$CalendarSelector$MonthDropDownList"]')[0].value;
		const month = parseInt(monthValue.replace(/^\D+/g, ''), 10);

		if (month < monthWanted) {
			console.log(`Month too early (${month}) - skipping to next month.`);
			$('[name="ctl00$ContentPlaceHolder$SalesChannelDetailControl$EventsDateTimeSelectorModal$EventsDateTimeSelector$CalendarSelector$NextMonthImageButton"]').click();
			return waitForAvailability(monthWanted).then(res => resolve(res));
		}

		resolve({ availableEls, month });
	}, 1000);
});

}

function playSounds() {
playSound('https://www.myinstants.com/media/sounds/mlg-airhorn.mp3')
.then(() => playSound('https://www.myinstants.com/media/sounds/sound-9______.mp3'))
.then(() => playSound('https://www.myinstants.com/media/sounds/ps_1.mp3'))
.then(() => playSound('https://www.myinstants.com/media/sounds/wrong-answer-sound-effect.mp3'))
.then(() => playSound('https://www.myinstants.com/media/sounds/lalalalala.swf.mp3'))
.then(() => playSound('https://www.myinstants.com/media/sounds/tuturu_1.mp3'))
.then(() => playSound('https://www.myinstants.com/media/sounds/hallelujahshort.swf.mp3'))
.then(repeatHeyListen);
}

async function addTicketsToBasket(dayElement) {
dayElement.click();

await new Promise((resolve) => setTimeout(resolve, 2000));
await waitForAvailability();

const chooseTimeButton = $('.ui-control.button.select-time:not(.disabled)')[0];
if (!chooseTimeButton) return false;

console.log('Found tickets!!!!!');
playSounds();

chooseTimeButton.click();

await new Promise((resolve) => setTimeout(resolve, 2000));

$('.typcn.typcn-shopping-cart.ng-binding')[0].click();
return true;

}

function checkForTickets(datesWanted=[4,5,6], adultTicketsWanted=4, checkFrequency=15, monthWanted=5, hourRange=9-8) {
setAdultTickets(adultTicketsWanted);

async function check() {
	try {
		if ($('.ui-control.button.extendSession').length != 0) {
			console.log('Extending session');
			$('.ui-control.button.extendSession').click();
		}
		
		$('.shared-calendar-button').click();

		await waitForAvailability(monthWanted)
			.then(async ({ availableEls, month }) => {
				console.log(new Date(), `Availability loaded${month != null ? ` for month ${month}` : ''}. Checking for relevant dates...`);

				if (monthWanted != null && month > monthWanted) {
					console.log(`Month is too late (${month}). Will check again in ${checkFrequency} seconds.`);
					setTimeout(check, checkFrequency * 1000);
					return;
				}

				for (let i = 0; i < availableEls.length; i++) {
					const day = parseInt($('.calendar>.row:not(.blankLoader) .calendar-body .day.available')[i].innerText, 10);
					console.log('Day', day, 'is available...');
					if (datesWanted.includes(day)) {
						const succeeded = await addTicketsToBasket($('.calendar>.row:not(.blankLoader) .calendar-body .day.available')[i], hourRange);
						if (succeeded) return;
					}
				}

				console.log(`Relevant dates not yet available. Will check again in ${checkFrequency} seconds.`);
				setTimeout(check, checkFrequency * 1000);
		});
	} catch (err) {
		console.error('Error checking. Just gonna keep trying.', err);
		setTimeout(check, checkFrequency * 1000);
	}
};
check();

}

function checkForTicketsInMonth(datesWanted, monthWanted, adultTicketsWanted, checkFrequency) {
return checkForTickets(datesWanted, adultTicketsWanted, checkFrequency, monthWanted);
}

and the below command

checkForTicketsInMonth([9], 10, 2, 3, { minHour: 0, maxHour: 11 })

It seems to work to a point. But it won't refresh or recheck and I get the below in the console.

image

I'm sure it will be user error.

@Rovack
Copy link
Author

Rovack commented Oct 5, 2025

It looks like the code you're running isn't exactly what you pasted in the comment, since the version in your comment doesn't include the words "Tickets found at wanted date" anywhere but the screenshot does.

I imagine you probably also made the modifications suggested here?
If so, perhaps one of the steps there wasn't completed. Note that there are 3 short sections that need to change in addition to the big function that's replaced.

That's my best guess, since when I copy the code from your comment, make those changes, and run:

checkForTicketsInMonth([9], 10, 2, 3, { minHour: 0, maxHour: 11 })

it seems to work just fine - keeps refreshing and trying, and I don't get any errors.

In case it helps, the full version I get with those changes is:

function setAdultTickets(adultTicketsWanted) {
	const adultTicketsCount = parseInt($('.quantity-control.row > input')[0].value, 4);
	const ticketChangeIterations = Math.abs(adultTicketsWanted - adultTicketsCount);
	const ticketChangeButton = $(`.quantity-control.row > button.typcn-${adultTicketsCount < adultTicketsWanted ? 'plus' : 'minus'}`)[0];

	for (let i = 0; i < ticketChangeIterations; i++) {
		ticketChangeButton.click();
	}
}

function playSound(src) {
	return new Promise((resolve) => {
		const audio = new Audio(src);
		audio.onended = resolve;
		audio.play();
	});
}

function repeatHeyListen() {
	playSound('https://www.myinstants.com/media/sounds/hey_listen.mp3')
		.then(repeatHeyListen);
}

function waitForAvailability(monthWanted) {
	return new Promise((resolve) => {
		setTimeout(() => {
			const availableEls = $('.calendar>.row:not(.blankLoader) .calendar-body .day.available'), isLoading = $('.calendar-modal[data-component=eventTimeModal] .modal-content > .loading-mask.hide').length === 0;
			if (isLoading) {
				return waitForAvailability(monthWanted)
					.then((res) => resolve(res));
			}

			if (monthWanted == null) {
				resolve({ availableEls });
			}

			const monthValue = $('[name="ctl00$ContentPlaceHolder$SalesChannelDetailControl$EventsDateTimeSelectorModal$EventsDateTimeSelector$CalendarSelector$MonthDropDownList"]')[0].value;
			const month = parseInt(monthValue.replace(/^\D+/g, ''), 10);

			if (month < monthWanted) {
				console.log(`Month too early (${month}) - skipping to next month.`);
				$('[name="ctl00$ContentPlaceHolder$SalesChannelDetailControl$EventsDateTimeSelectorModal$EventsDateTimeSelector$CalendarSelector$NextMonthImageButton"]').click();
				return waitForAvailability(monthWanted).then(res => resolve(res));
			}

			resolve({ availableEls, month });
		}, 1000);
	});
}

function playSounds() {
	playSound('https://www.myinstants.com/media/sounds/mlg-airhorn.mp3')
		.then(() => playSound('https://www.myinstants.com/media/sounds/sound-9______.mp3'))
		.then(() => playSound('https://www.myinstants.com/media/sounds/ps_1.mp3'))
		.then(() => playSound('https://www.myinstants.com/media/sounds/wrong-answer-sound-effect.mp3'))
		.then(() => playSound('https://www.myinstants.com/media/sounds/lalalalala.swf.mp3'))
		.then(() => playSound('https://www.myinstants.com/media/sounds/tuturu_1.mp3'))
		.then(() => playSound('https://www.myinstants.com/media/sounds/hallelujahshort.swf.mp3'))
		.then(repeatHeyListen);
}

async function addTicketsToBasket(dayElement, { minHour, maxHour } = {}) {
	dayElement.click();

	await new Promise((resolve) => setTimeout(resolve, 2000));
	await waitForAvailability();

	const timeRows = $('.time-selector .times .time.row');
	for (let i = 0; i < timeRows.length; i++) {
		const row = $(timeRows[i]);
		const timeString = row.find('.time')[0].innerText;
		const chooseTimeButton = row.find('.select-time:not(.disabled)')[0];

		if (!chooseTimeButton) continue;

		const hour = parseInt(timeString.split(':')[0], 10);
		if (minHour != null && hour < minHour) {
			console.log(`Tickets found at wanted date but time is too early (${timeString})`);
			continue;
		}
		if (maxHour != null && hour > maxHour) {
			console.log(`Tickets found at wanted date but time is too late (${timeString})`);
			continue;
		}

		console.log('Found tickets!!!!!');
		playSounds();
	
		chooseTimeButton.click();
	
		await new Promise((resolve) => setTimeout(resolve, 2000));
	
		$('.typcn.typcn-shopping-cart.ng-binding')[0].click();
		return true;	
	}

	return false;
}

function checkForTickets(datesWanted=[6, 7, 8], adultTicketsWanted=2, checkFrequency=15, monthWanted, hourRange) {
	setAdultTickets(adultTicketsWanted);

	async function check() {
		try {
			if ($('.ui-control.button.extendSession').length != 0) {
				console.log('Extending session');
				$('.ui-control.button.extendSession').click();
			}
			
			$('.shared-calendar-button').click();

			await waitForAvailability(monthWanted)
				.then(async ({ availableEls, month }) => {
					console.log(new Date(), `Availability loaded${month != null ? ` for month ${month}` : ''}. Checking for relevant dates...`);

					if (monthWanted != null && month > monthWanted) {
						console.log(`Month is too late (${month}). Will check again in ${checkFrequency} seconds.`);
						setTimeout(check, checkFrequency * 1000);
						return;
					}

					for (let i = 0; i < availableEls.length; i++) {
						const day = parseInt($('.calendar>.row:not(.blankLoader) .calendar-body .day.available')[i].innerText, 10);
						console.log('Day', day, 'is available...');
						if (datesWanted.includes(day)) {
							const succeeded = await addTicketsToBasket($('.calendar>.row:not(.blankLoader) .calendar-body .day.available')[i], hourRange);
							if (succeeded) return;
						}
					}

					console.log(`Relevant dates not yet available. Will check again in ${checkFrequency} seconds.`);
					setTimeout(check, checkFrequency * 1000);
			});
		} catch (err) {
			console.error('Error checking. Just gonna keep trying.', err);
			setTimeout(check, checkFrequency * 1000);
		}
	};
	check();
}

function checkForTicketsInMonth(datesWanted, monthWanted, adultTicketsWanted, checkFrequency, hourRange) {
	return checkForTickets(datesWanted, adultTicketsWanted, checkFrequency, monthWanted, hourRange);
}

@paterson37
Copy link

@Rovack That's it working now! Thank you very much. Here's hoping early morning tickets become available :D

@Rovack
Copy link
Author

Rovack commented Oct 5, 2025

Great to hear it! I certainly hope so too. :)

@wingpan79
Copy link

@Rovack Thank you very much!!!!! It works perfectly without modifying anything. Now I wait until I get the tickets I need.

@wingpan79
Copy link

@Rovack I noticed that when all the tickets for October are sold out, the script automatically jumps to the same date in November (for example, if I’m searching for the 24th)

@Rovack
Copy link
Author

Rovack commented Oct 5, 2025

IIRC that's just how the website behaves - skips to the month that has the nearest available tickets - but as long as the script recognizes that it's the wrong month and instead of adding tickets, just keeps refreshing and waiting, then it should be fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment