feat: add Ombi requests tab and webhook panel integration
- Add Ombi requests tab UI with movie/TV request display - Add showAll parameter support for Ombi requests (API and SSE) - Add Ombi webhook panel with enable/test functionality - Add Ombi webhook status endpoint with metrics - Add Ombi webhook test endpoint - Change GET /api/ombi/requests to use OmbiRetriever instead of cache - Add Ombi webhook state and API functions to frontend - Update SSE payload to include Ombi baseUrl and requests
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||
const express = require('express');
|
||||
const { logToFile } = require('../utils/logger');
|
||||
const cache = require('../utils/cache');
|
||||
const { getOmbiInstances } = require('../utils/config');
|
||||
const requireAuth = require('../middleware/requireAuth');
|
||||
|
||||
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 = req.requestedUser || req.userAlias || '';
|
||||
return requestedUser.toLowerCase() === usernameLower;
|
||||
});
|
||||
|
||||
filteredTvRequests = filteredTvRequests.filter(req => {
|
||||
const requestedUser = req.requestedUser || req.userAlias || '';
|
||||
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 ombiInstances = getOmbiInstances();
|
||||
if (ombiInstances.length === 0) {
|
||||
return res.status(400).json({ error: 'Ombi not configured' });
|
||||
}
|
||||
|
||||
const ombiInst = ombiInstances[0];
|
||||
const webhookUrl = `${process.env.SOFARR_BASE_URL}/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 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 ombiInstances = getOmbiInstances();
|
||||
if (ombiInstances.length === 0) {
|
||||
return res.status(400).json({ error: 'Ombi not configured' });
|
||||
}
|
||||
|
||||
const ombiInst = ombiInstances[0];
|
||||
const webhookUrl = `${process.env.SOFARR_BASE_URL}/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': process.env.SOFARR_WEBHOOK_SECRET,
|
||||
'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;
|
||||
Reference in New Issue
Block a user