5.2 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Build & Run
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/
# 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:
MediaGrid.jsxdetectsfiles.drmon video items, extracts DASH manifest URL + CloudFront cookies (and keeps the parent entity context:entityType+entityId, e.g.post/{postId})DrmVideo.jsxuses Shaka Player to load the DASH manifest via/api/drm-hlsproxy- Shaka's request filter rewrites CDN segment URLs through
/api/drm-hls(attaches CloudFront cookies server-side) - Widevine license challenges go to
/api/drm-licensewhich 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})
- Own media:
- The
/api/drm-hlsproxy handles DASH manifests (injects<BaseURL>), 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.jsmountsexpress.raw()for/api/drm-licensebefore the globalexpress.json(), andserver/proxy.jsprefersreq.body(Buffer) with a fallback stream read. - For subscribed content,
/api/drm-licensemust includeentityType+entityId(e.g.entityType=post&entityId=<postId>). Missing entity context typically yields OF403 User have no permissions, which Shaka surfaces as6007(LICENSE_REQUEST_FAILED). - Request signing rules normalize differently across GitHub sources —
signing.jshandles 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-hlsproxy skipsskd://URIs in HLS manifests (FairPlay key identifiers, Safari only). - Server log prefixes:
[signing],[drm-license],[drm-hls],[download],[media-proxy],[hls].