Skip to content

dmty/watchtrail

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

253 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WatchTrail

A local-first media-watch history tool. WatchTrail receives normalized "watch events" from any source (VLC, a browser extension, a script) and stores them in a single SQLite database you own.

What it solves: most media apps track nothing, or track on someone else's server. WatchTrail gives you one append-only log of what you watched, when, and how far you got — on your own disk, in a format that will outlast any app.

Design priorities:

  • One static binary + one file — no daemon, no Docker, no cloud account required.
  • Push, not poll — collectors send events; the server is idle otherwise.
  • Adding a new source requires no changes to core logic.
  • Raw events are stored verbatim so derived data (sessions, stats) can be recomputed as the logic improves.

Requirements

Go 1.26+ to build. No cgo. Runs on macOS, Linux, and Windows.


Build

go build -o watchtrail ./cmd/watchtrail

Run

watchtrail serve
watchtrail serve -config watchtrail.toml

-config defaults to watchtrail.toml. A missing config file is fine — built-in defaults apply.

On startup, the server logs the HTTP and TCP addresses it is listening on. It binds loopback by default; running watchtrail enable-tls switches the dashboard to HTTPS on the LAN and advertises it over mDNS as watchtrail.local. Shutdown cleanly with SIGINT or SIGTERM.


Configuration

All keys are optional. Environment variables override the file.

Key Default Env override Description
http_addr 127.0.0.1:8765 WATCHTRAIL_HTTP_ADDR HTTP ingest bind address
tcp_addr 127.0.0.1:8766 WATCHTRAIL_TCP_ADDR TCP line-protocol bind address
tls_addr :8443 WATCHTRAIL_TLS_ADDR HTTPS bind address (used when TLS is enabled)
token "" WATCHTRAIL_TOKEN Bearer / TCP-handshake token for collectors
db_path watchtrail.db WATCHTRAIL_DB_PATH SQLite database file
data_dir . WATCHTRAIL_DATA_DIR Directory for auth.key, tls/, thumbs, etc.
mdns_enabled true WATCHTRAIL_MDNS_ENABLED Advertise the dashboard over mDNS on the LAN
mdns_hostname host's name WATCHTRAIL_MDNS_HOSTNAME Hostname under .local to publish
auth_disabled false WATCHTRAIL_AUTH_DISABLED Skip cookie auth on the dashboard (loopback dev only)
session_gap_seconds 1800 Sessionizer gap threshold
completion_threshold 0.9 Sessionizer completion threshold
progress_cadence_seconds 30 Collector cadence hint

Dashboard access uses cookie auth bootstrapped from a one-time setup link (see print-link). Collectors continue to authenticate with the bearer / handshake token.

Minimal watchtrail.toml:

db_path = "~/.local/share/watchtrail/history.db"
token   = "change-me"

Subcommands

Command What it does
serve Run the server
recent / item / stats Local read CLI over the same store
rebuild-sessions Replay raw events through the sessionizer
print-link Print a fresh dashboard setup link
enable-tls / disable-tls Mint a local CA + leaf, install / remove user trust, switch the dashboard to HTTPS
enable-encryption / disable-encryption Turn field-level encryption at rest on or off
status Report TLS, encryption, and auth state
keystore-forget Forget a cached keychain passphrase

All commands accept -config and -h. enable-encryption accepts either -mode=keychain (macOS) or -mode=passphrase.


Access

Dashboard. First start prints a one-shot setup link with an embedded token; opening it sets a cookie and binds the browser. Run print-link later to mint another. enable-tls provisions a local CA, installs it into your user trust store (no sudo on macOS), and serves the dashboard on tls_addr with HTTP redirected. mDNS advertises watchtrail.local on the LAN so other devices can reach it without configuration.

Collectors. Bearer token over HTTP, or token-handshake then JSON-per-line over TCP. Both target loopback by default.

Optional encryption at rest. enable-encryption AEAD-encrypts identity columns (media.external_id, url_or_path, title) and the session-join projections; raw events stay verbatim so the sessionizer remains replayable. The key is wrapped by a keychain entry on macOS or derived from a passphrase via Argon2id elsewhere. A pidfile lock and atomic migration prevent concurrent rekey.


Sending events

HTTP (canonical)

POST http://127.0.0.1:8765/ingest
Authorization: Bearer <token>
Content-Type: application/json

<single event object, or a JSON array for a batch>

Responses:

  • 202 Accepted — single event stored
  • 200 OK with {"accepted": n} — batch stored
  • 400 — invalid or unsupported event
  • 401 — bad token

Re-sending the same event_id is a safe no-op (idempotent).

TCP line protocol (for lightweight collectors)

Connect to tcp_addr. Write one JSON event per line, newline-terminated.

If a token is configured, the first line must be the token (handshake). Every subsequent line is a JSON event. Same pipeline as HTTP — validation, dedup, and storage are identical.


The canonical event (v1)

One JSON object per event. Required fields:

Field Type Description
v int Protocol version — always 1
event_id UUID string Collector-generated; the idempotency key
type string start / progress / pause / resume / stop / seek
occurred_at ISO-8601 UTC When the event happened
source_kind string e.g. vlc, youtube
media.external_id string Source-scoped media identity

Optional: source_instance, position_seconds, media.kind, media.title, media.url_or_path, media.duration_seconds, meta (free-form object).

Example:

{
  "v": 1,
  "event_id": "f3c1e0a2-1111-4abc-8def-000000000001",
  "type": "progress",
  "occurred_at": "2026-06-14T09:31:02Z",
  "source_kind": "vlc",
  "source_instance": "laptop-vlc",
  "media": {
    "external_id": "file:9a1f",
    "title": "Spirited Away",
    "duration_seconds": 7500
  },
  "position_seconds": 1342.0,
  "meta": { "rate": 1.0 }
}

Project layout

cmd/watchtrail/     # binary entry point — all subcommands
internal/
  config/           # config loading: defaults → TOML → env
  event/            # canonical event type, parsing, validation
  ingest/           # transport-agnostic pipeline + HTTP and TCP transports
  store/            # Repository interface, SQLite impl, embedded migrations
  api/ web/         # read API and server-rendered htmx dashboard
  auth/             # cookie auth, setup-link, CSRF
  tlsca/            # local CA + leaf certs, user trust install
  discovery/        # mDNS service advertisement
  crypto/ keystore/ # field-level encryption + key providers (keychain / passphrase)
  lockfile/         # pidfile lock used during rekey
  sessionize/ rebuild/  thumb/ lang/ events/
  ids/              # dependency-free UUID generator

Data model: three tables — media_item (deduplicated identity), watch_event (append-only facts; the source of truth), watch_session (derived; not yet populated). All rows carry UUID primary keys and updated_at/deleted_at so multi-device sync can be layered in later without a schema rewrite.

Dependencies: modernc.org/sqlite (pure-Go, no cgo) and github.com/BurntSushi/toml.


Tests

go test ./...

Status and roadmap

Working today: HTTP and TCP ingest, event validation, media identity deduplication, idempotent append-only storage, SQLite with embedded migrations, sessionizer with replayable rebuild-sessions, a read API and CLI, a server-rendered htmx dashboard with stats and SSE live updates, a VLC Lua collector, and an MV3 browser extension (YouTube + generic <video>) with a full-tab settings page. Cookie auth with one-shot setup link, mDNS service advertisement, optional local TLS (enable-tls), and optional field-level encryption at rest (enable-encryption) all ship today. Recent additions: audio-language capture, VLC thumbnails, hourly stats, and soft-delete with restore-on-rewatch.

Planned next (each independently useful, not yet built):

  • Export / import — newline-delimited JSON of events, replayed through the normal pipeline
  • Multi-device sync — alternate Repository implementation, only if actually needed
  • More sources — mpv, Plex / Jellyfin webhooks, manual entry, platform-history import
  • Richer analytics — as the data justifies them

Because raw events are stored verbatim, the sessionizer and any future analytics can be recomputed over your full history whenever the logic improves.


macOS app

Download the .dmg for your Mac from the latest release:

  • Apple Silicon → WatchTrail-arm64.dmg
  • Intel → WatchTrail-amd64.dmg

Open the dmg and drag WatchTrail to Applications. The app lives in the menu bar (no Dock icon) and runs the server in the background. Use the menu to open the dashboard (it signs you in automatically), toggle Start at Login, or quit (which stops the server).

Data lives in ~/Library/Application Support/WatchTrail/.

A passphrase-encrypted database can't be unlocked from the menu bar app — run watchtrail serve in Terminal instead, or switch the database to keychain mode. Keychain-encrypted databases work transparently.

First launch (unsigned build)

The app is not yet code-signed, so macOS Gatekeeper will warn on first launch. Either:

  • Right-click (or Control-click) WatchTrail.appOpenOpen, or
  • Run once in Terminal: xattr -dr com.apple.quarantine /Applications/WatchTrail.app

You only need to do this once.


License

TBD

About

Local-first media-watch history tool. Receives normalized events from VLC, a browser extension, or any script.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages