// Copyright (c) 2026 Gordon Bolton. MIT License. const express = require('express'); const { logToFile } = require('../utils/logger'); const cache = require('../utils/cache'); const { getOmbiInstances, getWebhookSecret, getSofarrBaseUrl } = require('../utils/config'); const requireAuth = require('../middleware/requireAuth'); const { extractRequestedUser } = require('../utils/ombiHelpers'); const router = express.Router(); /** * @openapi * /api/ombi/requests: * get: * tags: [Ombi] * summary: Get Ombi requests * description: | * Returns Ombi movie and TV requests. Non-admin users only see their own requests * (filtered by Emby user mapping), while admins see all requests. * * **Authentication:** Requires cookie authentication. * security: * - cookieAuth: [] * responses: * '200': * description: Ombi requests retrieved successfully * content: * application/json: * schema: * type: object * properties: * user: * type: string * example: "username" * isAdmin: * type: boolean * example: false * requests: * type: object * properties: * movie: * type: array * items: * type: object * tv: * type: array * items: * type: object * total: * type: integer * example: 5 * '401': * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/ErrorResponse' */ router.get('/requests', requireAuth, async (req, res) => { try { const user = req.user; const isAdmin = req.isAdmin; const username = user.name; const showAll = isAdmin && req.query.showAll === 'true'; const arrRetrieverRegistry = require('../utils/arrRetrieverRegistry'); await arrRetrieverRegistry.initialize(); const ombiRequests = await arrRetrieverRegistry.getOmbiRequests(); // Filter by user if not admin or if showAll is false let filteredMovieRequests = ombiRequests.movie || []; let filteredTvRequests = ombiRequests.tv || []; if (!showAll && username) { // Ombi uses requestedUser field to track who made the request // Match by username (case-insensitive) const usernameLower = username.toLowerCase(); filteredMovieRequests = filteredMovieRequests.filter(req => { const requestedUser = extractRequestedUser(req); return requestedUser.toLowerCase() === usernameLower; }); filteredTvRequests = filteredTvRequests.filter(req => { const requestedUser = extractRequestedUser(req); return requestedUser.toLowerCase() === usernameLower; }); } const total = filteredMovieRequests.length + filteredTvRequests.length; res.json({ user: username, isAdmin, showAll, requests: { movie: filteredMovieRequests, tv: filteredTvRequests }, total }); } catch (error) { logToFile(`[Ombi] Error fetching requests: ${error.message}`); res.status(500).json({ error: 'Failed to fetch Ombi requests' }); } }); /** * @openapi * /api/ombi/webhook/enable: * post: * tags: [Ombi] * summary: Enable Ombi webhook * description: | * Registers or updates the Sofarr webhook in Ombi. Requires authentication and CSRF protection. * * **Authentication:** Requires cookie authentication. * **CSRF:** Requires X-CSRF-Token header matching csrf_token cookie. * security: * - cookieAuth: [] * requestBody: * required: false * responses: * '200': * description: Webhook enabled successfully * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * example: true * webhookUrl: * type: string * example: "https://sofarr.example.com/api/webhook/ombi" * applicationToken: * type: string * example: "your-ombi-api-key" * '400': * description: Invalid request or missing configuration * content: * application/json: * schema: * $ref: '#/components/schemas/ErrorResponse' * '401': * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/ErrorResponse' */ router.post('/webhook/enable', requireAuth, async (req, res) => { try { const sofarrBaseUrl = getSofarrBaseUrl(); const webhookSecret = getWebhookSecret(); if (!sofarrBaseUrl) { return res.status(400).json({ error: 'SOFARR_BASE_URL not configured' }); } if (!webhookSecret) { return res.status(400).json({ error: 'SOFARR_WEBHOOK_SECRET not configured' }); } const ombiInstances = getOmbiInstances(); if (ombiInstances.length === 0) { return res.status(400).json({ error: 'Ombi not configured' }); } const ombiInst = ombiInstances[0]; const webhookUrl = `${sofarrBaseUrl}/api/webhook/ombi`; // Call Ombi API to register webhook const axios = require('axios'); const response = await axios.post( `${ombiInst.url}/api/v1/Settings/notifications/webhook`, { enabled: true, webhookUrl: webhookUrl, applicationToken: ombiInst.apiKey }, { headers: { 'ApiKey': ombiInst.apiKey, 'Content-Type': 'application/json' } } ); logToFile(`[Ombi] Webhook enabled: ${webhookUrl}`); res.json({ success: true, webhookUrl: webhookUrl, applicationToken: ombiInst.apiKey }); } catch (error) { logToFile(`[Ombi] Error enabling webhook: ${error.message}`); res.status(500).json({ error: 'Failed to enable Ombi webhook' }); } }); /** * @openapi * /api/ombi/webhook/status: * get: * tags: [Ombi] * summary: Get Ombi webhook status * description: | * Returns the current Ombi webhook configuration status and metrics. * * **Authentication:** Requires cookie authentication. * security: * - cookieAuth: [] * responses: * '200': * description: Webhook status retrieved successfully * content: * application/json: * schema: * type: object * properties: * enabled: * type: boolean * example: true * webhookUrl: * type: string * nullable: true * example: "https://sofarr.example.com/api/webhook/ombi" * applicationToken: * type: string * nullable: true * example: "your-ombi-api-key" * triggers: * type: object * properties: * requestAvailable: * type: boolean * example: true * requestApproved: * type: boolean * example: true * requestDeclined: * type: boolean * example: true * requestPending: * type: boolean * example: true * requestProcessing: * type: boolean * example: true * stats: * type: object * properties: * eventsReceived: * type: integer * example: 10 * pollsSkipped: * type: integer * example: 5 * lastWebhookTimestamp: * type: integer * example: 1716326400000 * '401': * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/ErrorResponse' */ router.get('/webhook/status', requireAuth, async (req, res) => { try { const sofarrBaseUrl = getSofarrBaseUrl(); const webhookSecret = getWebhookSecret(); // Webhooks require SOFARR_BASE_URL and SOFARR_WEBHOOK_SECRET to be configured if (!sofarrBaseUrl || !webhookSecret) { return res.json({ enabled: false, webhookUrl: null, applicationToken: null, triggers: { requestAvailable: false, requestApproved: false, requestDeclined: false, requestPending: false, requestProcessing: false }, stats: null }); } const ombiInstances = getOmbiInstances(); if (ombiInstances.length === 0) { return res.json({ enabled: false, webhookUrl: null, applicationToken: null, triggers: { requestAvailable: false, requestApproved: false, requestDeclined: false, requestPending: false, requestProcessing: false }, stats: null }); } const ombiInst = ombiInstances[0]; // Call Ombi API to get webhook status const axios = require('axios'); const response = await axios.get( `${ombiInst.url}/api/v1/Settings/notifications/webhook`, { headers: { 'ApiKey': ombiInst.apiKey } } ); const webhookConfig = response.data; // Get webhook metrics from cache const metrics = cache.getWebhookMetrics(ombiInst.url); res.json({ enabled: webhookConfig.enabled || false, webhookUrl: webhookConfig.webhookUrl || null, applicationToken: webhookConfig.applicationToken || null, triggers: { requestAvailable: webhookConfig.enabled || false, requestApproved: webhookConfig.enabled || false, requestDeclined: webhookConfig.enabled || false, requestPending: webhookConfig.enabled || false, requestProcessing: webhookConfig.enabled || false }, stats: metrics ? { eventsReceived: metrics.eventCount || 0, pollsSkipped: metrics.pollsSkipped || 0, lastWebhookTimestamp: metrics.lastEventTimestamp || null } : null }); } catch (error) { logToFile(`[Ombi] Error fetching webhook status: ${error.message}`); res.status(500).json({ error: 'Failed to fetch Ombi webhook status' }); } }); /** * @openapi * /api/ombi/webhook/test: * post: * tags: [Ombi] * summary: Test Ombi webhook * description: | * Sends a test webhook event to the Sofarr Ombi webhook endpoint. * * **Authentication:** Requires cookie authentication and CSRF token. * security: * - cookieAuth: [] * - CsrfToken: [] * requestBody: * required: false * responses: * '200': * description: Test webhook sent successfully * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * example: true * '400': * description: Invalid request or missing configuration * content: * application/json: * schema: * $ref: '#/components/schemas/ErrorResponse' * '401': * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/ErrorResponse' */ router.post('/webhook/test', requireAuth, async (req, res) => { try { const sofarrBaseUrl = getSofarrBaseUrl(); const webhookSecret = getWebhookSecret(); if (!sofarrBaseUrl) { return res.status(400).json({ error: 'SOFARR_BASE_URL not configured' }); } if (!webhookSecret) { return res.status(400).json({ error: 'SOFARR_WEBHOOK_SECRET not configured' }); } const ombiInstances = getOmbiInstances(); if (ombiInstances.length === 0) { return res.status(400).json({ error: 'Ombi not configured' }); } const ombiInst = ombiInstances[0]; const webhookUrl = `${sofarrBaseUrl}/api/webhook/ombi`; // Simulate a test webhook event const axios = require('axios'); await axios.post(webhookUrl, { notificationType: 'RequestAvailable', requestId: 0, requestedUser: 'test', title: 'Test Request', type: 'Movie', requestStatus: 'Pending' }, { headers: { 'X-Sofarr-Webhook-Secret': webhookSecret, 'Content-Type': 'application/json' } }); logToFile(`[Ombi] Test webhook sent to ${webhookUrl}`); res.json({ success: true }); } catch (error) { logToFile(`[Ombi] Error testing webhook: ${error.message}`); res.status(500).json({ error: 'Failed to test Ombi webhook' }); } }); module.exports = router;