Skip to content

turnepf/Show-Picker-Club

Repository files navigation

Show Picker Club

A shared TV-show and movie tracker for a small club. Each member maintains four ranked lists (Watching, Awaiting, Recommending, Up Next); the home page surfaces what everyone is watching and exposes a per-member iCalendar feed for upcoming premiere dates.

Live at showpicker.club.

There are native iOS, tvOS, and watchOS apps. They share a ShowPickerCore Swift package (at the repo root) and are opened via the ShowPickerClub.xcworkspace workspace. iOS and tvOS ship as a single universal App Store app (one bundle id, net.patrickturner.showpickerios, running on iPhone + Apple TV).

At a glance

  • Multi-tenant. One deployment, many members. Each member is a slug (/whitt, /patrick) with their own lists; they sign in with a one-time code (text or email) or Sign in with Apple.
  • Auto-enriched. OMDB supplies IMDB ratings and canonical titles; TMDB supplies cast, next-season dates, finale dates, series-ended flags, and genres.
  • Social. Suggest a show to another member and share a show across lists.
  • Vibe. /vibe profiles each member's taste across 27 trait dimensions and assigns one of seven cluster identities.
  • Calendar feed. webcal://showpicker.club/calendar/<slug>.ics keeps upcoming premieres and finales in Apple Calendar / Google Calendar / Fantastical.
  • PWA. Installable to home screen.

Tech stack

  • Frontend: Static HTML + vanilla JS, no build step. Service worker for PWA support.
  • API: Cloudflare Pages Functions (file-system-routed JavaScript handlers).
  • Database: Cloudflare D1 (SQLite at the edge).
  • Enrichment: OMDB API + TMDB API.
  • Vibe trait scoring: Claude API (Sonnet 4.6 with prompt caching), admin-triggered batch only.
  • Auth: One-time codes (SMS via Twilio Verify, email via Resend) plus Sign in with Apple; HttpOnly session cookies, 30-day expiry.

Project structure

Only public/ (static assets) and functions/ (Pages Functions) are deployed. Everything else — schema.sql, docs/, workflow files, backups — stays out of the build output dir so it can't be served.

├── public/                     Deployed static assets
│   ├── index.html              Landing + per-member SPA
│   ├── vibe.html               Member taste profiles
│   ├── reporting.html          Admin metrics (auth-gated)
│   ├── setup.html              Admin: create new member (secret-gated)
│   ├── url-cleanup.html        Admin: fix missing network URLs (secret-gated)
│   ├── vibe-admin.html         Admin: batch-score trait vectors (secret-gated)
│   ├── manifest.json           PWA manifest
│   ├── sw.js                   Service worker
│   ├── _headers                Security headers (CSP, HSTS, etc.)
│   └── _redirects              SPA fallback + legacy slug rewrites
├── functions/
│   ├── api/                    All /api/* endpoints
│   ├── auth/                   Login, logout, session check
│   ├── calendar/[slug].js      Per-member iCalendar feed
│   └── _shared/                Reusable helpers (auth, enrichment, vibe traits, vibe clusters)
├── docs/                       Product + architecture documentation
├── schema.sql                  Starter DB schema (not deployed)
├── wrangler.toml               Cloudflare config
└── .github/workflows/
    ├── deploy.yml              Push-to-main → Cloudflare Pages
    └── backup.yml              Daily D1 dump → Google Drive

Routing is documented in docs/ARCHITECTURE.md.

Setup

Prerequisites

Steps

  1. Clone and create the D1 database.

    git clone https://github.com/turnepf/Shows.git
    cd Shows
    wrangler d1 create shows-db

    Copy the returned database_id into wrangler.toml.

  2. Apply the schema. Note that schema.sql is the starter shape; columns and tables added over time (added_by, enriched_at, genres, sessions.last_seen_at, etc.) are documented in docs/ARCHITECTURE.md. New deployments should apply the schema and then any subsequent ALTERs from that doc.

    wrangler d1 execute shows-db --remote --file=schema.sql
  3. Seed at least one member + a contact for login. New members are usually created via the admin /setup page once you're deployed; bootstrap by hand the first time. Login is by one-time code, so seed an email and/or phone the member will receive the code at.

    INSERT INTO members (slug, name, first_name) VALUES ('patrick', 'Patrick Turner', 'Patrick');
    INSERT INTO member_emails (email, member_slug, is_primary) VALUES ('patrick@example.com', 'patrick', 1);
    INSERT INTO member_phones (phone, member_slug, is_primary) VALUES ('+13365550123', 'patrick', 1);
  4. Set API key secrets. Use printf (not echo) so trailing newlines don't break runtime calls.

    printf "your-omdb-key"    | wrangler pages secret put OMDB_API_KEY    --project-name shows
    printf "your-tmdb-key"    | wrangler pages secret put TMDB_API_KEY    --project-name shows
    printf "your-tmdb-token"  | wrangler pages secret put TMDB_TOKEN      --project-name shows
    # Optional — only if you wire up the corresponding features:
    printf "sk-ant-..."       | wrangler pages secret put ANTHROPIC_API_KEY --project-name shows  # /vibe-admin trait scoring
    printf "your-watchmode"   | wrangler pages secret put WATCHMODE_API_KEY --project-name shows  # auto URL deep-links
    printf "ACxxxx"           | wrangler pages secret put TWILIO_ACCOUNT_SID --project-name shows # SMS
    printf "your-twilio-tok"  | wrangler pages secret put TWILIO_AUTH_TOKEN --project-name shows
    printf "+1336..."         | wrangler pages secret put TWILIO_PHONE_NUMBER --project-name shows
    printf "MGxxxx"           | wrangler pages secret put TWILIO_MESSAGING_SERVICE_SID --project-name shows

    Watchmode deep-link lookups default to the US region. To target another country, set the non-secret WATCHMODE_REGION var (e.g. GB, CA) in the Cloudflare Pages dashboard → Settings → Environment variables.

    Reviewer / demo login (optional). The app is invite-only with no public sign-up, so App Review needs a way in. Create a throwaway demo member with an email (in member_emails), then set these two secrets to let that one email log in with a fixed code — no SMS/email round-trip needed:

    printf "demo@example.com" | wrangler pages secret put DEMO_LOGIN_EMAIL --project-name shows
    printf "424242"           | wrangler pages secret put DEMO_LOGIN_CODE  --project-name shows  # 6 digits → iOS auto-submits

    The bypass (in functions/auth/login.js) only ever unlocks the configured email and only when both secrets are set; leave them unset to disable it. Put the same email + code in App Store Connect → App Review Information.

  5. Create the Pages project and do the first deploy.

    wrangler pages project create shows
    wrangler pages deploy public --project-name shows

    pages_build_output_dir = "public" in wrangler.toml ensures only public/ is uploaded. Functions at the repo root are picked up automatically. Never pass . as the deploy directory — it would publish secrets, backups, and .env files.

  6. (Optional) Add a custom domain via the Cloudflare dashboard → Pages → your project → Custom domains.

Deploys and backups

  • Production deploy is automatic on push to main via .github/workflows/deploy.yml. The workflow runs wrangler pages deploy, then probes a few endpoints to confirm secrets aren't leaking and auth gates are still in place.
  • Daily D1 backup at 03:00 UTC via .github/workflows/backup.yml. Dumps the full SQL to Google Drive (gdrive:Shows-Backups/), prunes backups older than 30 days, and prunes failed_logins rows older than 7 days from D1.

Both workflows require these GitHub Actions secrets:

Secret Used by Purpose
CLOUDFLARE_API_TOKEN deploy.yml, backup.yml Pages:Edit + D1:Edit on the account
CLOUDFLARE_ACCOUNT_ID deploy.yml, backup.yml The account ID
RCLONE_CONF backup.yml Full ~/.config/rclone/rclone.conf with gdrive token

License

MIT

About

Shared TV show and movie tracker built on Cloudflare Pages + D1, now includes iOS and TVOS.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors