feat(universal): migrate UOR + Hollywood to /resort-areas/*/places#191
Merged
Conversation
The server already binds to 0.0.0.0 — only the printed URL was misleading for users running parksapi on a separate host. Print os.hostname() (or $MIGRATE_HOST override) so the LAN-reachable URL is clickable.
Update user-agent from Dart/3.6 to Dart/3.11 and add x-channel-type: Mobile header to match the official Flutter app. This aligns our UDX inject chain with the current device fingerprint for the new /resort-areas/*/places endpoint.
Mirrors the type set already shipping for USJ. Defines the wire shape
returned by /resort-areas/{UOR,USH}/places and the CDN show-list.json
feed, plus the PLACE_TYPE_TO_ENTITY map for the migration's
entity build. No behavior change yet — wired up in subsequent commits.
Wires up the UDX /resort-areas/{resortKey}/places endpoint plus the
CDN show-list.json feed. Cached behind @cache; no consumer yet — the
buildEntityList / buildLiveData refactors in the next commits use them.
Keeps buildSchedules on the working legacy /schedule endpoint while exposing new place_id-based park IDs to consumers. Also serves as the allow-list for which Park-type place records become PARK entities — CityWalk + Hollywood Upper/Lower Lots are excluded (not theme parks). Five entries: UOR (usf/ioa/eu/vb) + USH (ush). Easy to delete when the schedule endpoint itself migrates to the UDX platform in a follow-up.
Maps a UniversalPlace to a wiki Entity. Returns null for place_types we don't surface (Park handled separately, Shop/Amenity/etc. dropped). Sanitizes place_id and venue_id via the shared helper so any future upstream noise can't smuggle disallowed chars into entity IDs.
Replaces the legacy /api/pointsofinterest entity build. Parks emitted first (filtered to the PARK_PLACE_ID_TO_LEGACY_VENUE_ID allow-list to drop CityWalk + Hollywood sub-lots), then placeToEntity() handles attractions / shows / restaurants. Dining filter removed — Epic Universe and Volcano Bay restaurants are now exposed. Live data + schedules still reference legacy POI state and break here; fixed in the next two commits.
The USH umbrella park (ush.ush) carries only a GEOFENCE entry in geometry.locations[], no map entry. The previous PARK emission code required a map entry, so ush.ush emitted without location and the base-class anchor-entity validation rejected it. Fall back to any geometry entry with lat_lng when no map entry exists — preserves the strict 'map preferred' behaviour for parks that have one, and keeps base-class validation happy for those that don't. Non-park entities (placeToEntity) intentionally do not get this fallback — their location is best-effort, not validation-critical.
Filters show_times[] to ENABLED future slots and projects them onto the wiki LiveData showtimes shape. Used by the buildLiveData refactor in the next commit to expose showtimes for UOR + Hollywood shows for the first time (the legacy POI feed didn't carry per-day times).
…CDN) Wait-time CDN entries are now keyed straight to sanitizeId(wait_time_attraction_id), matching the place_id scheme — no more POI numeric-ID lookups. Showtimes come from the CDN show-list.json (new — the legacy POI feed had no per-day times). Express Now offers look up entities by sanitized place_id directly. The numeric-WaitTime fallback that read from poi.Rides/Shows is dropped — the new endpoint doesn't carry one. If any ride loses status info as a result, surface in the pre-merge diff and treat as a separate fix.
Schedule endpoint still hits the legacy /schedule URL by numeric VenueId (unchanged — that endpoint hasn't migrated), but the emitted EntitySchedule.id is now the new place_id so it joins up with the PARK entities buildEntityList emits.
All entity, live-data, and showtime sources now flow through /resort-areas/*/places + the CDN feeds. Drops fetchPOI / getPOI, UniversalPOIResponse / UniversalPOIData types, WANTED_DINING_TYPES + IGNORE_SHOW_TYPES filters, getFilteredShows, getRideIDFromWaitTimeId, getParks/fetchParks, and UniversalVenuesResponse. Also removes the getUniversalAttractionType and shouldIncludeUniversalAttraction helpers that exclusively served the old POI path. ~200 lines lighter. Updates schedule.test.ts to assert on new place IDs (uor.usf / ush.ush) instead of legacy numeric venue IDs, and drops the stale getParks mock. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Migrates Universal Orlando + Universal Studios Hollywood to consume entities/live data from the UDX /resort-areas/{UOR,USH}/places endpoint plus the CDN shows/show-list.json feed, aligning with the official app’s current data sources and enabling per-show showtimes.
Changes:
- Refactors
src/parks/universal/universal.tsto build entities from/places, map IDs to sanitizedplace_ids, and source showtimes fromshows/show-list.json. - Updates Universal schedule emission to use
place_idpark IDs while still fetching hours from the legacy numeric venue schedule endpoint via a translation table. - Improves the migration tool server startup logging and adds unit tests for the new pure helpers (
placeToEntity,parseShowTimes), plus updates schedule tests for the new park IDs.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/tools/migrate/server.ts | Adjusts migration server startup logging and host display for easier browser access. |
| src/parks/universal/universal.ts | Core migration: entities via /places, showtimes via CDN feed, ID/key changes, schedule relabeling. |
| src/parks/universal/tests/schedule.test.ts | Updates schedule tests to assert new place_id-based park IDs. |
| src/parks/universal/tests/places.test.ts | Adds unit tests for placeToEntity and parseShowTimes. |
Comment on lines
860
to
861
| // Process virtual queues | ||
| for (const vQueue of vQueueStates) { |
Comment on lines
901
to
+905
| for (const queue of attraction.queues) { | ||
| let rideId: string | null = null; | ||
|
|
||
| const poiId = queue.alternate_ids.find((x) => x.system_name === 'POI'); | ||
| if (poiId) { | ||
| rideId = poiId.system_id; | ||
| } else if (attraction.wait_time_attraction_id) { | ||
| rideId = this.getRideIDFromWaitTimeId(poi, attraction.wait_time_attraction_id); | ||
| if (attraction.wait_time_attraction_id) { | ||
| rideId = sanitizeId(attraction.wait_time_attraction_id); |
… QueueEntityId Post place_id migration, the entity list emits sanitized place_ids (e.g. `uor.ioa.rides.the_amazing_adventures_of_spider_man`) instead of the legacy numeric ids. The VQ loop was still keying RETURN_TIME by `vQueue.QueueEntityId` — a numeric id that no longer matches any emitted entity. With 22+ active VQ entries observed on the current feed, the orphan-attached RETURN_TIME data was a real live-data regression vs main: the data went to phantom numeric ids while the real ride entities received nothing. The VQ feed already carries a `PlaceId` that matches the new entity scheme (40 of 45 entries observed). Switch the key to `sanitizeId(vQueue.PlaceId)` and skip entries that lack one (rather than silently attach to a non-existent id). Caught by Copilot review and confirmed against a fresh pre/post snapshot.
|
Makes sense that Hollywood Rip Ride Rockit isn't present - this ride closed on August 18, 2025. Fast & Furious: Hollywood Drift isn't open yet, should be opening this summer so I bet that it will appear soon/when it opens. |
Comment on lines
+256
to
+260
| const host = process.env.MIGRATE_HOST || os.hostname(); | ||
| console.log(`\nMigration review server running on :${config.port} (bound 0.0.0.0)`); | ||
| console.log(` ${config.parkName}`); | ||
| console.log(` ${mappings.length} mappings to review`); | ||
| console.log(`\nOpen http://${host}:${config.port}/ in your browser to review (or http://localhost:${config.port}/ when running on the same machine).\n`); |
Comment on lines
+816
to
+819
| // Park location: prefer `map`, fall back to any geometry entry (USH's | ||
| // `ush.ush` umbrella has only a GEOFENCE entry — base class validation | ||
| // requires the PARK to carry a location, so prefer-map-else-first beats | ||
| // dropping coords entirely). |
Comment on lines
1105
to
1107
| schedules.push({ | ||
| id: park.Id.toString(), | ||
| id: placeId, | ||
| schedule, |
| @@ -10,8 +10,7 @@ | |||
| import {describe, test, expect, beforeEach} from 'vitest'; | |||
Four cosmetic / defensive fixes, no behaviour change: - universal.ts (buildEntityList): clarify the park-location-fallback comment — the anchor-entity location check lives in src/testRunner.ts, not the base destination class. Previous comment was misleading. - universal.ts (buildSchedules): apply sanitizeId() to placeId before emitting EntitySchedule.id, for symmetry with the PARK entity emission. The table keys are clean today (uor.usf etc.), but the symmetry future-proofs the join if a key ever needs sanitising. - migrate/server.ts: bracket IPv6 hosts when printing the browser URL (\`http://[::1]:9900/\`) so MIGRATE_HOST=::1 produces a clickable URL. - schedule.test.ts: drop unused \`beforeEach\` import left over from the earlier mock-rewrite.
4 tasks
cubehouse
added a commit
that referenced
this pull request
May 25, 2026
…192) * fix(universal): restore CHILD_SWAP + MINIMUM_HEIGHT attraction tags The /places migration in #191 silently dropped the attraction tags the legacy POI build emitted (HasChildSwap → CHILD_SWAP, MinHeightInInches → MINIMUM_HEIGHT). Reporter noticed Harry Potter and the Forbidden Journey losing its 48" minimum height in Orlando. The new feed exposes the same data under place_type.attributes[]: - has_child_swap (string "true"/"false") - minimum_rider_height_inches (string) Read both in placeToEntity for ATTRACTION-type emissions only (matches the legacy surface — only rides had these), emit via TagBuilder. Verified live against the real /places feed: - Harry Potter and the Forbidden Journey: 48 in + CHILD_SWAP ✓ - Despicable Me Minion Mayhem: 40 in + CHILD_SWAP ✓ Added 6 unit tests covering present/absent/zero/non-numeric height values, has_child_swap true/false, and that non-ATTRACTION types (Show / Dining) don't pick up these tags even if the upstream feed ever includes them on a non-ride. * test(universal): strengthen attribute-tag tests per Copilot review - 'both tags' test now asserts the exact tags (CHILD_SWAP + MINIMUM_HEIGHT with height/unit), and explicitly verifies the unrelated express_pass / mfdo_enabled attributes do not produce extra tags. - 'Non-Ride entities' test now covers BOTH Show and Dining (matches the test name's claim — it previously only covered Show).
4 tasks
cubehouse
added a commit
that referenced
this pull request
May 25, 2026
The CDN /shows/show-list.json feed emits UTC ISO strings (e.g. 2026-05-25T20:30:00.000Z). The /places migration in #191 passed those through unchanged, but downstream displays were rendering the Z string in local time and showing all showtimes as "after park close" — e.g. Shrek's Swamp Meet (UOR USF) displayed as 8:30 PM instead of 4:30 PM EDT. The legacy pre-/places code emitted park-local ISO with offset via constructDateTime(date, time, this.timezone). Restore that behaviour by re-projecting each ENABLED show_time through formatInTimezone. parseShowTimes now takes a timezone parameter. Updated the buildLiveData call site (uses this.timezone — UOR: America/New_York, USH: America/Los_Angeles) and the unit tests, which now assert against the expected -04:00 / -07:00 offsets and cover both timezones. Verified live against /places + /shows feeds: Shrek's Swamp Meet emits 2026-05-25T18:20:00-04:00 / 19:20:00-04:00 (6:20 PM / 7:20 PM EDT) instead of the bare-UTC 22:20:00.000Z / 23:20:00.000Z.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Migrates
UniversalOrlandoandUniversalStudios(Hollywood) entity, live-data, and showtime sources from the deprecatedservices.universalorlando.com/api/pointsofinterestendpoint to the UDX-platform/resort-areas/{UOR,USH}/placesendpoint + the CDNshows/show-list.jsonfeed. Restores missing Epic Universe + Volcano Bay restaurants reported in the wiki, aligns with what the official Flutter app actually consumes (verified via captured device traffic), and adds per-show showtimes for the first time.Every UOR + Hollywood entity ID changes — hard cut from legacy numeric IDs (
10010,24000, etc.) to sanitized place_ids (uor.usf,uor.eu, etc.). Park IDs likewise:10010 → uor.usf,10000 → uor.ioa,24000 → uor.eu,13801 → uor.vb,13825 → ush.ush. Downstream wiki re-key handled post-merge via the existingnpm run migrate -- universalorlando/... -- universalstudiostool (matches by name + Haversine distance, browser UI confirms each pair, then pushes externalId rewrites preserving wiki UUIDs).Entity counts (before → after)
Headline wins
Architecture
Single-file refactor of
src/parks/universal/universal.ts, mirroring the pattern already shipping forsrc/parks/usj/universalstudiosjapan.ts(USJ). Existing UDX OAuth (getUdxToken+injectUdxToken) already authenticates the new endpoint — no new auth code. Park schedules stay on the legacygetVenueScheduleendpoint, with a hard-codedplace_id ↔ legacy VenueIdtranslation table (PARK_PLACE_ID_TO_LEGACY_VENUE_ID) for ID continuity until the schedule endpoint itself migrates in a follow-up.UDX request headers brought in line with the captured Flutter session:
user-agent: Dart/3.11 (dart:io),x-channel-type: Mobile,x-uniwebservice-appversion: 2026.5.1.Two new pure helpers landed with unit tests (TDD):
placeToEntity(8 cases) andparseShowTimes(3 cases). Plus an existing-test fix:schedule.test.tsnow asserts on the new place_id-based park IDs.Known caveats
/resort-areas/{UOR,USH}/placesupstream — confirmed by searching the after-snapshot. The official Flutter app doesn't surface them either, so this matches user-facing behaviour, but worth flagging. A manual override list could be added as a follow-up if needed.ush.lower_lot/ush.upper_lotemit with parentIds that don't resolve to any emitted PARK entity (the Hollywood umbrella park isush.ush). Same orphan-parent pattern as legacy hotel/CityWalk restaurants — not a regression, but a follow-upVENUE_ID_ALIAS: {ush.lower_lot → ush.ush, ush.upper_lot → ush.ush}map would clean this up. Filed out-of-scope here.Captured device traffic
Behavior verified against a fresh Flutter session (darkride session 16274, 2026-05-23). The official Flutter app no longer calls
services.universalorlando.com/api/pointsofinterestat all — it's exclusively on/resort-areas/UOR/places+ the CDN feeds. Header set (Bearer +x-channel-type: Mobile+ Dart/3.11 user-agent) matched verbatim.Test plan
npm run buildcleannpm test→ 1099 / 1099 pass (11 new tests inplaces.test.ts)npm run dev -- universalorlando→ 4/4npm run dev -- universalstudios→ 4/4/tmp/uor-diff/REPORT.md) — counts + name diffs + caveatsnpm run migrate -- universalorlandothen... -- universalstudiosto hand new IDs to the wiki via the existing browser-based migration toolnpm run audit:live -- --diff --only=universalorlando,universalstudiosto confirm continuity🤖 Generated with Claude Code