Skip to content

Repository files navigation

MediaList - Household TV & Movie Watchlist Manager

A modern, mobile-first web application for managing household TV shows and movies to watch. Built with React, Express, and Vite.

Features

  • πŸ“‹ Multiple Lists: Separate watchlists for different household members (Dad, Mom, Both)
  • 🎬 Movies Tab: Every movie across all lists in one place, with an owner chip
  • 🎬 Content Tracking: Track TV shows and movies across streaming services
  • 🎟 Theaters: Mark a film as in-cinemas; it moves to a service when it lands on one
  • πŸ” TMDB Lookup: Search as you type, with artwork, synopsis and true-owner info
  • ✏️ Editable Titles: Tap any card to change service, venue, list or category
  • βš™οΈ Editable Services: Add your own streaming services, hide the built-ins you never use
  • πŸ” Shared Passphrase: Optional household passphrase, asked once per device
  • πŸ”€ Two-Device Safe: Concurrent edits from two phones merge instead of overwriting
  • πŸ”„ Drag & Drop: Reorder your watchlist with intuitive drag-and-drop
  • πŸ“œ Watch History: Keep track of completed titles with dates
  • 🎨 Beautiful UI: Light theme with brand-coloured service marks and small posters
  • πŸ“± Mobile-First: Optimized for phones and tablets with touch gestures
  • πŸ‘† Touch Gestures: Swipe and long-press for quick actions on mobile
  • πŸ“² PWA Ready: Install as an app on your phone or desktop
  • πŸ”Œ Offline Support: Works without internet with cached data
  • β™Ώ Accessible: ARIA labels and keyboard navigation support
  • βœ… Confirmation Dialogs: Prevent accidental deletions
  • πŸ”” Toast Notifications: Clear feedback for all actions
  • ✨ Error Handling: Graceful error recovery with helpful messages

Tech Stack

  • Frontend: React 19, Vite
  • Backend: Express.js
  • State Management: React Context + useReducer
  • Drag & Drop: @dnd-kit
  • Validation: Zod
  • Styling: Pure CSS with CSS variables

Getting Started

Prerequisites

  • Node.js 16+ and npm

Installation

  1. Clone or download the project

  2. Install dependencies:

    npm install
  3. Optionally configure TMDB and friends:

    cp .env.example .env

    Everything in it is optional β€” the app runs without any of it.

  4. Start the development server:

    npm run dev

This runs both the Express backend (port 3002) and Vite dev server (port 5173) concurrently.

To develop against a scratch copy of the data instead of whatever data.json happens to be sitting in the project root:

DATA_DIR=$PWD/.devdata npm run dev
  1. Open http://localhost:5173 in your browser

πŸ“² Installing as PWA

MediaList is a Progressive Web App and can be installed on your device:

Android/Desktop Chrome:

  • Click the install button in the app banner
  • Or use browser menu β†’ "Install app"

iOS Safari:

  • Tap Share button β†’ "Add to Home Screen"

Benefits:

  • πŸ“± Appears as standalone app (no browser UI)
  • πŸ”Œ Works offline with cached data
  • ⚑ Faster loading with cached assets
  • 🏠 Quick access from home screen

See PWA.md for detailed installation instructions and PWA features.

Project Structure

MediaList-fixed/
β”œβ”€β”€ server.js              # Express API server with validation
β”œβ”€β”€ data.example.json      # Sample/testing data; the real list is not in the repo
β”œβ”€β”€ public/
β”‚   β”œβ”€β”€ manifest.json      # PWA manifest (app metadata)
β”‚   β”œβ”€β”€ sw.js              # Service worker (offline support)
β”‚   └── icon.svg           # App icon
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ App.jsx            # Main app component
β”‚   β”œβ”€β”€ main.jsx           # React entry point
β”‚   β”œβ”€β”€ index.css          # Global styles
β”‚   β”œβ”€β”€ components/        # React components
β”‚   β”‚   β”œβ”€β”€ TitleCard.jsx         # Card: poster thumb, chips, touch gestures
β”‚   β”‚   β”œβ”€β”€ TitleDetail.jsx       # Tap a card: full artwork + all editing
β”‚   β”‚   β”œβ”€β”€ TitleList.jsx         # List view with drag-and-drop
β”‚   β”‚   β”œβ”€β”€ AddTitleForm.jsx      # Add modal with TMDB search
β”‚   β”‚   β”œβ”€β”€ SettingsView.jsx      # Services, backups
β”‚   β”‚   β”œβ”€β”€ HistoryView.jsx       # Watch history with grouping
β”‚   β”‚   β”œβ”€β”€ TabBar.jsx            # Bottom navigation
β”‚   β”‚   β”œβ”€β”€ ServiceMark.jsx       # Brand-coloured service tile
β”‚   β”‚   β”œβ”€β”€ StreamingBadge.jsx    # Service mark + label
β”‚   β”‚   β”œβ”€β”€ PassphraseGate.jsx    # Optional household passphrase
β”‚   β”‚   β”œβ”€β”€ Toast.jsx             # Toast notification system
β”‚   β”‚   β”œβ”€β”€ ConfirmDialog.jsx     # Confirmation dialog
β”‚   β”‚   β”œβ”€β”€ ErrorBoundary.jsx     # Error boundary component
β”‚   β”‚   └── InstallPrompt.jsx     # PWA install banner
β”‚   β”œβ”€β”€ context/
β”‚   β”‚   β”œβ”€β”€ MediaContext.jsx    # Titles: state, autosave, conflict merge
β”‚   β”‚   └── ServicesContext.jsx # Streaming service catalogue
β”‚   └── utils/
β”‚       β”œβ”€β”€ storage.js        # API communication, auth, versioning
β”‚       β”œβ”€β”€ merge.js          # Three-way merge for concurrent edits
β”‚       β”œβ”€β”€ tmdb.js           # TMDB client + poster sizes
β”‚       β”œβ”€β”€ services.js       # Service catalogue helpers
β”‚       β”œβ”€β”€ streamingApps.js  # Built-in services
β”‚       β”œβ”€β”€ constants.js      # Shared constants
β”‚       β”œβ”€β”€ validation.js     # Data validation schemas
β”‚       └── serviceWorker.js  # Service worker registration
β”œβ”€β”€ .env.example           # Copy to .env for TMDB / passphrase / backup
└── package.json

Available Scripts

  • npm run dev - Start development servers (frontend + backend)
  • npm run build - Build for production
  • npm run preview - Preview production build
  • npm run lint - Run ESLint

Where the data lives

The real watchlist is not in this repo.

  • Production: /data/data.json on the Fly volume. This is the live list.
  • Local: point DATA_DIR at a scratch directory (.devdata/, gitignored) so experiments never touch anything real.
  • In this repo: data.example.json, two obviously fake records, enough for a fresh clone to run and to document the record shape.

Any watchlist JSON you find in this repo or its history is sample/testing data β€” an old development snapshot, not the maintained list and not kept in sync with anything.

data.json and data copy.json are gitignored so a local working copy never gets committed by accident.

Because the only live copy is the Fly volume, configure the off-box backup β€” see Backups. Local backups/ sit on that same volume and do not protect against losing it.

Configuration

All optional β€” the app runs with none of these set.

Variable Default Purpose
PORT 3002 Server port
DATA_DIR /data in production, repo root otherwise Where data.json, services.json and backups/ live. Point it at a scratch copy to develop without touching the real list.
MEDIALIST_PASSPHRASE unset Shared passphrase for /api. Unset means no auth.
ALLOWED_ORIGINS unset Comma-separated origins allowed to call the API cross-origin. Unset sends no CORS headers, which is right when the app is served from this same server.
BACKUP_MIN_INTERVAL_MS 3600000 Minimum gap between local backup snapshots.
TMDB_ACCESS_TOKEN / TMDB_API_KEY unset TMDB credential. Unset disables title search and artwork; everything else still works.
TMDB_REGION IL Region used for "where does this stream now" lookups.
BACKUP_GITHUB_REPO unset owner/repo for the daily off-box backup.
BACKUP_GITHUB_TOKEN unset Fine-grained PAT with Contents: read and write on that repo.
BACKUP_GITHUB_PATH medialist/data.json Path within the backup repo.

Values can go in a .env file (gitignored) β€” see .env.example.

Set the passphrase on Fly with:

fly secrets set MEDIALIST_PASSPHRASE='your-passphrase'

Concurrency

Both devices hold the whole list and POST it wholesale, so a stale client could silently overwrite the other person's changes. To prevent that:

  • GET /api/titles returns an X-List-Version header (a hash of the content).
  • POST /api/titles sends it back as If-Match. A mismatch returns 409 along with the server's current copy.
  • The client three-way merges its own changes with the server's and retries. Changes this device made win; changes it did not make are taken from the server; deletions are honoured only when this device made them.
  • The list is re-read whenever the tab regains focus.

Backups

Local. DATA_DIR/backups/ holds timestamped snapshots, taken at most once an hour. Retention keeps everything from the last 24 hours plus one snapshot per day for 14 days. GET /api/backup downloads the newest; Settings has a Download Backup button.

Off-box. Local backups sit on the same Fly volume as the data, so they are no help if the volume goes. Set BACKUP_GITHUB_REPO and BACKUP_GITHUB_TOKEN and the server commits data.json to that private repo once a day.

It runs after a save rather than on a timer: Fly stops the machine when idle (auto_stop_machines), so a nightly setInterval would often never fire. Piggybacking on activity means it runs on any day the app is used. Settings shows when it last ran and has a Back up now button.

TMDB

With a credential set, typing a title in the add form searches TMDB and fills in the poster, year and category. List rows show deliberately small thumbnails so plenty of titles fit on a phone; tapping a card opens the detail view with the large artwork, synopsis, runtime and where it currently streams in your region.

Requests are proxied through this server, so the credential never reaches the browser. Posters load directly from image.tmdb.org, which needs no key.

Who actually owns a title

TMDB lists a title under resellers as well as its real owner: "Paramount+ Amazon Channel" means you can buy a Paramount+ subscription through Prime, not that Prime carries it. It also splits owners into tiers ("Paramount Plus Premium", "Netflix Standard with Ads").

Both are filtered out server-side, so "Streaming now in β€Ήregionβ€Ί" shows only the true owner. When that disagrees with the service recorded on a title, the detail view offers a one-tap correction. Providers are matched on TMDB's provider id rather than name, so rebrands (Apple TV+ to "Apple TV", Max back to HBO Max) do not break the mapping.

Editing a title

Tap any card to open its detail view: change the streaming service, flip between Streaming and In theaters, move it to another list, change category or watch style, mark it watched, or delete it.

Venue and service are separate fields on purpose. A film in cinemas has no service yet; when it lands on one, pick the service in the detail view and it moves to Streaming automatically. The detail view also shows what TMDB says is streaming it in your region, which is usually how you find out.

Data Model

Each title has:

  • id: Unique identifier (UUID)
  • title: Show/movie name
  • streamingApp: Platform (netflix, disney, hbo, etc.)
  • category: "tv" or "movie"
  • type: "binge" or "on-going"
  • list: "dad", "mom", or "both"
  • position: Sort order in list
  • status: "watching" or "watched"
  • addedAt: Timestamp when added
  • watchedAt: Timestamp when marked watched (null if watching)

Supported Streaming Platforms

Built in, in picker order: Netflix, HBO Max, Apple TV+, Prime Video, Disney+, Paramount+, Peacock, Starz, AMC+, Israel, Other. Add your own from Settings β€” they are stored in services.json and appear in the picker before Other.

Hulu is absent: it folded into Disney+, and existing Hulu titles are migrated across automatically. Theaters is absent too β€” that is a venue, not a service.

Each service shows as a brand-coloured tile with a short mark. These are brand-coloured abbreviations, not reproductions of the official logos.

Keyboard Shortcuts

  • Escape - Close modals and dialogs
  • Enter - Confirm actions in dialogs
  • Tab - Navigate between interactive elements

Features Explained

Confirmation Dialogs

All destructive actions (delete, permanent delete) now require confirmation to prevent accidents. Dialogs can be dismissed with Escape or confirmed with Enter.

Toast Notifications

Success and error messages appear as toast notifications at the bottom of the screen:

  • βœ… Green for success (e.g., "Added Breaking Bad!")
  • ❌ Red for errors (e.g., "Failed to save")
  • ⚠️ Orange for warnings
  • ℹ️ Purple for info

Error Handling

  • Server-side validation prevents corrupt data
  • Automatic retry on save failures
  • Graceful degradation if backend is unavailable
  • Error boundary catches React errors

Accessibility

  • ARIA labels for screen readers
  • Keyboard navigation support
  • Focus management in dialogs
  • Semantic HTML elements
  • Sufficient color contrast

Browser Support

Modern browsers with ES6+ support:

  • Chrome/Edge 90+
  • Firefox 88+
  • Safari 14+
  • Mobile browsers (iOS Safari, Chrome Mobile)

Development Notes

  • The app saves automatically; every change is persisted immediately
  • Writes take an in-process lock and are version-checked, so two devices cannot silently overwrite each other (see Concurrency)
  • All data is validated with Zod on both client and server
  • Records are normalised on read and write, so schema changes migrate themselves rather than needing a migration step
  • Responsive design adapts to tablet and desktop screens

License

Private household project.

Contributing

This is a personal household project, but feel free to fork and adapt for your own use!

About

Household TV & Movie watchlist manager with drag-and-drop, multiple lists, and watch history tracking

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages