# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Build & Run ```bash npm run install:all # Install server + client deps npm run dev # Concurrent: client (5173) + server (3001) npm run dev:server # Server only with --watch npm run dev:client # Vite dev server only npm run build # Build client to client/dist/ npm start # Production server (serves client/dist/) ``` ## Deployment Remote server: `root@10.3.3.11`, password `Intel22`, path `/mnt/user/appdata/OFApp/` ```bash # Deploy sshpass -p 'Intel22' rsync -avz --exclude='node_modules' --exclude='.git' --exclude='dist' --exclude='data' /Users/treyt/Desktop/code/OFApp/ root@10.3.3.11:/mnt/user/appdata/OFApp/ # Rebuild container sshpass -p 'Intel22' ssh root@10.3.3.11 'cd /mnt/user/appdata/OFApp && docker-compose down && docker-compose up -d --build' # Check logs sshpass -p 'Intel22' ssh root@10.3.3.11 'docker logs ofapp 2>&1 | tail -20' ``` Port mapping: HTTP 3002->3001, HTTPS 3003->3443. Data volumes at `/mnt/user/downloads/OFApp/{db,media}`. ## Architecture **Client**: React 18 + Vite + Tailwind. Pages in `client/src/pages/`, API wrappers in `client/src/api.js`. Vite proxies `/api` to `localhost:3001` in dev. **Server**: Express (ESM modules). All routes are in separate router files mounted in `server/index.js`. ### Server Modules | File | Purpose | |------|---------| | `proxy.js` | OF API proxy with auth headers + media/DRM proxy endpoints | | `signing.js` | Dynamic request signing (fetches rules from GitHub, auto-refreshes hourly) | | `download.js` | Background media download orchestration with resume support | | `db.js` | SQLite (better-sqlite3, WAL mode) — auth, download history, cursors, settings | | `gallery.js` | Serves downloaded media as browsable gallery | | `hls.js` | On-demand HLS transcoding via FFmpeg | | `settings.js` | Key-value settings API | ### Auth & Signing Flow All OF API requests go through `proxy.js` which adds auth headers (Cookie, user-id, x-bc, x-of-rev, app-token) plus a dynamic `sign` header generated by `signing.js`. The signing uses SHA-1 with parameters from community-maintained rules fetched from GitHub repos (rafa-9, datawhores, DATAHOARDERS — tried in order). ### DRM Video Playback OF videos use PallyCon DRM (Widevine for Chrome/Firefox, FairPlay for Safari). The flow: 1. `MediaGrid.jsx` detects `files.drm` on video items, extracts DASH manifest URL + CloudFront cookies (and keeps the parent entity context: `entityType` + `entityId`, e.g. `post/{postId}`) 2. `DrmVideo.jsx` uses Shaka Player to load the DASH manifest via `/api/drm-hls` proxy 3. Shaka's request filter rewrites CDN segment URLs through `/api/drm-hls` (attaches CloudFront cookies server-side) 4. Widevine license challenges go to `/api/drm-license` which forwards to OF's DRM resolver: - Own media: `POST /api2/v2/users/media/{mediaId}/drm/?type=widevine` - Posts/messages/etc: `POST /api2/v2/users/media/{mediaId}/drm/{entityType}/{entityId}?type=widevine` (e.g. `post/{postId}`) 5. The `/api/drm-hls` proxy handles DASH manifests (injects ``), HLS playlists (rewrites URLs), and binary segments (pipes through) **EME requires HTTPS** — the server auto-generates a self-signed cert at `/data/certs/` on first boot. Access DRM content via `https://10.3.3.11:3003`. ### Media Proxy CDN media from `*.onlyfans.com` is proxied through `/api/media-proxy` to avoid CORS issues. It passes Range headers and caches responses. URLs from `public.` and `thumbs.` subdomains are NOT proxied (client-side check in `MediaGrid.jsx`). ### Download System `download.js` runs background downloads in-memory (not persisted across restarts). It paginates through `/api2/v2/users/:id/posts/medias`, downloads each file to `MEDIA_PATH/:username/`, records in SQLite, and supports cursor-based resume for interrupted downloads. ## Environment Variables | Variable | Default | Notes | |----------|---------|-------| | `PORT` | 3001 | HTTP | | `HTTPS_PORT` | 3443 | HTTPS for DRM/EME | | `DB_PATH` | /data/db/ofapp.db | SQLite | | `MEDIA_PATH` | /data/media | Downloaded files | | `DOWNLOAD_DELAY` | 1000 | ms between downloads | | `HLS_ENABLED` | false | FFmpeg HLS transcoding | ## Key Gotchas - Widevine license challenges must be treated as raw binary. `server/index.js` mounts `express.raw()` for `/api/drm-license` before the global `express.json()`, and `server/proxy.js` prefers `req.body` (Buffer) with a fallback stream read. - For subscribed content, `/api/drm-license` must include `entityType` + `entityId` (e.g. `entityType=post&entityId=`). Missing entity context typically yields OF `403 User have no permissions`, which Shaka surfaces as `6007` (`LICENSE_REQUEST_FAILED`). - Request signing rules normalize differently across GitHub sources — `signing.js` handles both old format (static_param) and new format (static-param with dashes). - CloudFront cookies for DRM content are IP-locked to the server's public IP (47.185.183.191). They won't work from a different IP. - The `/api/drm-hls` proxy skips `skd://` URIs in HLS manifests (FairPlay key identifiers, Safari only). - Server log prefixes: `[signing]`, `[drm-license]`, `[drm-hls]`, `[download]`, `[media-proxy]`, `[hls]`.