Skip to content
Open
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
4 changes: 4 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ import {
import {
SeaWorldGoldCoast,
WarnerBrosMovieWorld,
ParadiseCountry,
WetNWildGoldCoast,
} from './parks/te2/te2.js';

export default {
Expand Down Expand Up @@ -203,5 +205,7 @@ export default {
Futuroscope,
WarnerBrosMovieWorld,
SeaWorldGoldCoast,
ParadiseCountry,
WetNWildGoldCoast,
},
};
193 changes: 183 additions & 10 deletions lib/parks/te2/te2.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export class TE2Destination extends Destination {
* @param {string} [options.apidomain] TE2 API domain
* @param {string} [options.apiuser] TE2 API username
* @param {string} [options.apipass] TE2 API password
* @param {string} [options.rideStatusEndpoint] Endpoint for ride wait times and status
* @param {Array<string>} [options.rideTypes] Categories to classify as rides
* @param {Array<string>} [options.diningTypes] Categories to classify as dining
* @param {Array<string>} [options.showTypes] Categories to classify as shows
Expand All @@ -41,6 +42,7 @@ export class TE2Destination extends Destination {
options.apidomain = options.apidomain || 'te2.biz';
options.apiuser = options.apiuser || '';
options.apipass = options.apipass || '';
options.rideStatusEndpoint = options.rideStatusEndpoint || null;
options.rideTypes = options.rideTypes || ['Ride', 'Coasters', 'Family', 'ThrillRides', 'Kids', 'Rides & Attractions'];
options.diningTypes = options.diningTypes || ['Snacks', 'wpDining', 'Meals', 'Dining'];
options.showTypes = options.showTypes || ['Shows', 'Show', 'Entertainment', 'Live Entertainment', 'Presentation'];
Expand All @@ -59,6 +61,9 @@ export class TE2Destination extends Destination {
if (!this.config.apipass) throw new Error('Missing apipass');

this.config.apiBase = this.config.apiBase || `https://${this.config.subdomain}.${this.config.apidomain}`;
if (!this.config.rideStatusEndpoint) {
this.config.rideStatusEndpoint = `${this.config.apiBase}/rest/venue/${this.config.venueId}/poi/all/status`;
}

const baseURLHostname = new URL(this.config.apiBase).hostname;

Expand All @@ -72,7 +77,7 @@ export class TE2Destination extends Destination {
'Content-Type': 'application/json',
};

if (requestUrl.pathname.startsWith('/rest/')) {
if (requestUrl.pathname.startsWith('/rest/') || requestUrl.pathname.startsWith('/v2/')) {
const credentials = Buffer.from(`${this.config.apiuser}:${this.config.apipass}`).toString('base64');
options.headers.Authorization = `Basic ${credentials}`;
}
Expand All @@ -85,8 +90,122 @@ export class TE2Destination extends Destination {
*/
async getPOIStatus() {
'@cache|1';
const resp = await this.http('GET', `${this.config.apiBase}/rest/venue/${this.config.venueId}/poi/all/status`);
return resp?.body ?? resp;
const resp = await this.http('GET', this.config.rideStatusEndpoint);
const rideData = resp?.body ?? resp;
if (!Array.isArray(rideData)) {
return rideData;
}

const sampleEntry = rideData.find((entry) => !!entry);
if (sampleEntry?.status !== undefined && sampleEntry?.id !== undefined) {
return rideData;
}

const statusEntries = [];
rideData.forEach((ride) => {
const te2RideId = this._extractTe2RideId(ride?.tags);
if (!te2RideId) return;

const primaryQueue = this._getPrimaryQueue(ride);
const waitTimeMins = ride?.waitTimeMins ?? primaryQueue?.waitTimeMins;
const waitTimeValue = Number(waitTimeMins);
const isOpen = this._isRideOpen(ride, primaryQueue);

const status = {
isOpen,
};

if (Number.isFinite(waitTimeValue)) {
status.waitTime = waitTimeValue;
}

statusEntries.push({
id: te2RideId,
status,
});
});

return statusEntries;
}

/**
* Collect TE2 ride IDs available from the ride status endpoint
* @return {Promise<Set<string>>}
* @private
*/
async _getRideStatusIds() {
const statusData = await this.getPOIStatus();
const ids = new Set();
if (Array.isArray(statusData)) {
statusData.forEach((entry) => {
if (entry?.id) {
ids.add(entry.id);
}
});
}
return ids;
}

/**
* Extract TE2 ride ID from tags returned by the wait time endpoint
* @param {Array} tags
* @return {string|null}
* @private
*/
_extractTe2RideId(tags) {
if (!Array.isArray(tags)) {
return null;
}

for (const rawTag of tags) {
const tag = typeof rawTag === 'string' ? rawTag : rawTag?.label;
if (typeof tag !== 'string') continue;

const match = tag.match(/^te2_rideid:(.+)$/i);
if (match && match[1]) {
return match[1];
}
}

return null;
}

/**
* Select the primary queue entry for a ride
* @param {object} ride
* @return {object|null}
* @private
*/
_getPrimaryQueue(ride) {
const queues = Array.isArray(ride?.queues) ? ride.queues : [];
return queues.find((queue) => queue?.isPrimary) || queues.find((queue) => queue?.isDefault) || queues[0] || null;
}

/**
* Determine if a ride is open based on queue or state data
* @param {object} ride
* @param {object|null} primaryQueue
* @return {boolean}
* @private
*/
_isRideOpen(ride, primaryQueue) {
if (typeof primaryQueue?.isOpen === 'boolean') {
return primaryQueue.isOpen;
}

if (typeof ride?.isOpen === 'boolean') {
return ride.isOpen;
}

if (typeof ride?.state === 'string') {
const normalized = ride.state.toLowerCase();
if (normalized.includes('open')) return true;
if (normalized.includes('closed') || normalized.includes('down') || normalized.includes('maintenance')) {
return false;
}
}

return false;
}

/**
Expand Down Expand Up @@ -284,7 +403,7 @@ export class TE2Destination extends Destination {
* @return {Promise<Array<object>>} Array of filtered entity objects
* @private
*/
async _getFilteredEntities({types, entities}, data, {includeFn} = {}) {
async _getFilteredEntities({types, entities}, data, {includeFn, excludeIds} = {}) {
const poi = await this.getPOIData();
if (!Array.isArray(poi)) {
return [];
Expand All @@ -293,10 +412,17 @@ export class TE2Destination extends Destination {
const typeSet = new Set(Array.isArray(types) ? types : []);
const entitySet = new Set(Array.isArray(entities) ? entities : []);
const seenIds = new Set();
const excludeSet = excludeIds
? new Set(Array.isArray(excludeIds) ? excludeIds : Array.from(excludeIds))
: null;

return poi.filter((entry) => {
if (!entry?.id) return false;

if (excludeSet?.has(entry.id)) {
return false;
}

if (seenIds.has(entry.id)) {
return false;
}
Expand Down Expand Up @@ -327,14 +453,22 @@ export class TE2Destination extends Destination {
* @return {Promise<Array<object>>} Array of attraction entity objects
*/
async buildAttractionEntities() {
const diningTypes = await this.getDiningTypes();
const excludeIds = new Set(Array.isArray(diningTypes?.entities) ? diningTypes.entities : []);
const rideStatusIds = await this._getRideStatusIds();
return await this._getFilteredEntities(
await this.getAttractionTypes(),
{
entityType: entityType.attraction,
attractionType: attractionType.ride,
},
{
excludeIds,
includeFn: (entry) => {
if (entry?.id && rideStatusIds.has(entry.id)) {
return true;
}

// Include entries with status data that have ride indicator tags
const status = entry?.status;
if (!status || (status.waitTime === undefined && status.operationalStatus === undefined)) {
Expand Down Expand Up @@ -498,8 +632,8 @@ export class TE2Destination extends Destination {
const poiMap = this._buildPOIMap(poiData);

for (const event of events) {
if (!event?.id) return;
if (existingIds.has(event.id)) return;
if (!event?.id) continue;
if (existingIds.has(event.id)) continue;

const entity = await this._buildShowEntityFromEvent(event, poiMap);
showEntities.push(entity);
Expand All @@ -520,7 +654,7 @@ export class TE2Destination extends Destination {
async _fetchScheduleData({days = 120} = {}) {
'@cache|1440';
const resp = await this.http('GET', `${this.config.apiBase}/v2/venues/${this.config.venueId}/venue-hours?days=${days}`);
return resp.body;
return resp?.body ?? resp;
}

/**
Expand Down Expand Up @@ -573,10 +707,15 @@ export class TE2Destination extends Destination {
if (!start) return;

const startMs = Date.parse(start);
if (!Number.isFinite(startMs) || startMs < nowMs) return;
if (!Number.isFinite(startMs)) return;

const end = slot?.end;
const endMs = end ? Date.parse(end) : Number.NaN;
if (Number.isFinite(endMs)) {
if (endMs < nowMs) return;
} else if (startMs < nowMs) {
return;
}
const showtime = {
type: SCHEDULE_TYPE_PERFORMANCE,
startTime: start,
Expand Down Expand Up @@ -677,12 +816,15 @@ export class TE2Destination extends Destination {

async buildEntityLiveData() {
const liveDataMap = new Map();
const attractionIds = new Set((await this.getAttractionEntities()).map((entry) => entry?._id).filter((id) => !!id));
const restaurantIds = new Set((await this.getRestaurantEntities()).map((entry) => entry?._id).filter((id) => !!id));

const statusData = await this.getPOIStatus();
if (Array.isArray(statusData)) {
statusData.forEach((entry) => {
if (!entry?.status || !entry.id) return;
if (entry.id.includes('_STANDING_OFFER_BEACON')) return;
if (!attractionIds.has(entry.id) && !restaurantIds.has(entry.id)) return;

const liveData = liveDataMap.get(entry.id) || {_id: entry.id};

Expand Down Expand Up @@ -712,8 +854,19 @@ export class TE2Destination extends Destination {
if (!slot?.startTime) return false;
const startMoment = moment.tz(slot.startTime, this.config.timezone);
if (!startMoment.isValid()) return false;
if (startMoment.isBefore(now)) return false;
return startMoment.format('YYYY-MM-DD') === todayKey;

const endMoment = slot?.endTime ? moment.tz(slot.endTime, this.config.timezone) : null;
if (endMoment && !endMoment.isValid()) return false;

const startKey = startMoment.format('YYYY-MM-DD');
const endKey = endMoment ? endMoment.format('YYYY-MM-DD') : null;
if (startKey !== todayKey && endKey !== todayKey) return false;

if (endMoment) {
return !endMoment.isBefore(now);
}

return !startMoment.isBefore(now);
});
};

Expand All @@ -738,6 +891,7 @@ export class SeaWorldGoldCoast extends TE2Destination {
options.name = options.name || 'Sea World Gold Coast';
options.destinationId = options.destinationId || 'vrtp_sw_te2';
options.venueId = options.venueId || 'VRTP_SW';
options.rideStatusEndpoint = options.rideStatusEndpoint || 'https://sw.vrtpfastpass.com.au/api/api/guest/rides';
super(options);
}
}
Expand All @@ -747,6 +901,25 @@ export class WarnerBrosMovieWorld extends TE2Destination {
options.name = options.name || 'Warner Bros. Movie World';
options.destinationId = options.destinationId || 'vrtp_mw_te2';
options.venueId = options.venueId || 'VRTP_MW';
options.rideStatusEndpoint = options.rideStatusEndpoint || 'https://mw.vrtpfastpass.com.au/api/api/guest/rides';
super(options);
}
}

export class ParadiseCountry extends TE2Destination {
constructor(options = {}) {
options.name = options.name || 'Paradise Country';
options.destinationId = options.destinationId || 'vrtp_pc_te2';
options.venueId = options.venueId || 'VRTP_PC';
super(options);
}
}

export class WetNWildGoldCoast extends TE2Destination {
constructor(options = {}) {
options.name = options.name || 'Wet n Wild Gold Coast';
options.destinationId = options.destinationId || 'vrtp_ww_te2';
options.venueId = options.venueId || 'VRTP_WW';
super(options);
}
}
Loading