diff --git a/server/openapi.yaml b/server/openapi.yaml new file mode 100644 index 0000000..f5ba177 --- /dev/null +++ b/server/openapi.yaml @@ -0,0 +1,1413 @@ +openapi: 3.1.0 +info: + title: sofarr API + description: | + sofarr is a personal media download dashboard that aggregates download activity from SABnzbd, Sonarr, Radarr, qBittorrent, Transmission, and rTorrent, filters results by Emby/Jellyfin user identity, and presents a real-time personalized dashboard. + + ## Authentication + sofarr uses cookie-based authentication with Emby/Jellyfin. To authenticate: + 1. Call POST /api/auth/login with username and password + 2. The server sets an httpOnly signed cookie named `emby_user` + 3. The server also sets a `csrf_token` cookie (readable by JS) + 4. Subsequent requests must include the cookies and send the `X-CSRF-Token` header for state-changing operations (POST, PUT, PATCH, DELETE) + + ## Rate Limiting + - General API: 300 requests per 15 minutes per IP + - Login: 10 failed attempts per 15 minutes per IP + - Webhooks: 60 requests per minute per IP + + ## SSE Streaming + Real-time updates are available via Server-Sent Events at GET /api/dashboard/stream. + version: 1.6.0 + contact: + name: sofarr + license: + name: MIT + +servers: + - url: http://localhost:3001 + description: Local development server + - url: https://sofarr.example.com + description: Production server + +tags: + - name: Health + description: Public health check endpoints + - name: Auth + description: Authentication and session management + - name: Dashboard + description: User dashboard and download data + - name: Status + description: Server status and metrics (admin-only) + - name: History + description: Download history + - name: Webhook + description: Webhook receivers for Sonarr/Radarr + - name: Sonarr + description: Sonarr API proxy + - name: Radarr + description: Radarr API proxy + - name: SABnzbd + description: SABnzbd API proxy + - name: Emby + description: Emby/Jellyfin API proxy + +security: + - CookieAuth: [] + +components: + securitySchemes: + CookieAuth: + type: apiKey + in: cookie + name: emby_user + description: | + httpOnly signed cookie containing user session. Set by POST /api/auth/login. + Format: JSON string with {id, name, isAdmin}. + Must be sent with every authenticated request along with X-CSRF-Token header for mutations. + CsrfToken: + type: apiKey + in: header + name: X-CSRF-Token + description: | + CSRF token from csrf_token cookie. Required for POST, PUT, PATCH, DELETE requests. + Obtain via GET /api/auth/csrf or from the response of POST /api/auth/login. + + schemas: + NormalizedDownload: + type: object + description: Standardized download object from any client + properties: + id: + type: string + description: Client-specific unique ID + example: "abc123" + title: + type: string + description: Download title/name + example: "Show.Name.S01E01.1080p.WEB-DL" + type: + type: string + enum: [usenet, torrent] + description: Download type + example: "torrent" + client: + type: string + description: Client identifier + example: "qbittorrent" + instanceId: + type: string + description: Instance identifier + example: "qbittorrent-main" + instanceName: + type: string + description: Instance display name + example: "Main qBittorrent" + status: + type: string + description: Normalized status + example: "Downloading" + progress: + type: number + description: Progress percentage (0-100) + example: 45.5 + size: + type: integer + description: Total size in bytes + example: 1073741824 + downloaded: + type: integer + description: Downloaded bytes + example: 536870912 + speed: + type: integer + description: Current speed in bytes/sec + example: 1048576 + eta: + type: integer + nullable: true + description: ETA in seconds, null if unknown + example: 300 + category: + type: string + description: Download category + example: "tv" + tags: + type: array + items: + type: string + description: Download tags + example: ["user-john"] + savePath: + type: string + description: Save path + example: "/downloads/tv" + addedOn: + type: string + description: Added timestamp (ISO 8601) + example: "2026-05-21T10:00:00.000Z" + arrQueueId: + type: integer + nullable: true + description: Sonarr/Radarr queue ID + example: 123 + arrType: + type: string + enum: [series, movie] + nullable: true + description: Sonarr/Radarr type + example: "series" + + DashboardPayload: + type: object + properties: + user: + type: string + description: Username + example: "john" + isAdmin: + type: boolean + description: Admin flag + example: false + downloads: + type: array + items: + $ref: '#/components/schemas/NormalizedDownload' + description: Matched download objects + downloadClients: + type: array + items: + type: object + properties: + id: + type: string + example: "qbittorrent-main" + name: + type: string + example: "Main qBittorrent" + type: + type: string + example: "qbittorrent" + description: Configured download clients + + ErrorResponse: + type: object + properties: + error: + type: string + description: Error message + example: "Invalid username or password" + details: + type: string + nullable: true + description: Additional error details (dev-only) + example: "Emby API returned 401" + + BlocklistSearchRequest: + type: object + required: + - arrQueueId + - arrType + - arrInstanceUrl + - arrInstanceKey + - arrContentId + - arrContentType + properties: + arrQueueId: + type: integer + description: Sonarr/Radarr queue record ID + example: 123 + arrType: + type: string + enum: [sonarr, radarr] + description: Which *arr service + example: "sonarr" + arrInstanceUrl: + type: string + format: uri + description: Base URL of the *arr instance + example: "http://sonarr:8989" + arrInstanceKey: + type: string + description: API key for the *arr instance + example: "abc123def456" + arrContentId: + type: integer + description: episodeId (Sonarr) or movieId (Radarr) + example: 456 + arrContentType: + type: string + enum: [episode, movie] + description: Content type for search command + example: "episode" + + WebhookPayload: + type: object + description: Webhook payload from Sonarr/Radarr + properties: + eventType: + type: string + description: Event type (Grab, Download, DownloadFolderImported, etc.) + example: "Grab" + instanceName: + type: string + description: Instance name from configuration + example: "Main Sonarr" + date: + type: string + format: date-time + description: Event timestamp + example: "2026-05-21T10:00:00.000Z" + + HistoryItem: + type: object + properties: + type: + type: string + enum: [series, movie] + example: "series" + outcome: + type: string + enum: [imported, failed] + example: "imported" + title: + type: string + description: Source title from *arr record + example: "Show.Name.S01E01.1080p.WEB-DL" + seriesName: + type: string + nullable: true + example: "Show Name" + movieName: + type: string + nullable: true + example: "Movie Name" + coverArt: + type: string + nullable: true + format: uri + example: "http://sonarr:8989/media/poster.jpg" + completedAt: + type: string + format: date-time + example: "2026-05-21T10:00:00.000Z" + quality: + type: string + nullable: true + example: "HDTV-1080p" + instanceName: + type: string + nullable: true + example: "Main Sonarr" + arrLink: + type: string + nullable: true + format: uri + example: "http://sonarr:8989/series/show-slug" + allTags: + type: array + items: + type: string + example: ["user-john", "user-jane"] + matchedUserTag: + type: string + nullable: true + example: "user-john" + availableForUpgrade: + type: boolean + description: True if failed but content is on disk + example: true + + StatusResponse: + type: object + properties: + server: + type: object + properties: + uptimeSeconds: + type: integer + example: 3600 + nodeVersion: + type: string + example: "v22.0.0" + memoryUsageMB: + type: number + example: 128.5 + heapUsedMB: + type: number + example: 64.2 + heapTotalMB: + type: number + example: 128.0 + polling: + type: object + properties: + enabled: + type: boolean + example: true + intervalMs: + type: integer + example: 5000 + lastPoll: + type: object + additionalProperties: true + cache: + type: object + additionalProperties: true + webhooks: + type: object + properties: + sonarr: + type: object + properties: + configured: + type: boolean + example: true + eventsReceived: + type: integer + example: 42 + lastWebhookTimestamp: + type: string + format: date-time + pollsSkipped: + type: integer + example: 15 + radarr: + type: object + properties: + configured: + type: boolean + example: true + eventsReceived: + type: integer + example: 38 + lastWebhookTimestamp: + type: string + format: date-time + pollsSkipped: + type: integer + example: 12 + +paths: + # Public health endpoints + /health: + get: + tags: [Health] + summary: Health check + description: Returns server uptime and status. No authentication required. + responses: + '200': + description: Server is healthy + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "ok" + uptime: + type: number + example: 3600.5 + + /ready: + get: + tags: [Health] + summary: Readiness check + description: Checks if critical configuration (EMBY_URL) is present. Used by Docker HEALTHCHECK. + responses: + '200': + description: Server is ready + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "ready" + '503': + description: Server not ready + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "not ready" + reason: + type: string + example: "EMBY_URL not configured" + + # Auth endpoints (detailed in JSDoc) + /api/auth/login: + post: + tags: [Auth] + summary: Login + description: Authenticate with Emby/Jellyfin and set session cookies + security: [] + responses: + '200': + description: Login successful + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + user: + type: object + properties: + id: + type: string + name: + type: string + isAdmin: + type: boolean + csrfToken: + type: string + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Invalid credentials + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/auth/me: + get: + tags: [Auth] + summary: Get current user + description: Returns the currently authenticated user from the session cookie + security: + - CookieAuth: [] + responses: + '200': + description: User data + content: + application/json: + schema: + type: object + properties: + authenticated: + type: boolean + user: + type: object + properties: + id: + type: string + name: + type: string + isAdmin: + type: boolean + + /api/auth/csrf: + get: + tags: [Auth] + summary: Refresh CSRF token + description: Get a fresh CSRF token without re-authenticating + security: [] + responses: + '200': + description: CSRF token + content: + application/json: + schema: + type: object + properties: + csrfToken: + type: string + + /api/auth/logout: + post: + tags: [Auth] + summary: Logout + description: Clear session cookies and revoke Emby token + security: + - CookieAuth: [] + - CsrfToken: [] + responses: + '200': + description: Logout successful + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + + # Dashboard endpoints (detailed in JSDoc) + /api/dashboard/user-downloads: + get: + tags: [Dashboard] + summary: Get user downloads (deprecated) + description: DEPRECATED: Use /api/dashboard/stream for real-time updates. Returns current downloads for the authenticated user. + security: + - CookieAuth: [] + responses: + '200': + description: User downloads + content: + application/json: + schema: + type: object + properties: + user: + type: string + isAdmin: + type: boolean + downloads: + type: array + items: + $ref: '#/components/schemas/NormalizedDownload' + + /api/dashboard/cover-art: + get: + tags: [Dashboard] + summary: Cover art proxy + description: Proxies external poster images server-side for CSP compliance + security: + - CookieAuth: [] + parameters: + - name: url + in: query + required: true + schema: + type: string + format: uri + responses: + '200': + description: Image data + content: + image/*: + schema: + type: string + format: binary + '400': + description: Invalid URL or non-image + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/dashboard/stream: + get: + tags: [Dashboard] + summary: SSE stream + description: Server-Sent Events stream for real-time download updates. No CSRF token required (GET request). + security: + - CookieAuth: [] + responses: + '200': + description: SSE stream + content: + text/event-stream: + schema: + type: string + + /api/dashboard/blocklist-search: + post: + tags: [Dashboard] + summary: Blocklist and re-search + description: Admin-only. Removes queue item with blocklist=true, then triggers new automatic search. + security: + - CookieAuth: [] + - CsrfToken: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BlocklistSearchRequest' + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + example: true + '403': + description: Admin access required + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + # Status endpoint (detailed in JSDoc) + /api/status/status: + get: + tags: [Status] + summary: Get server status + description: Admin-only endpoint returning server metrics, cache stats, polling info, and webhook metrics + security: + - CookieAuth: [] + responses: + '200': + description: Status data + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '403': + description: Admin access required + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + # History endpoint (detailed in JSDoc) + /api/history/recent: + get: + tags: [History] + summary: Get recent history + description: Returns Sonarr/Radarr history records for the authenticated user, filtered by tag and date + security: + - CookieAuth: [] + parameters: + - name: days + in: query + schema: + type: integer + minimum: 1 + maximum: 90 + default: 7 + description: Number of days to look back + - name: showAll + in: query + schema: + type: boolean + default: false + description: Admin-only: show all users' history + responses: + '200': + description: History items + content: + application/json: + schema: + type: object + properties: + user: + type: string + isAdmin: + type: boolean + days: + type: integer + history: + type: array + items: + $ref: '#/components/schemas/HistoryItem' + + # Webhook endpoints (detailed in JSDoc) + /api/webhook/sonarr: + post: + tags: [Webhook] + summary: Sonarr webhook + description: Receives webhook events from Sonarr. Requires X-Sofarr-Webhook-Secret header. + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookPayload' + responses: + '200': + description: Event received + content: + application/json: + schema: + type: object + properties: + received: + type: boolean + duplicate: + type: boolean + '401': + description: Invalid or missing secret + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '400': + description: Invalid payload + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/webhook/radarr: + post: + tags: [Webhook] + summary: Radarr webhook + description: Receives webhook events from Radarr. Requires X-Sofarr-Webhook-Secret header. + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookPayload' + responses: + '200': + description: Event received + content: + application/json: + schema: + type: object + properties: + received: + type: boolean + duplicate: + type: boolean + '401': + description: Invalid or missing secret + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '400': + description: Invalid payload + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + # Sonarr proxy endpoints (detailed in JSDoc) + /api/sonarr/queue: + get: + tags: [Sonarr] + summary: Get Sonarr queue + description: Proxy to Sonarr's queue endpoint + security: + - CookieAuth: [] + responses: + '200': + description: Queue data + content: + application/json: + schema: + type: object + '500': + description: Proxy error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/sonarr/history: + get: + tags: [Sonarr] + summary: Get Sonarr history + description: Proxy to Sonarr's history endpoint + security: + - CookieAuth: [] + parameters: + - name: pageSize + in: query + schema: + type: integer + default: 50 + responses: + '200': + description: History data + content: + application/json: + schema: + type: object + + /api/sonarr/series/{id}: + get: + tags: [Sonarr] + summary: Get series details + description: Proxy to Sonarr's series details endpoint + security: + - CookieAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Series data + content: + application/json: + schema: + type: object + + /api/sonarr/series: + get: + tags: [Sonarr] + summary: Get all series + description: Proxy to Sonarr's series list endpoint + security: + - CookieAuth: [] + responses: + '200': + description: Series list + content: + application/json: + schema: + type: array + + /api/sonarr/notifications: + get: + tags: [Sonarr] + summary: List notifications + description: Proxy to Sonarr's notification list endpoint + security: + - CookieAuth: [] + responses: + '200': + description: Notifications list + content: + application/json: + schema: + type: array + '503': + description: Sonarr not configured + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/sonarr/notifications/{id}: + get: + tags: [Sonarr] + summary: Get notification + description: Proxy to Sonarr's notification details endpoint + security: + - CookieAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Notification data + content: + application/json: + schema: + type: object + + /api/sonarr/notifications: + post: + tags: [Sonarr] + summary: Create notification + description: Proxy to Sonarr's notification create endpoint + security: + - CookieAuth: [] + - CsrfToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + '200': + description: Created notification + content: + application/json: + schema: + type: object + + /api/sonarr/notifications/{id}: + put: + tags: [Sonarr] + summary: Update notification + description: Proxy to Sonarr's notification update endpoint + security: + - CookieAuth: [] + - CsrfToken: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + '200': + description: Updated notification + content: + application/json: + schema: + type: object + + /api/sonarr/notifications/{id}: + delete: + tags: [Sonarr] + summary: Delete notification + description: Proxy to Sonarr's notification delete endpoint + security: + - CookieAuth: [] + - CsrfToken: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Deleted notification + content: + application/json: + schema: + type: object + + /api/sonarr/notifications/test: + post: + tags: [Sonarr] + summary: Test notification + description: Proxy to Sonarr's notification test endpoint + security: + - CookieAuth: [] + - CsrfToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + '200': + description: Test result + content: + application/json: + schema: + type: object + + /api/sonarr/notifications/schema: + get: + tags: [Sonarr] + summary: Get notification schema + description: Proxy to Sonarr's notification schema endpoint + security: + - CookieAuth: [] + responses: + '200': + description: Schema data + content: + application/json: + schema: + type: object + + /api/sonarr/notifications/sofarr-webhook: + post: + tags: [Sonarr] + summary: Configure Sofarr webhook + description: One-click setup for Sofarr webhook notification in Sonarr + security: + - CookieAuth: [] + - CsrfToken: [] + responses: + '200': + description: Configured notification + content: + application/json: + schema: + type: object + '400': + description: Missing configuration + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '503': + description: Sonarr not configured + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + # Radarr proxy endpoints (detailed in JSDoc) + /api/radarr/queue: + get: + tags: [Radarr] + summary: Get Radarr queue + description: Proxy to Radarr's queue endpoint + security: + - CookieAuth: [] + responses: + '200': + description: Queue data + content: + application/json: + schema: + type: object + + /api/radarr/history: + get: + tags: [Radarr] + summary: Get Radarr history + description: Proxy to Radarr's history endpoint + security: + - CookieAuth: [] + parameters: + - name: pageSize + in: query + schema: + type: integer + default: 50 + responses: + '200': + description: History data + content: + application/json: + schema: + type: object + + /api/radarr/movies/{id}: + get: + tags: [Radarr] + summary: Get movie details + description: Proxy to Radarr's movie details endpoint + security: + - CookieAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Movie data + content: + application/json: + schema: + type: object + + /api/radarr/movies: + get: + tags: [Radarr] + summary: Get all movies + description: Proxy to Radarr's movie list endpoint + security: + - CookieAuth: [] + responses: + '200': + description: Movies list + content: + application/json: + schema: + type: array + + /api/radarr/notifications: + get: + tags: [Radarr] + summary: List notifications + description: Proxy to Radarr's notification list endpoint + security: + - CookieAuth: [] + responses: + '200': + description: Notifications list + content: + application/json: + schema: + type: array + '503': + description: Radarr not configured + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/radarr/notifications/{id}: + get: + tags: [Radarr] + summary: Get notification + description: Proxy to Radarr's notification details endpoint + security: + - CookieAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Notification data + content: + application/json: + schema: + type: object + + /api/radarr/notifications: + post: + tags: [Radarr] + summary: Create notification + description: Proxy to Radarr's notification create endpoint + security: + - CookieAuth: [] + - CsrfToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + '200': + description: Created notification + content: + application/json: + schema: + type: object + + /api/radarr/notifications/{id}: + put: + tags: [Radarr] + summary: Update notification + description: Proxy to Radarr's notification update endpoint + security: + - CookieAuth: [] + - CsrfToken: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + '200': + description: Updated notification + content: + application/json: + schema: + type: object + + /api/radarr/notifications/{id}: + delete: + tags: [Radarr] + summary: Delete notification + description: Proxy to Radarr's notification delete endpoint + security: + - CookieAuth: [] + - CsrfToken: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Deleted notification + content: + application/json: + schema: + type: object + + /api/radarr/notifications/test: + post: + tags: [Radarr] + summary: Test notification + description: Proxy to Radarr's notification test endpoint + security: + - CookieAuth: [] + - CsrfToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + '200': + description: Test result + content: + application/json: + schema: + type: object + + /api/radarr/notifications/schema: + get: + tags: [Radarr] + summary: Get notification schema + description: Proxy to Radarr's notification schema endpoint + security: + - CookieAuth: [] + responses: + '200': + description: Schema data + content: + application/json: + schema: + type: object + + /api/radarr/notifications/sofarr-webhook: + post: + tags: [Radarr] + summary: Configure Sofarr webhook + description: One-click setup for Sofarr webhook notification in Radarr + security: + - CookieAuth: [] + - CsrfToken: [] + responses: + '200': + description: Configured notification + content: + application/json: + schema: + type: object + '400': + description: Missing configuration + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '503': + description: Radarr not configured + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + # SABnzbd proxy endpoints (detailed in JSDoc) + /api/sabnzbd/queue: + get: + tags: [SABnzbd] + summary: Get SABnzbd queue + description: Proxy to SABnzbd's queue endpoint + security: + - CookieAuth: [] + responses: + '200': + description: Queue data + content: + application/json: + schema: + type: object + + /api/sabnzbd/history: + get: + tags: [SABnzbd] + summary: Get SABnzbd history + description: Proxy to SABnzbd's history endpoint + security: + - CookieAuth: [] + parameters: + - name: limit + in: query + schema: + type: integer + default: 50 + responses: + '200': + description: History data + content: + application/json: + schema: + type: object + + # Emby proxy endpoints (detailed in JSDoc) + /api/emby/sessions: + get: + tags: [Emby] + summary: Get active sessions + description: Proxy to Emby's sessions endpoint + security: + - CookieAuth: [] + responses: + '200': + description: Sessions data + content: + application/json: + schema: + type: array + + /api/emby/users/{id}: + get: + tags: [Emby] + summary: Get user by ID + description: Proxy to Emby's user details endpoint + security: + - CookieAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: User data + content: + application/json: + schema: + type: object + + /api/emby/users: + get: + tags: [Emby] + summary: Get all users + description: Proxy to Emby's users list endpoint + security: + - CookieAuth: [] + responses: + '200': + description: Users list + content: + application/json: + schema: + type: array + + /api/emby/session/{sessionId}/user: + get: + tags: [Emby] + summary: Get user from session + description: Get user details for a specific session ID + security: + - CookieAuth: [] + parameters: + - name: sessionId + in: path + required: true + schema: + type: string + responses: + '200': + description: User data + content: + application/json: + schema: + type: object + '404': + description: Session not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse'