Skip to content

Repository files navigation

EuroSnapBot

Project Status

EuroSnap was a working Telegram bot I implemented to monitor Eurostar Snap availability and send alerts when matching offers appeared.

The bot and website have now been taken offline after Eurostar requested that I stop using the Eurostar name/term in this project. Because of that, I do not recommend cloning this repository to run a commercial service.

If you want to learn from the code or adapt the idea for a personal/non-commercial project, feel free to clone it and study the architecture. The repository is kept as a technical portfolio project and code reference.

This README explains what the project did, how it worked, how the code is organized, and which files controlled the main product behavior.

Last updated:

  • 2026-07-01

1. What This Project Is

EuroSnap is a Telegram bot + Firebase backend that monitors Eurostar Snap availability and sends Telegram alerts when matching offers appear.

Main product promise:

  • the user chooses a route
  • chooses a date
  • chooses a time preference
  • optionally chooses seat type on supported routes
  • optionally sets a max price
  • the bot checks Eurostar Snap automatically
  • the bot sends an alert with a purchase link when matching availability is found

There is also:

  • a Firebase-hosted public website
  • a Firebase-hosted admin dashboard
  • Stripe billing for paid subscriptions
  • a small separate support-bot codebase
  • Git version history in a private GitHub repository, with local secrets ignored
  • an encrypted backup flow for local secrets

2. High-Level Architecture

Main stack:

  • Firebase Functions v2
  • Firestore
  • Firebase Hosting
  • Telegram Bot API
  • Stripe subscriptions
  • TypeScript for the main bot/backend

Main codebases:

  • functions/
    • the main TypeScript backend
  • public/
    • the public marketing site source
    • local-only / ignored by git for the current public-safe repository setup
    • the live Firebase Hosting site is currently deployed as a temporary offline page
  • video/
    • standalone pure-Python ad video renderer (see section 29)
    • horizontal 1920×1080 ad for the product ("promo/pub")
    • built with Pillow + ffmpeg only — no browser, no Remotion
    • currently a work-in-progress we are iterating on together
  • video-remotion/
    • fresh Remotion implementation of the EuroSnap promo video
    • built from the current video/render_v4.py concept, not from the deleted old Remotion/MCP project
    • main composition: EurosnapAd, 1920×1080, 30fps, 750 frames
    • first three pre-phone scenes are equal length: 2.5 seconds each
    • phone scene is 13 seconds with a faster conversation, longer wake, and smooth notification zoom; CTA closes in the final 5 seconds
  • support_functions_py/
    • Python Firebase Functions codebase for support tooling
  • supporteurosnapbot/
    • local standalone Python support bot using SQLite; useful as reference/legacy support implementation

Project operations / recovery docs in the repo root:

  • GIT_WORKFLOW.md
    • how to commit, push, inspect history, restore files, create safe branches, and revert bad commits
  • SECRETS_BACKUP.md
    • why raw secrets are intentionally not committed and how encrypted secret backups should be handled
  • BACKUP_RECOVERY.md
    • exact commands to decrypt and extract the encrypted backup

Sensitive local-only paths intentionally ignored by git:

  • .config/
  • .firebase-tools/
  • .local/
  • .tooling/
  • public/
  • secure_backups/

Encrypted backup local-only path:

  • secure_backups/secrets-backup-2026-04-18.tar.gz.enc

Important:

  • the encrypted backup file is NOT committed
  • the decryption password is NOT stored in the repository
  • the password must live in an external password manager

Hosting / URLs:

  • public site: https://eurosnap-422cf.web.app (currently temporary offline page)
  • billing entry domain: https://billing.eurosnapbot.com
  • checkout entry: https://billing.eurosnapbot.com/checkout?chatId=...
  • portal entry: https://billing.eurosnapbot.com/manage?chatId=...

3. What Runs In Production

Main Functions exported from functions/src/index.ts:

  • checkSnapScheduled
    • scheduled every 10 minutes
    • processes due shared search jobs
    • sends 1h reminders
    • handles subscription ending-soon reminders
    • handles expired premium access
  • telegramWebhook
    • receives Telegram webhook updates
    • drives the full user conversation flow
  • adminDashboard
    • serves the admin dashboard HTML
  • stripeCheckout
    • creates a Stripe Checkout Session URL and redirects user
  • stripePortal
    • creates a Stripe Billing Portal session URL and redirects user
  • stripeWebhook
    • receives Stripe events and updates Firestore / Telegram state
  • stripePortalReturn
    • return point after Stripe Billing Portal, used to resync status and redirect back to the bot

4. Main User Experience

Non-premium user

Current behavior:

  • a new user can use /start, /help, /support, and open /menu
  • the user can build an alert flow almost fully
  • if the user is not premium, the paywall happens at the moment the alert would actually be created
  • the alert draft is stored, but not created yet
  • after successful payment:
    • the user receives the premium activation message
    • then the pending alert is created automatically
    • then the normal “alert created” message is sent

This paywall placement is intentional. It was moved later in the flow so new users can understand how the bot works before paying.

Premium user

If the user has premium access:

  • they can create alerts normally
  • they can have up to MAX_ACTIVE_ALERTS = 3 active alerts at once
  • they can manage subscription through Stripe Portal

End of subscription

When a paid subscription truly ends:

  • premium access is removed
  • active alerts are stopped
  • the user is effectively pushed back into the blocked non-premium path
  • /menu should lead back to the premium-required flow

Cancel during the month

If the user cancels but the paid period is not over:

  • access remains active until the true end date
  • the bot sends an immediate cancellation message
  • within the last 24 hours before the actual end:
    • the bot sends an ending-soon reminder
  • on the final end date:
    • access is removed
    • alerts are stopped

5. Core Product Concepts

5.1 Alert

An alert belongs to one Telegram chat and contains:

  • route
  • date
  • time selection
  • seat preference
  • optional unassigned departure windows
  • optional max price

Stored in Firestore collection:

  • alerts

Main type:

  • AlertRecord in functions/src/types.ts

5.2 Shared search job

The bot does not check Eurostar once per user. It groups multiple user alerts into shared jobs.

Important refactor already done:

  • old grouping: route + date + time slot
  • current grouping: route + date

Then each user alert is filtered after the fetch by:

  • seat preference
  • time slot
  • unassigned departure windows
  • max price

Stored in Firestore collection:

  • searchJobs

Main type:

  • SearchJobRecord in functions/src/types.ts

5.3 Telegram user state

Stored in Firestore collection:

  • telegramUsers

Main type:

  • TelegramUserRecord

This stores:

  • Telegram identity
  • whether the user has started
  • current pending alert draft
  • Stripe IDs and subscription summary
  • premium flags
  • notification dedupe keys

6. Route Model

Routes are configured in:

  • functions/src/config.ts

Important constants/functions there:

  • ROUTE_PRESETS
  • ROUTE_ORDER
  • listRouteDirections()
  • parseRouteDirection()
  • routeSupportsUnassignedSeats()
  • getMonitoringWindowDays()
  • createTripConfig()

Current route count:

  • 20 directions total

Current routes with explicit unassigned-seat support:

  • paris_brussels
  • brussels_paris
  • amsterdam_brussels
  • brussels_amsterdam

Important:

  • Not every Snap route supports the allocated / unassigned distinction in the bot.
  • That distinction was enabled only on routes that were confirmed compatible.
  • If adding this feature to more routes later, verify the actual Snap page behavior first.

Route kinds:

  • london
  • continental

This matters because:

  • site behavior differs
  • currency/market defaults differ

7. Time Logic And Monitoring Frequency

All scheduling logic lives in:

  • functions/src/policy.ts

The Cloud Scheduler trigger itself runs:

  • every 10 minutes

But each route/date is not checked every 10 minutes all the time. The function decides whether a shared job is due.

Current monitoring windows:

  • allocated seats on London routes: starts at J-14
  • allocated seats on continental routes: starts at J-12
  • unassigned seats on supported continental routes: starts at J-21

Current cadence once a source is inside its monitoring window:

  • J-21 to J-8: every 1h
  • J-7 to J-1: every 10 min
  • when a future alert first reaches its opening day, the first check is scheduled at the 03:30 probe before daytime cadence resumes

For any seat alerts on routes that support unassigned seats:

  • unassigned availability can be checked from J-21
  • allocated availability starts later, at J-12 for continental routes or J-14 for London routes

Important operational detail:

  • when a new alert is created and it is already inside the active monitoring window, the first check is scheduled immediately on the next scheduler tick
  • after that first run, the alert falls back to the normal cadence above
  • active legacy alerts can be resynced to the current nextEligibleCheckAt by the scheduler if they were created under an older timing policy

Useful-hours policy:

  • there is still a day window
  • there is also a 03:30 night probe
  • the scheduler wakes every 10 min, but many jobs will be skipped as “not due”

If changing frequency only:

  • edit functions/src/policy.ts
  • keep index.ts scheduler frequency unless you intentionally want a platform-level change

8. Seat Type Feature

This is one of the most subtle product areas.

Seat preference values:

  • allocated
  • unassigned
  • any

Type is defined in:

  • functions/src/types.ts

Allocated seats

Behavior:

  • uses the normal Snap search page
  • works with Morning / Afternoon / Any time
  • user is treated as having a guaranteed seat

Unassigned seats

Behavior:

  • uses the Snap unallocated page
  • intended for routes where Snap exposes more precise train data
  • user is warned that a seat is not guaranteed

Current UX:

  • on supported routes, after date selection the bot asks for seat type
  • if user chooses unassigned, the bot asks for numbered 2-hour departure windows
  • user replies with one or more numbers, for example:
    • 1,3,5

Configured departure bands in functions/src/config.ts:

  • 1 = 06:00-07:59
  • 2 = 08:00-09:59
  • 3 = 10:00-11:59
  • 4 = 12:00-13:59
  • 5 = 14:00-15:59
  • 6 = 16:00-17:59
  • 7 = 18:00-19:59
  • 8 = 20:00-21:59

Approximate journey times currently documented for the seat prompt:

  • Paris ↔ Brussels: about 1h25
  • Amsterdam ↔ Brussels: about 1h55

Important subtlety:

  • allocated and unassigned do not come from the same Snap page
  • allocated uses search
  • unassigned uses unallocated
  • any seat type means the backend may need both sources

9. What The Bot Actually Checks On Eurostar Snap

There are two provider layers:

  • functions/src/providers/snapHttp.ts
    • main HTTP parser
    • checkSnapHttp() for allocated/search page
    • checkSnapUnallocatedHttp() for unassigned page
  • functions/src/providers/snapHeadless.ts
    • fallback provider when allocated/search parsing is inconclusive

Important reality:

  • the bot does not scrape the exact same final browser view in all cases
  • on some routes, Snap search results are slot-based
  • on some continental flows, the unallocated page exposes exact trains

Current provider usage in scheduler:

  • allocated source if any active alert requires it
  • unassigned source if supported route and at least one active alert requires it
  • results are then filtered per alert

This filtering happens in:

  • functions/src/alertMatching.ts

10. Filtering Logic

functions/src/alertMatching.ts is critical.

It determines whether a raw provider result truly matches a user alert.

Filters applied:

  • seat preference
  • unassigned departure windows
  • time slot
  • max price

Order:

  1. seat preference
  2. unassigned departure windows
  3. time slot
  4. max price

Important nuance:

  • shared jobs fetch broader data
  • alert-level filtering is where user-specific behavior actually happens

If alerts seem wrong, this file is one of the first places to inspect.

11. Pending Alert Draft Flow

This is important because the paywall now sits before final creation.

Main files:

  • functions/src/bot.ts
  • functions/src/alertCreation.ts
  • functions/src/state.ts
  • functions/src/billing.ts

Flow:

  1. user picks route/date/etc.
  2. selections are stored in pendingAlertDraft
  3. if user is premium:
    • alert is created immediately
  4. if user is not premium:
    • draft is preserved
    • premium-required message is shown
  5. after payment:
    • Stripe activation flow runs
    • pending draft is converted into a real alert

This is an intentional product behavior and should not be broken accidentally.

12. Stripe / Billing Architecture

Main file:

  • functions/src/billing.ts

Other important files:

  • functions/src/telegram.ts for billing-related messages
  • functions/src/subscription.ts for dashboard billing/access categorization
  • functions/src/index.ts for the HTTP endpoints and scheduler hooks

Key ideas:

  • Firestore is not the billing source of truth
  • Stripe is the billing source of truth
  • Firestore stores a local summary used by the bot and dashboard

Main stored user billing fields:

  • stripeCustomerId
  • stripeSubscriptionId
  • subscriptionStatus
  • subscriptionCancelAtPeriodEnd
  • subscriptionCancelAt
  • subscriptionEndedAt
  • subscriptionAmountMinor
  • lastInvoiceAmountPaid
  • promoCode
  • premiumUntil
  • isPremium
  • premiumBypassEnabled

Important helper logic:

  • hasPremiumAccess()
  • getEffectiveSubscriptionEndIso()
  • handleExpiredPremiumAccesses()
  • handleSubscriptionsEndingSoon()
  • reconcilePremiumAccess()
  • processStripeWebhook()

Important Stripe details already supported:

  • monthly subscription
  • live/test separation handled by env secrets and STRIPE_PRICE_ID
  • cancel at period end
  • renew before end
  • ending-soon reminder in last 24h
  • end-of-subscription removal
  • promo code support
  • portal return sync

Current promo:

  • taktik
  • configured to be first-time only

Billing custom domain:

  • public user-facing billing links use billing.eurosnapbot.com
  • internal webhook still uses the Cloud Function URL

13. Premium Access Edge Cases

These are easy to break and worth remembering:

Premium bypass

premiumBypassEnabled gives access without Stripe. Used for manual/admin testing or free access users.

This affects:

  • hasPremiumAccess()
  • dashboard user categories
  • whether expiration logic applies

Expiration behavior

When subscription ends:

  • isPremium is removed
  • hasStarted is reset to false
  • pendingAlertDraft is cleared
  • alert-related last state is cleared
  • active alerts are stopped

This is meant to make the user behave like a blocked user again.

Ending-soon reminder

Reminder is sent only when:

  • user still has access
  • user is scheduled to end
  • effective end is within 24h
  • reminder has not already been sent for that end timestamp

This logic uses:

  • premiumUntil
  • subscriptionCancelAt
  • dedupe keys on the user record

14. Telegram Message / Keyboard Layer

All user-facing copy and keyboards are centralized in:

  • functions/src/telegram.ts

This file contains:

  • text formatting helpers
  • all bot messages
  • all inline keyboards
  • Telegram API send helpers
  • premium/billing copy
  • seat/time prompts
  • offer notification formatting

If the user asks:

  • “change wording”
  • “add emoji”
  • “bold this”
  • “change buttons”

then functions/src/telegram.ts is usually the first file to edit.

Important:

  • many strings are part of tested flows
  • after message changes, run tests
  • richer prompts and richer alert/detail messages now rely on Telegram HTML parse mode, not legacy Markdown
  • if bold text appears raw in Telegram, check the corresponding sendTelegramMessage(...) call and confirm it passes parse mode "HTML"

15. Bot Conversation Orchestration

Main file:

  • functions/src/bot.ts

This file is the state machine of the Telegram product.

It handles:

  • /start
  • /menu
  • /help
  • /support
  • /reset
  • text input parsing
  • callback query parsing
  • when to ask date / seat / slot / price
  • when to enforce paywall

If the user asks:

  • “change the user flow”
  • “move the paywall”
  • “change where a question appears”
  • “change how menu pages work”

then bot.ts is the main entry point.

Important recent UX detail:

  • prompts that contain HTML emphasis such as <b>...</b> must be sent with Telegram parse mode "HTML"
  • if Telegram shows raw tags instead of bold text, inspect the exact sendTelegramMessage(...) call in bot.ts or index.ts

16. Admin Dashboard

Main endpoint:

  • functions/src/admin.ts

Data snapshot building:

  • functions/src/state.ts
  • functions/src/subscription.ts
  • functions/src/usage.ts

The dashboard includes:

  • users
  • alerts
  • shared jobs
  • KPI cards
  • billing/access categories
  • filtering
  • grouped shared jobs behavior
  • counts that update when filters change

Important user categories:

  • free_access
  • paid_subscription
  • promo_free
  • access grouped as:
    • active renewing
    • active canceling
    • inactive

Recent simplification intent:

  • the dashboard should stay practical, not overly technical
  • many internal categories are collapsed for readability

If changing dashboard behavior:

  • UI and rendering are mostly in admin.ts
  • filtering logic is split between admin.ts, state.ts, and subscription.ts

17. Firestore Collections

Main collections:

  • telegramUsers
  • alerts
  • searchJobs

Support collections in Python support function:

  • supportUsers
  • supportTickets
  • supportMessageLinks

General rule:

  • alerts are per-user
  • searchJobs are shared scheduler units
  • telegramUsers stores user identity + entitlement + pending state

18. Files And What They Control

Core backend

  • functions/src/index.ts
    • exported Functions and scheduler processing pipeline
  • functions/src/types.ts
    • global types and record shapes
  • functions/src/config.ts
    • route list, route capabilities, trip keys, monitoring window basics, seat bands
  • functions/src/policy.ts
    • monitoring cadence and useful-hours logic
  • functions/src/state.ts
    • Firestore reads/writes and snapshot building
  • functions/src/alertCreation.ts
    • pending draft evaluation and real alert creation
  • functions/src/alertMatching.ts
    • raw provider result filtering per alert
  • functions/src/bot.ts
    • Telegram conversation flow
  • functions/src/telegram.ts
    • messages, keyboards, Telegram API helper calls
  • functions/src/billing.ts
    • Stripe integration and premium lifecycle
  • functions/src/subscription.ts
    • dashboard billing/access categories and billing summaries
  • functions/src/admin.ts
    • admin dashboard HTML and request handling
  • functions/src/usage.ts
    • usage/admin metrics helpers

Snap providers

  • functions/src/providers/snapHttp.ts
    • HTTP parsing for search and unallocated Snap pages
  • functions/src/providers/snapHeadless.ts
    • headless fallback when HTTP allocated parsing is inconclusive

Public site (local-only)

The public/ folder exists locally but is intentionally ignored by git in the current GitHub setup. Do not push website source unless this policy changes.

  • public/index.html
    • main marketing site
  • public/logo.png
    • favicon / site icon currently in use
  • public/eurosnap-logo.png
    • additional logo asset

Infra/config

  • firebase.json
    • functions codebases
    • hosting setup
    • /manage and /checkout rewrites
  • firestore.rules
  • firestore.indexes.json

Stripe docs in repo

  • STRIPE_COMMERCIAL_CHECKLIST.md
  • HOW_TO_USE_STRIPE.md

Support

  • support_functions_py/main.py
    • Python Firebase support function
  • supporteurosnapbot/supporteurosnapbot.py
    • standalone support bot with SQLite; useful reference but separate from main bot

19. Testing

Test framework:

  • Vitest

Command:

npm -C functions test

Build command:

npm -C functions run build

Key test files:

  • functions/src/bot.test.ts
  • functions/src/telegram.test.ts
  • functions/src/billing.test.ts
  • functions/src/billing-flow.test.ts
  • functions/src/alertMatching.test.ts
  • functions/src/state.test.ts
  • functions/src/config.test.ts
  • functions/src/policy.test.ts
  • functions/src/subscription.test.ts
  • functions/src/providers/snapHttp.test.ts
  • functions/src/usage.test.ts

Whenever changing:

  • route logic
  • seat logic
  • price logic
  • billing flow
  • dashboard categorization

run at least:

  • build
  • full test suite

20. Deployment

Functions

From repo root:

npx firebase-tools deploy --only functions

Or target specific functions:

npx firebase-tools deploy --only functions:telegramWebhook,functions:checkSnapScheduled

Hosting

npx firebase-tools deploy --only hosting

Important subtlety:

  • Hosting has rewrites pinned to Functions:
    • /manage -> stripePortal
    • /checkout -> stripeCheckout
  • so a hosting deploy may also involve pinned function behavior in Firebase tooling

21. Secrets / Environment

Defined Firebase secrets:

  • TELEGRAM_BOT_TOKEN
  • ADMIN_DASHBOARD_TOKEN
  • STRIPE_SECRET_KEY
  • STRIPE_WEBHOOK_SECRET

Important env/config expectations:

  • STRIPE_PRICE_ID
  • optional APP_BASE_URL
  • optional BILLING_BASE_URL
  • optional TELEGRAM_BOT_URL
  • optional HEADLESS_FALLBACK_ENABLED
  • optional Eurostar gateway/base/product-family settings

Do not store secret values in this README.

22. Current Known Product Rules

These are intentional and should not be changed casually:

  • max active alerts per user = 3
  • scheduler frequency = every 10 min
  • allocated seats on London routes monitor from J-14
  • allocated seats on continental routes monitor from J-12
  • unassigned seats on supported continental routes monitor from J-21
  • monitored sources use 1h cadence from their opening day to J-8
  • all routes use 10 min cadence from J-7 to J-1
  • non-premium users can explore flow before paywall
  • paywall happens when creation would happen
  • after payment, pending alert is created automatically
  • dashboard should stay simple and operator-friendly
  • only confirmed routes expose unassigned seat type

23. Common Change Recipes

If user says “change route list”

Start with:

  • functions/src/config.ts

Then verify:

  • menu order
  • route support flags
  • market/currency behavior
  • provider parsing still works

If user says “change frequency”

Edit:

  • functions/src/policy.ts

Usually do not change:

  • checkSnapScheduled cron in index.ts

If user says “change paywall behavior”

Inspect:

  • functions/src/bot.ts
  • functions/src/billing.ts
  • functions/src/alertCreation.ts

If user says “change alert text / button wording”

Edit:

  • functions/src/telegram.ts

If user says “dashboard filters/cards/layout”

Inspect:

  • functions/src/admin.ts
  • functions/src/state.ts
  • functions/src/subscription.ts

If user says “Stripe is wrong”

Inspect:

  • functions/src/billing.ts
  • functions/src/index.ts
  • STRIPE_COMMERCIAL_CHECKLIST.md
  • HOW_TO_USE_STRIPE.md

If user says “matching logic is wrong”

Inspect:

  • functions/src/alertMatching.ts
  • functions/src/providers/snapHttp.ts
  • sometimes functions/src/config.ts

24. Subtleties And Easy Mistakes

24.1 Trip key vs search key

This matters a lot.

  • searchKey
    • shared grouping key
    • currently based on route + date
  • tripKey
    • alert-specific key
    • includes user-facing filters such as:
      • time slot
      • seat preference
      • unassigned departure bands

Breaking this distinction can break deduplication and scheduler sharing.

24.1bis Retroactive timing fixes

There are two layers of timing state:

  • the alert document
  • the shared searchJob document

Changing cadence logic in policy.ts is not enough by itself if old active jobs already have stale nextEligibleCheckAt values.

Relevant code paths:

  • syncSearchJobForSearchKey() in functions/src/state.ts
  • backfillUnstartedSearchJobs() in functions/src/state.ts
  • scheduler entry in functions/src/index.ts

If production timing looks wrong after a policy change:

  • inspect searchJobs.nextEligibleCheckAt
  • inspect alerts.nextEligibleCheckAt
  • inspect recent checkSnapScheduled logs

24.2 Unassigned seat filtering

The bot currently stores and filters numbered departure windows for unassigned alerts. This is not the same as general Morning / Afternoon.

24.3 Dashboard simplification vs real state

The dashboard intentionally simplifies some categories for readability. Firestore may contain richer internal state than the dashboard exposes.

24.4 Premium expiration

Removing premium is not just isPremium = false. There is also user UX reset and active alert stopping.

24.5 Telegram UX changes

Many small copy changes are harmless, but changes to prompt order can affect:

  • pending draft logic
  • callback patterns
  • tests
  • Stripe post-payment continuation

25. Known Improvement Areas

These are not necessarily bugs, but useful engineering notes:

  • functions/src/alertMatching.ts duplicates the unassigned time band ranges instead of importing them from config
  • support system exists in two forms:
    • Firebase Python function
    • standalone local SQLite bot
    • future cleanup could reduce duplication if desired
  • deeper route-specific Snap parsing may still evolve
  • new scheduler/backfill queries should be checked carefully against existing Firestore composite indexes before deploying

26. Short Repo Map

The main backend files are:

  • config.ts
    • routes and shared identifiers
  • policy.ts
    • when checks are allowed
  • state.ts
    • Firestore persistence
  • bot.ts
    • Telegram conversation flow
  • telegram.ts
    • all user-facing messages
  • alertCreation.ts
    • turn draft into alert
  • alertMatching.ts
    • decide whether an offer matches
  • billing.ts
    • Stripe + premium lifecycle
  • index.ts
    • scheduled runner and HTTP entry points
  • admin.ts
    • dashboard

27. Engineering Notes

This repository is already more than a simple bot script. It is a small product backend with:

  • entitlement logic
  • shared workload optimization
  • billing
  • admin operations
  • a public landing page source kept locally

So changes should be made with care:

  • preserve the product flow
  • avoid breaking dedupe logic
  • avoid breaking Stripe lifecycle behavior
  • avoid accidentally regressing dashboard clarity

28. Ad Video Renderer (video/)

This folder holds a standalone promo/ad video renderer for EuroSnap. It is completely independent from the bot, the Firebase Functions, and telegram-ad-video-mcp/. It is a separate pure-Python pipeline: Pillow draws frames, ffmpeg encodes them into MP4.

28.1 Current files

  • video/render_v4.py
    • the current renderer, v4
    • everything (frames, compositing, scenes, output) lives in this one file

28.2 What the script does

High level:

  • renders 600 PNG frames at 1920×1080, 30fps (20 seconds total)
  • calls ffmpeg to encode those PNG frames into a single MP4
  • no browser, no Remotion, no Node, no HTML — just Python + ffmpeg

28.3 Key building blocks

  • ip(f, a, b, va, vb, ease)
    • clamped animation interpolation used by everything that moves
    • applies a cubic-bezier easing, default EXPO = (0.16, 1.0, 0.3, 1.0) — fast out, Apple-style deceleration
  • mask_up(base, txt, font, col, cx, clip_y, clip_h, p)
    • signature motion design primitive
    • text is drawn on a temporary image, then only a vertical band [clip_y, clip_y + clip_h] is pasted back
    • text enters by sliding up from below the invisible band → produces the classic overflow:hidden + translateY reveal
  • _draw_phone(canvas, lf, py)
    • draws the entire Telegram UI (phone shell, screen, header, chat bubbles, keyboard buttons, notification) directly onto an RGB canvas
    • intentionally NOT RGBA — previous versions used surf.putalpha(clip) which overwrote drawn pixels; v4 avoids that by drawing on RGB and doing the fade-in with bld(img, phone_canvas, phone_a) between two RGB images

28.4 Scene structure

Each scene has its own render_sN(lf) function receiving a local frame index lf that resets to 0 at the start of the scene. render_frame(gf) translates the global frame index into a local one and handles 15-frame crossfades between scenes via bld().

Durations in video/render_v4.py:

  • S1, S2, S3, S4 = 60, 120, 330, 135
  • FD = 15
  • derived starts: G1=0, G2=45, G3=150, G4=465
  • TOTAL = 600 (= 20.0s at 30fps)

The three intro phrases are pinned to ~2s each of perceived on-screen time (P1 gf 0–59, P2 gf 45–104, P3 gf 104–164), and the phone is fully visible at gf=165 = 5.5s.

Scene Global frames Duration Content
S1 0–59 2.0s Headline: "Tired of missing Eurostar Snap deals?"
S2 45–164 4.0s Two-phase headline: Phase A "Stop checking for tickets all day." then Phase B "We found the solution for you." — each phrase is ~2s perceived, with a clean fade handoff at f45/f60 (no overlap)
S3 150–479 11.0s Animated iPhone (Telegram) with a four-sided caption layout and a symmetric top/bottom gap (PY_FINAL = 128, bottom caption clip at y=1010..1058): "Welcome to Eurosnap." above the phone, a bigger/bolder "The telegram bot that monitors Eurostar Snap deals in real time." below (F_CAP_BOT = MEDI 40), "You set your / dates, route / & price." stacked on the LEFT with a staggered reveal (f110-148) once the chat is already moving, and "and get / notified / as soon as a / matching deal / appears." stacked on the RIGHT (reveals at f200-250, ~25f earlier than before) after the phone has turned off. The bot is named "Eurosnap" in both the chat header and the lockscreen notification card. The chat now uses realistic multi-choice keyboards: Route offers 3 buttons ("Paris to London" picked
S4 465–599 4.5s €1.99 price, feature bullets, CTA (retimed to 135 frames for a punchier end: accent f0–8, price/sparkle f4–28, bullets f30–76, CTA f80–104, ~30f hold)

The phone is fully visible (end of S2→S3 crossfade) at global frame 180 = 6.0s, matching the intended "phone arrives on second 6" pacing.

Audio is generated procedurally at encode time by _synthesize_audio in video/render_v4.py. It builds a louder pub-tech stem with:

  • a 4-on-the-floor kick at 120 BPM (60 Hz sine burst, exponential decay retriggered every 0.5s via aevalsrc),
  • a hi-hat (aevalsrc white-noise gated every 0.25s through a 7.5 kHz highpass) for motion,
  • a quiet A-minor pad (220 / 277.18 / 329.63 Hz sines with a slow tremolo) as the harmonic bed, and
  • a bright Telegram-style bell pop — two overlapping tones (C7 2093 Hz + G6 1567 Hz, ~40ms stagger, bell envelope) — fired at 13.50 s, exactly when the lockscreen starts waking on the phone.

The whole mix runs through an alimiter at -0.5 dBFS so nothing clips after the boost. The final ffmpeg call mixes the video frames with the generated WAV using -c:a aac -b:a 160k -shortest. If audio synthesis fails for any reason, the encode automatically falls back to a silent MP4.

Top docstring in video/render_v4.py mirrors this breakdown; keep both in sync when retiming.

28.5 Running it

The script now uses paths relative to the video/ folder:

  • frames: video/frames/frame_0000.png through video/frames/frame_0599.png
  • audio: video/out/audio.wav
  • final MP4: video/out/eurostar_snap_bot_v4.mp4
  • fonts: video/fonts/Poppins-*.ttf

Required local tooling:

  • python3 with Pillow
  • ffmpeg, resolved by render_v4.py in this order:
    • bundled imageio-ffmpeg under video/.pydeps
    • imageio-ffmpeg on the current Python path
    • system ffmpeg on PATH

Typical run:

python3 video/render_v4.py

This will:

  1. write 600 PNGs into video/frames
  2. synthesize video/out/audio.wav
  3. spawn ffmpeg to encode video/out/eurostar_snap_bot_v4.mp4

More renderer-specific details are documented in video/README.md.

28.6 Video editing notes

The ad video is under active iteration. When editing render_v4.py:

  • keep each scene's timing table in the docstring in sync with the real ip(lf, a, b, ...) calls, otherwise the scene comments will drift from the actual animation
  • when editing _draw_phone, keep everything on an RGB surface; do NOT reintroduce putalpha on surfaces where text was already drawn, that was the v3 bug
  • when changing global scene lengths, update both the S1, S2, S3, S4 constants AND the scene table in section 29.4 of this README

About

An unofficial student-built alert bot that monitored Snap ticket availability and notified users when matching travel dates became available.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages