Skip to content

Latest commit

 

History

41 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ticket Watch

A small, self-hosted Viagogo ticket monitor with zone-aware filtering and rich Discord alerts.

PHP Symfony PostgreSQL Bootstrap Playwright License

Ticket Watch polls a Viagogo event at a configurable interval, keeps the latest result in PostgreSQL, and sends a Discord embed when the tracked price changes. A lightweight web dashboard manages monitors, while Playwright discovers Viagogo's human-readable ticket zones and converts them into the internal filter IDs required by listing requests.

Important

Ticket Watch is an experimental personal project. It uses Viagogo's website and internal web requests rather than a stable public buyer API. It is not affiliated with, endorsed by, or sponsored by Viagogo. Review the limitations and responsible-use notes before running it.

Contents

What it does

  • Creates named monitors from Viagogo event URLs.
  • Checks every 30 seconds, 1 minute, 5 minutes, 10 minutes, or any API-provided interval of at least 10 seconds.
  • Tracks either all tickets or a selected set of discovered zones.
  • Supports two Viagogo sort strategies:
    • NEWPRICE — the lowest-priced result.
    • RECOMMENDED — Viagogo's recommended result.
  • Runs listing checks and zone discovery asynchronously through Symfony Messenger.
  • Persists monitor state, scheduling data, filters, prices, and failures in PostgreSQL.
  • Sends rich Discord embeds with:
    • A clickable event title.
    • Alert copy that distinguishes lowest-price and recommended monitors.
    • Old and new prices.
    • Section, row, seat, availability, ticket type, and deal score.
    • A direct link to the exact listing.
    • Viagogo branding and the available venue/seat-view image.
  • Provides a JSON API for the complete monitor lifecycle.
  • Keeps the browser UI deliberately small: Bootstrap, Stimulus, Twig, and Symfony AssetMapper; there is no frontend build pipeline.

Screenshots

Dashboard

The dashboard creates monitors, displays current prices and scheduling information, and exposes the start, stop, retry, and delete actions.

Ticket Watch dashboard showing the new-monitor form and active monitors

Zone-aware monitoring

Zone monitors stay in a preparation state while Playwright discovers the filters. Once ready, one or more zones can be selected before the monitor is started.

Ticket Watch dashboard showing the zone selection interface

Discord alerts

Lowest-price alert Recommended alert
Discord alert for a new lowest price Discord alert for a recommended listing

Architecture

Ticket Watch is a small Symfony monolith with two deliberately separate scraping paths:

  • PHP performs recurring listing requests using a browser-like TLS client and a saved cookie.
  • Node.js and Playwright perform DOM-driven zone discovery because the zone-to-filter mapping is only exposed through interactions with the event page.
flowchart LR
    Operator([Operator]) --> Dashboard[Bootstrap + Stimulus dashboard]
    Dashboard --> API[Symfony monitor API]

    subgraph Application[Ticket Watch]
        API --> Database[(PostgreSQL)]
        API --> Queue[[Doctrine Messenger queue]]
        Queue --> Worker[Messenger worker]
        Worker --> Check[Price check handler]
        Worker --> Discovery[Zone discovery handler]
        Check --> Parser[Listing parser]
        Check --> Notifier[Discord notifier]
        Discovery --> Playwright[Node.js + Playwright]
    end

    Check --> Listings[Viagogo listing request]
    Playwright --> EventPage[Viagogo event page]
    Notifier --> Queue
    Worker --> Discord[Discord webhook]
Loading

This layout keeps the web request fast: controllers validate and persist state, then enqueue the expensive work. The same PostgreSQL database stores both application data and Messenger messages, so Redis or another queue service is not required.

How monitoring works

Monitor lifecycle

stateDiagram-v2
    [*] --> Draft: create monitor
    Draft --> Active: start all-ticket monitor
    Draft --> Discovering: create zone monitor
    Discovering --> Ready: zones discovered
    Discovering --> Failed: challenge or DOM failure
    Failed --> Discovering: retry discovery
    Ready --> Active: select zones and start
    Active --> Active: schedule next check
    Active --> Stopped: stop
    Stopped --> Active: start again
    Stopped --> Discovering: rediscover zones
    Active --> Error: unhandled worker error
    Draft --> [*]: delete
    Discovering --> [*]: delete
    Ready --> [*]: delete
    Active --> [*]: delete
    Stopped --> [*]: delete
    Failed --> [*]: delete
    Error --> [*]: delete
Loading

