A two-person Trakt.tv sync that tracks which watches were together vs solo. Every other Trakt-aware tool I could find treats accounts as independent — none model the couple-watching case. This one does.
The primary person's history pulls automatically from Trakt. After each sync,
items go into a queue; an interactive bin/wtw review lets you mark each one
as watched-together or solo. Joint watches mirror the episode count to the
partner's row. Partner-only watches (things they saw without you) are entered
manually — by direct SQLite insert, or via the /add-watch Claude Code slash
command in .claude/commands/ if you use Claude Code.
Standard Trakt clients have one history per account. Couples watching shows together end up with one person's account "owning" the data and the other person's history full of gaps — or two accounts duplicating each other. This tool keeps one canonical Trakt account but lets you attribute joint watches to a second person manually, with a queue-based UX so you can defer the attribution decision until after sync.
Also doubles as a small demo of:
- Per-event idempotency (Debezium-style watermark + dedupe table) — re-running sync never double-counts, even after partial failures
- Transaction-wrapped writes so crashes never leave half-resolved state
- Hard-fail-on-error pagination (the silent-truncate variant once cost 173 lost events before it was fixed)
Ruby 3.2+ (uses endless method definitions and numbered block params). The
repo's .ruby-version is latest so mise/asdf will install whatever they
consider current; that's fine for any 3.x release.
# 1. Get the code
git clone <repo-url>
cd what-to-watch
# 2. Create a Trakt OAuth app at https://trakt.tv/oauth/applications
# (any name works; "redirect URI" doesn't matter for this read-only use)
# 3. Configure
cp .env.example .env # fill in TRAKT_CLIENT_ID, TRAKT_USERNAME, PRIMARY_USER, PARTNER_USER
cp config.yml.example config.yml # adjust filters per person if you want
# 4. Run — bin/wtw self-bootstraps `bundle install`
bin/wtw export # initial full pull
bin/wtw update # incremental from now on
bin/wtw review # interactively attribute pending items
bin/wtw stats # JSON summarybin/wtw updatepulls Trakt history events since the last watermark.- New events upsert into
watchesfor the primary user. - Each touched item (movie or show) is enqueued in
pending_partner_reviews. bin/wtw reviewopens atty-promptmulti-select (arrow keys + spacebar). Items you check are mirrored to the partner's row; items you leave unchecked are marked as solo.- Mirroring uses the primary's authoritative episode count (with
MAXto preserve any solo-ahead count the partner might have) and never downgrades acompletedpartner row.
SQLite. See db/schema.sql for the full DDL.
Key tables:
watches— current state per (person, type, trakt_id).episodes_watchedis mutated viaupsert_watch(MAX semantics) oradd_episodes(additive).trakt_history_events— per-event idempotency. PK on Trakt'sevent_id.pending_partner_reviews— queue drained bybin/wtw review.- Views:
v_stats,v_genre_profile,v_overlap,v_recent,v_incomplete,v_exclusive.
Query views directly:
sqlite3 -json db/trakt.db "SELECT * FROM v_overlap ORDER BY rating DESC LIMIT 10"See docs/redesign-notes.md for the deferred
event-sourced ledger refactor (replace the mutating watches.episodes_watched
with a projection over an append-only watch_events table). The current code
is correct, but the ledger shape would eliminate the dual-write footgun that
caused the bugs this rewrite fixed.
- No OAuth. Reads use the public profile + client-id header only. Anything
under Trakt's
/sync/*endpoints (e.g.last_activitiesfor a cheap pre-flight) needs Bearer auth and isn't wired up. - Manual entry for the partner. No browser extension, no auto-detect. You
decide attribution at review time. If both people have Trakt accounts and
want auto-detected joint-watch attribution, see
docs/redesign-notes.mdfor the design sketch — good first contribution. - Re-watches over-count via the history endpoint. Trakt's
/users/:u/watched/showsreturns unique episode counts, while/users/:u/historyreturns play events. The fullexportpulls the former and is the authoritative source; incrementalupdatepulls the latter and can over-count if you re-watch episodes. Seedocs/redesign-notes.md.
CLAUDE.md is the canonical project-context file. AGENTS.md and GEMINI.md
symlink to it, so Codex, Gemini CLI, Antigravity, and any other harness that
follows the "look for an instructions file named after me" convention will
pick up the same content. If your harness uses yet another name, just symlink
it: ln -s CLAUDE.md <whatever>.md.
The .claude/commands/*.md files are Claude Code slash commands
(/recommend, /add-watch, etc.) but the content is plain markdown prose
describing workflows — any LLM can read them directly via @, file
attachment, or copy-paste. Move or symlink them into your harness's
recipe/skill directory if it has one.
MIT — see LICENSE.