Skip to content
alsayadiPublic

About

Clip 90 seconds of any video, a passage of any article, or a slice of any podcast — and annotate it. Chrome sidebar extension + public annotation feed. Built to the annotated.com bounty spec.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

annotated

Clip 90 seconds of any video, a passage of any article, or a slice of any podcast — then put your own commentary on it, on a public page that always links back to the source.

Built to the annotated.com bounty spec: a Chrome sidebar extension as the primary surface, a "File a claim" button on every annotation, and a link back to the source on every clip.

Try it in your browser → · Project page

That demo is static — GitHub Pages cannot run Express, SQLite, ffmpeg or yt-dlp, so it keeps the 90s ceiling, source deep-links, the claim button, the feed, follows and comments, and plays clips range-limited from the original instead of re-encoding them to 240p. The full pipeline needs the server below.

extension/   Chrome MV3 side panel — the primary surface
server/      Express + SQLite: auth, clip pipeline, feed, claims
public/      the web app's CSS/JS and generated media
test/        contract tests for the spec's hard requirements

Run it

npm install && npm run doctor && npm run seed && npm start

Then open http://localhost:4000. doctor tells you if anything the clip pipeline needs is missing; seed fills the feed by actually clipping two real sources.

The pipeline shells out to ffmpeg, ffprobe, and yt-dlp:

brew install ffmpeg yt-dlp

Install the sidebar

Chrome 137 removed the --load-extension command-line switch, so the sidebar has to be loaded through the UI — there is no scripted install any more.

  1. Open chrome://extensions and turn on Developer mode.
  2. Load unpacked → pick the extension/ folder.
  3. Pin annotated and click it on any page to open the side panel.
  4. Hit Connect account — it opens the site to sign in, then pairs itself.

The panel reads whatever tab you are on: it detects YouTube / article / podcast, grabs the video's playhead for the in and out points, or pulls the text you highlighted. Commentary can be typed or recorded. Publishing hands the source URL and range to the server, which does the actual clipping.

How the spec maps to the code

Requirement Where it lives
Sidebar Chrome extension is the primary surface extension/manifest.json, sidepanel.js
"File a claim" on every annotation views/annotation.js, routes/pages.js
Every clip links back to its source views/partials.js sourceLink()
Max clip 90s config.js MAX_CLIP_SECONDS, clamped in youtube.js and podcast.js
Video downgraded to 240p (< 480p) media/youtube.js ffmpeg scale filter
Accounts via X or Google only auth.js
Public feed with follow + comment routes/pages.js, store.js
Commentary as text and recorded audio sidepanel.js, routes/api.js /api/publish

The limits are enforced on the server, not in the UI — no client can talk it past 90 seconds or above 240p.

The three source types

YouTube — yt-dlp --download-sections pulls only the requested range, then ffmpeg re-encodes it to 240p H.264. The stored source URL carries a ?t= so "Source" lands the viewer on the exact moment.

Articles — Readability parses the page. If the side panel sent the user's highlight, that is kept verbatim; otherwise annotated takes a lead-sized excerpt (~900 chars) with the byline and timestamp stubs stripped.

Podcasts — yt-dlp resolves the episode's audio stream (falling back to og:audio scraping), then ffmpeg cuts a ≤90s mono AAC segment.

Auth

X and Google OAuth2 are both implemented (X with PKCE). Fill in .env from .env.example:

GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET   redirect: ${PUBLIC_URL}/auth/google/callback
X_CLIENT_ID / X_CLIENT_SECRET             callback: ${PUBLIC_URL}/auth/x/callback

With neither configured the server falls back to a local dev sign-in so you can run it immediately. Configuring either provider disables that fallback automatically.

The side panel can't read the site's cookie (different origin), so it pairs instead: it opens the sign-in page carrying a one-time state, and polls until the site hands back a bearer token.

YouTube gating

YouTube answers some requests with "Sign in to confirm you're not a bot". That one message covers two different problems, and they have different fixes.

Bot-check throttling — the host looks automated. Fixed by generating PO tokens locally, no account involved:

npm run setup:youtube

This installs the bgutil plugin into yt-dlp's own environment and builds its token generator into vendor/ (gitignored). The server detects it automatically.

Video-level gating — YouTube refuses a specific video to anonymous callers, whatever the client or token. The only fix is a real session:

npm run youtube:login          # reads Chrome; pass firefox/safari/brave/edge to override

Run that in a real terminal — macOS pops a keychain prompt the first time, because Chrome encrypts its cookie store, and that prompt needs a human. It writes data/youtube-cookies.txt (chmod 600, gitignored), which the server picks up with no configuration.

npm run doctor probes both an open and a gated video and tells you which of the two you are hitting. Articles and podcasts are never affected.

Deploying the full build

The clip pipeline shells out to ffmpeg and yt-dlp, so it needs a host that runs containers — not static hosting. The Dockerfile carries both binaries.

docker build -t annotated .
docker run -p 4000:4000 \
  -e PUBLIC_URL=https://your-host \
  -e SESSION_SECRET=$(openssl rand -hex 32) \
  -v annotated-data:/app/data -v annotated-media:/app/public/media \
  annotated

fly.toml and render.yaml are ready to go. Whatever you use, set PUBLIC_URL to the public hostname — annotation links, OAuth callbacks and extension pairing are all built from it — and mount a volume at /app/data and /app/public/media so the database and the generated clips survive a redeploy.

To expose a local instance without deploying anything:

cloudflared tunnel --url http://localhost:4000

then restart the server with PUBLIC_URL set to the URL it prints. That URL is ephemeral and the clipping runs on your own machine, so treat it as a demo link rather than hosting.

Rate limits

Clipping runs inline, so the expensive endpoints are capped per IP: 12 clips per 10 minutes, 30 URL probes and 40 writes per minute. Responses carry RateLimit-* headers and a Retry-After on 429. Raise them in server/routes/api.js once clipping moves to a queue.

Tests

npm test

13 contract tests over the parts the spec is strict about: the 90s and 240p ceilings, the source link and claim button being present on every annotation page, claim validation, takedown removing an annotation from both the feed and its page, auth gating, the extension pairing handshake being single-use, follow/comment behaviour, and source-type routing. They use their own database (DB_PATH) and never touch the network.

API

POST /api/publish clip + annotate in one call (multipart, optional audio)
POST /api/capture clip only, returns the clip
GET /api/context?url= what is this URL, and how long is it
GET /api/feed?scope=&before= the public feed, cursor-paginated
GET /api/annotations/:slug one annotation plus its comments
POST /api/annotations/:slug/comments comment
POST /api/users/:handle/follow toggle follow
POST /api/claims file a fair-use claim
POST /api/ext/pair/start · GET /api/ext/pair/status extension pairing

Auth is a session cookie on the web, or Authorization: Bearer <token> from the extension.

Known limits

  • Claims are recorded and shown, and annotations.takedown hides an upheld one everywhere — but there is no moderator UI yet; flipping that flag is a SQL update today.
  • Clipping happens inline on the request. A real deployment wants a job queue for anything longer than a few seconds.
  • Media is served off local disk. Point public/media at object storage before this sees traffic.

About

Clip 90 seconds of any video, a passage of any article, or a slice of any podcast — and annotate it. Chrome sidebar extension + public annotation feed. Built to the annotated.com bounty spec.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages