Files
OFApp/CLAUDE.md
Trey t c60de19348 Initial commit — OFApp client + server
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:07:06 -06:00

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:

  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 <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.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=<postId>). 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].