Recurring check cycle

  1. Starting a monitor dispatches a CheckMonitorMessage.
  2. The Messenger worker reloads the monitor and ignores the message if the monitor is no longer active.
  3. ViagogoClient POSTs an IndexShGridOnly payload to the stored event URL using the selected sort mode and optional zone filters.
  4. ViagogoListingParser extracts the first listing returned by Viagogo.
  5. The monitor's price, currency, timestamps, and error state are updated.
  6. If the integer price differs from the previously stored price, a Discord ChatMessage is enqueued. The first successful check also produces an alert because no previous price exists.
  7. A new CheckMonitorMessage is scheduled with a Messenger delay matching intervalSeconds.

Stopping a monitor does not need to remove already queued messages. When a delayed message becomes available, the handler sees that the monitor is stopped and exits without performing a request.

Sort modes

UI option API value Behavior
Lowest price NEWPRICE Requests Viagogo's lowest-priced first result and labels alerts New Lowest Price.
Recommended RECOMMENDED Requests Viagogo's recommended first result and labels alerts Recommended.

The notification trigger currently compares prices, not listing IDs. If Viagogo changes the first listing but the rounded price remains identical, no new notification is sent.

Technology

Area Implementation
Backend PHP 8.2+, Symfony 7.4, Doctrine ORM
Database PostgreSQL 16
Queue and scheduling Symfony Messenger with Doctrine transport and delayed messages
Dashboard Twig, Bootstrap 5.3, Stimulus, Fetch API
Browser automation Node.js, Playwright, Chromium or Google Chrome
Listing transport xxx-bin/php-tls-client with a Chrome-compatible TLS profile
Notifications Symfony Notifier with the Discord bridge
Assets Symfony AssetMapper and Importmap; no webpack, Vite, or npm build step
Tests PHPUnit 13 and Symfony BrowserKit
Formatting Prettier with the PHP plugin

Requirements

  • PHP 8.2 or newer with Composer.
  • Node.js and npm. Node.js 20 or newer is recommended.
  • Docker with Docker Compose, or a compatible PostgreSQL installation.
  • Chromium or Google Chrome. Playwright can install Chromium automatically.
  • A Discord webhook.
  • A graphical browser session for the initial Viagogo challenge/session bootstrap.

The repository is currently developed on macOS, but the application code is platform-neutral. The browser and TLS-client dependencies must support the host platform.

Quick start

1. Clone and configure

git clone git@github.com:JakobAIOdev/ticket-watch.git
cd ticket-watch
cp .env.example .env

Update .env for the Docker Compose database and your Discord webhook:

APP_ENV=dev
APP_SECRET=replace-with-a-long-random-value
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5433/app?serverVersion=16&charset=utf8"
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
MAILER_DSN=null://null
DISCORD_DSN="discord://DISCORD_WEBHOOK_TOKEN@default?webhook_id=DISCORD_WEBHOOK_ID"

The included Compose override publishes PostgreSQL on host port 5433. If PostgreSQL runs directly on the host or uses a different port, adjust DATABASE_URL accordingly.

A suitable application secret can be generated with:

php -r 'echo bin2hex(random_bytes(32)), PHP_EOL;'

2. Install dependencies

composer install
npm ci
npx playwright install chromium

Composer installs the PHP dependencies and AssetMapper imports. npm is used for Playwright and formatting; there is no frontend compilation step.

3. Start PostgreSQL and migrate

docker compose up -d database
php bin/console doctrine:migrations:migrate --no-interaction

The migrations create both the monitor table and the Doctrine-backed messenger_messages queue.

4. Establish a Viagogo session

For a simple all-ticket setup, open an interactive browser and save a valid session:

npm run fetch-session -- --fresh 'https://www.viagogo.com/'
mkdir -p var/viagogo
cp var/viagogo-cookie.txt var/viagogo/viagogo-cookie.txt

Wait until Viagogo works normally in the opened browser, then return to the terminal and press Enter. The copy step is currently required because the generic session helper and the Symfony service use different cookie locations.

If zone monitoring is required, also complete the interactive zone bootstrap.

5. Start the web server and worker

Use two terminals:

# Terminal 1
symfony server:start
# Terminal 2
php bin/console messenger:consume async -vv

Open the URL printed by Symfony CLI, normally http://127.0.0.1:8000.

If Symfony CLI is not installed, PHP's development server can be used instead:

php -S 127.0.0.1:8000 -t public public/index.php

Viagogo session setup

Viagogo may require cookies created by a real browser and may present an anti-bot challenge. Ticket Watch does not solve that challenge automatically. The operator completes it interactively once, and subsequent listing checks reuse the saved cookie.

Session files

Path Purpose
var/viagogo/viagogo-cookie.txt Cookie header consumed by the Symfony listing client.
var/viagogo/browser-profile-en-US/ Persistent Playwright profile used by automated zone discovery.
var/viagogo/browser-session.json Session metadata and challenge status from zone discovery.
var/viagogo/discovered-zones.json Most recent standalone zone-discovery result.
var/viagogo/zone-discovery/ HTML and control snapshots written when discovery fails.
var/viagogo-cookie.txt Cookie written by the generic fetch-session helper before it is copied.

All files below var/ are ignored by Git. They can contain sensitive session information and must never be committed or shared.

Refreshing an expired session

npm run fetch-session -- --fresh 'EVENT_URL'
mkdir -p var/viagogo
cp var/viagogo-cookie.txt var/viagogo/viagogo-cookie.txt

Use a full Viagogo event URL when the homepage alone does not establish all necessary cookies.

Running the application

Ticket Watch needs both processes to remain alive:

Process Command Responsibility
Web server symfony server:start Dashboard, JSON API, and static assets.
Worker php bin/console messenger:consume async -vv Zone discovery, delayed listing checks, and outgoing Discord messages.

After changing PHP code, restart the long-running worker so it loads the new classes:

php bin/console messenger:stop-workers
php bin/console messenger:consume async -vv

Messenger's stop command is graceful: the worker finishes its current message before exiting.

Using the dashboard

All tickets

  1. Enter a descriptive monitor name.
  2. Paste the full Viagogo event URL.
  3. Choose a check interval.
  4. Select All tickets.
  5. Choose Lowest price or Recommended.
  6. Create the monitor, then select Start.

The first completed check stores the current price and sends an initial Discord alert.

Selected zones

  1. Select Selected zones while creating the monitor.
  2. The monitor is saved as a draft and zone discovery is queued automatically.
  3. Wait for the status to become Ready. The dashboard refreshes every five seconds while it is visible.
  4. Select one or more discovered zones.
  5. Start the monitor. The selected zone mappings are merged into one filter object and persisted.

Active zone monitors display the zones chosen at start time but do not allow live editing. Stop the monitor before changing the selection or rerunning discovery.

Zone discovery

The Playwright script opens Viagogo's filter panel, finds visible zone checkboxes, clicks each zone, and captures the sections, ticketClasses, rows, and seats identifiers from the resulting URL or grid request.

The Symfony worker runs discovery as:

node scripts/discover-zones.mjs --headless --locale=en-US --json EVENT_URL

Interactive bootstrap

Headless discovery works only after the persistent profile is trusted. Run the same locale once in a visible browser before relying on the worker:

npm run discover-zones -- --fresh --locale=en-US 'EVENT_URL'

If a challenge appears, solve it in the browser and press Enter in the terminal when prompted. The script then saves the profile and application cookie below var/viagogo/.

Standalone options

npm run discover-zones -- --help
npm run discover-zones -- --json --locale=en-US 'EVENT_URL'
npm run discover-zones -- --headless --json --locale=en-US 'EVENT_URL'
npm run discover-zones -- --include-sections 'EVENT_URL'
npm run discover-zones -- --keep-url-filters 'EVENT_URL'
npm run discover-zones -- --with-english --locale=de-DE 'EVENT_URL'
Option Effect
--json Writes only JSON to stdout; progress messages go to stderr.
--fresh Deletes the selected persistent browser profile before launch.
--headless Runs without a visible browser and fails if a challenge is detected.
--include-sections Includes labels beginning with Section or Bereich.
--keep-url-filters Preserves existing filter parameters from the supplied URL.
--locale=LOCALE Sets browser locale and Accept-Language; defaults to en-US.
--profile-dir=PATH Overrides the persistent profile directory.
--with-english Performs an additional en-US pass and merges English labels by filter IDs.

Discord notifications

Webhook configuration

Create a webhook in the desired Discord channel. A Discord webhook URL has this shape:

https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN

Convert it to the Symfony Notifier DSN used in .env:

DISCORD_DSN="discord://WEBHOOK_TOKEN@default?webhook_id=WEBHOOK_ID"

Do not commit the webhook URL, ID, token, or generated .env file.

Alert contents

  • New Lowest Price · Event Name for NEWPRICE monitors.
  • Recommended · Event Name for RECOMMENDED monitors.
  • The title links to the event page.
  • Price changes are rendered as old → new; the first alert contains only the current price.
  • Ticket metadata is arranged into compact Discord fields.
  • Open exact listing → links to the event URL with listingId and listingQty parameters.
  • A Viagogo thumbnail is always included.
  • The listing's vfsUrl venue/seat image is included when Viagogo provides one.
  • The footer includes the monitor ID and alert timestamp.

Discord messages are themselves routed through the async transport. If the worker is stopped, the alert remains queued until a worker resumes processing.

API reference

All endpoints accept and return JSON unless noted otherwise. The dashboard is a client of the same API.

Method Endpoint Purpose
GET /api/monitors List all monitors, newest first.
POST /api/monitors Create a draft monitor and optionally queue zone discovery.
GET /api/monitors/{id} Retrieve one monitor.
PATCH, PUT /api/monitors/{id} Update name, interval, sort mode, or selected zone filters.
POST /api/monitors/{id}/start Activate a monitor and queue its first check.
POST /api/monitors/{id}/stop Stop an active monitor.
GET /api/monitors/{id}/zones Return completed zone discovery results.
POST /api/monitors/{id}/zones/discover Clear and rerun discovery for a stopped zone monitor.
DELETE /api/monitors/{id} Permanently delete a monitor; returns 204 No Content.

Create a monitor

curl -X POST http://127.0.0.1:8000/api/monitors \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Don Toliver Munich",
    "sourceUrl": "https://www.viagogo.com/.../E-161443419?quantity=2",
    "intervalSeconds": 60,
    "ticketScope": "all",
    "filterMode": "NEWPRICE"
  }'
Field Type Rules
sourceUrl string Required, non-empty, maximum 1,000 characters.
name string Required, non-empty, maximum 255 characters.
intervalSeconds integer Minimum 10; defaults to 60.
ticketScope string all or zones; defaults to all.
filterMode string NEWPRICE or RECOMMENDED; normalized to uppercase.

Start an all-ticket monitor

curl -X POST http://127.0.0.1:8000/api/monitors/1/start \
  -H 'Content-Type: application/json' \
  -d '{}'

Start a zone monitor

Wait until zoneDiscoveryStatus is completed, then submit at least one usable filter array:

curl -X POST http://127.0.0.1:8000/api/monitors/2/start \
  -H 'Content-Type: application/json' \
  -d '{
    "selectedZoneFilters": {
      "sections": ["141765"],
      "ticketClasses": ["21217"],
      "rows": ["395659"],
      "seats": []
    }
  }'

Update a monitor

curl -X PATCH http://127.0.0.1:8000/api/monitors/1 \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Munich — recommended",
    "intervalSeconds": 300,
    "filterMode": "RECOMMENDED"
  }'

The update endpoint does not currently change sourceUrl or ticketScope.

Monitor response

{
    "id": 1,
    "name": "Don Toliver Munich",
    "sourceUrl": "https://www.viagogo.com/.../E-161443419?quantity=2",
    "status": "active",
    "intervalSeconds": 60,
    "lastPrice": 96,
    "currency": "EUR",
    "lastCheckedAt": "2026-07-21T20:30:00+02:00",
    "nextCheckAt": "2026-07-21T20:31:00+02:00",
    "createdAt": "2026-07-21T20:20:00+02:00",
    "stoppedAt": null,
    "lastError": null,
    "availableZones": null,
    "zoneDiscoveryStatus": "not_required",
    "zoneDiscoveryError": null,
    "selectedZoneFilters": null,
    "ticketScope": "all",
    "filterMode": "NEWPRICE"
}

Status values

Monitor statuses used by the current implementation include draft, ready, active, stopped, failed, and error. Zone discovery separately uses pending, running, completed, failed, and not_required.

Validation failures return HTTP 400 with a stable shape:

{
    "error": "Validation failed",
    "violations": [
        {
            "field": "intervalSeconds",
            "message": "This value should be greater than or equal to 10."
        }
    ]
}

Project structure

.
├── assets/
│   ├── controllers/monitor_controller.js   Dashboard behavior and API client
│   └── styles/app.css                      Small layer over Bootstrap
├── config/
│   ├── packages/messenger.yaml             Async and failed transports
│   └── services.yaml                       Viagogo service configuration
├── docs/                                   README screenshots
├── migrations/                             Monitor and Messenger database schema
├── scripts/
│   ├── discover-zones.mjs                  Playwright zone mapping
│   ├── fetch-session.mjs                   Interactive cookie bootstrap
│   └── format-staged.mjs                   Pre-commit formatter
├── src/
│   ├── Controller/                         Dashboard and monitor API
│   ├── Dto/Monitor/                        Request normalization and validation
│   ├── Entity/Monitor.php                  Persisted monitor state
│   ├── Message/                            Queue message contracts
│   ├── MessageHandler/                     Check and discovery workers
│   └── Service/
│       ├── Notification/                    Discord embed construction
│       └── Viagogo/                        Request, parsing, filters, and links
├── templates/                              Twig dashboard templates
├── tests/                                  PHPUnit unit and controller tests
└── viagogo-scraper-lab/                    Separate experimental scraper harness

The viagogo-scraper-lab/ directory predates the integrated service and remains useful for isolated fetching and parser experiments. It has its own dependencies and README.

Development and verification

Tests

php bin/phpunit

The suite covers the dashboard, request DTO validation, payload construction, listing parsing, exact-listing URL generation, and Discord payload composition.

Formatting

npm run format:check
npm run format

The repository uses Prettier for JavaScript, JSON, Markdown, CSS, Twig, YAML, and PHP. A local pre-commit hook can format staged supported files through npm run format:staged.

Additional checks

php bin/console lint:container
node --check scripts/discover-zones.mjs
php bin/console debug:router

Database and queue inspection

php bin/console doctrine:migrations:status
php bin/console messenger:stats
php bin/console messenger:failed:show

Operations and deployment

For anything beyond local use:

  1. Set APP_ENV=prod, disable debug mode, and use a strong APP_SECRET.

  2. Put the application behind HTTPS and authentication. The current dashboard and API are not access-controlled.

  3. Store DATABASE_URL and DISCORD_DSN in a secret manager or injected environment variables.

  4. Persist var/viagogo/ securely if browser profiles and cookies must survive deployments.

  5. Run database migrations during deployment.

  6. Compile production assets:

    php bin/console asset-map:compile
  7. Run messenger:consume async under systemd, Supervisor, a container orchestrator, or another process manager with automatic restart behavior.

  8. Monitor the failed transport and application logs.

  9. Keep check intervals conservative; every active monitor creates recurring external requests.

Troubleshooting

The dashboard works, but no prices or Discord alerts appear

  • Confirm php bin/console messenger:consume async -vv is running.
  • Check lastError on the monitor card or through GET /api/monitors/{id}.
  • Inspect php bin/console messenger:stats and php bin/console messenger:failed:show.
  • Verify that var/viagogo/viagogo-cookie.txt exists and is not empty.

New PHP changes do not affect alerts

Messenger workers are long-running processes and retain the old classes in memory. Restart them:

php bin/console messenger:stop-workers
php bin/console messenger:consume async -vv

Zone discovery reports a challenge

The worker intentionally fails rather than attempting to bypass the challenge. Run interactive discovery using the same locale and profile, solve the challenge, then select Retry discovery in the dashboard:

npm run discover-zones -- --fresh --locale=en-US 'EVENT_URL'

Zone discovery returns no usable zones

  • Confirm the event actually exposes zone filters.
  • Retry without stale URL filters; the script clears them by default.
  • Use --include-sections when the desired controls are section-level labels.
  • Inspect the HTML and JSON snapshots in var/viagogo/zone-discovery/.
  • Viagogo DOM changes may require selector updates in scripts/discover-zones.mjs.

Database connection fails on port 5432

The included Compose override maps PostgreSQL to 127.0.0.1:5433. Use port 5433 in DATABASE_URL, or change the Compose port mapping to match the environment.

A price is displayed with the seller's currency

The current parser pairs rawPrice with Viagogo's listingCurrencyCode. For cross-currency listings, Viagogo may return a buyer-displayed price in EUR while listingCurrencyCode reflects the seller's GBP or USD listing. Until the parser uses buyerCurrencyCode, treat cross-currency labels as a known correctness limitation.

Discord receives duplicate or delayed alerts

  • Ensure only the intended number of Messenger workers is running.
  • Check for delayed or failed ChatMessage records in the Doctrine transport.
  • Remember that an alert is queued separately after the listing check; it may arrive slightly later.

Limitations and responsible use

  • The listing request and zone discovery depend on undocumented Viagogo behavior and can break without notice.
  • Cookies expire and may need manual renewal.
  • Anti-bot challenges require a human-controlled browser. This project does not include automated challenge solving.
  • Zone discovery depends on visible labels, DOM structure, and URL/request side effects.
  • Currency handling currently prefers the seller's listingCurrencyCode; see troubleshooting.
  • Prices are rounded to integers before comparison, so sub-unit changes are ignored.
  • Alerts compare the first listing's price, not listing identity or the complete result set.
  • There is no authentication, authorization, rate limiting, audit log, or multi-user isolation.
  • Deleting a monitor is permanent.
  • The UI polls the monitor list every five seconds while the tab is visible.
  • Use conservative intervals and comply with all applicable website terms, laws, and policies.

License

composer.json currently declares this repository as proprietary, and no open-source license is provided. No permission to copy, modify, or redistribute the code is granted by default.

About

Self-hosted Viagogo ticket monitor with zone filtering, automated price checks, and rich Discord alerts.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages