1dccda529a
- 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
396 lines
12 KiB
JavaScript
396 lines
12 KiB
JavaScript
// 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;
|