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:
2026-05-21 20:59:06 +01:00
parent 884fb5285f
commit 1dccda529a
13 changed files with 1186 additions and 54 deletions
+2
View File
@@ -25,6 +25,7 @@ const statusRoutes = require('./routes/status');
const historyRoutes = require('./routes/history');
const authRoutes = require('./routes/auth');
const webhookRoutes = require('./routes/webhook');
const ombiRoutes = require('./routes/ombi');
const verifyCsrf = require('./middleware/verifyCsrf');
function createApp({ skipRateLimits = false } = {}) {
@@ -218,6 +219,7 @@ function createApp({ skipRateLimits = false } = {}) {
app.use('/api/sonarr', sonarrRoutes);
app.use('/api/radarr', radarrRoutes);
app.use('/api/emby', embyRoutes);
app.use('/api/ombi', ombiRoutes);
app.use('/api/dashboard', dashboardRoutes);
app.use('/api/status', statusRoutes);
app.use('/api/history', historyRoutes);
+29 -2
View File
@@ -29,6 +29,7 @@ function readCacheSnapshot() {
const radarrHistoryData = cache.get('poll:radarr-history') || { records: [] };
const radarrTagsData = cache.get('poll:radarr-tags') || [];
const qbittorrentTorrents = cache.get('poll:qbittorrent') || [];
const ombiRequests = cache.get('poll:ombi-requests') || { movie: [], tv: [] };
return {
sabnzbdQueue: { data: { queue: sabQueueData } },
@@ -39,7 +40,8 @@ function readCacheSnapshot() {
radarrHistory: { data: radarrHistoryData },
radarrTags: { data: radarrTagsData },
qbittorrentTorrents,
sonarrTagsResults
sonarrTagsResults,
ombiRequests
};
}
@@ -489,7 +491,32 @@ router.get('/stream', requireAuth, async (req, res) => {
name: c.name,
type: c.getClientType()
}));
res.write(`data: ${JSON.stringify({ user: user.name, isAdmin, downloads: userDownloads, downloadClients })}\n\n`);
// Filter Ombi requests by user if not admin or if showAll is false
const ombiRequests = snapshot.ombiRequests || { movie: [], tv: [] };
let filteredOmbiMovieRequests = ombiRequests.movie || [];
let filteredOmbiTvRequests = ombiRequests.tv || [];
const showAllOmbi = showAll; // Use the same showAll flag for Ombi
if (!showAllOmbi && username) {
const usernameLower = username.toLowerCase();
filteredOmbiMovieRequests = filteredOmbiMovieRequests.filter(req => {
const requestedUser = req.requestedUser || req.userAlias || '';
return requestedUser.toLowerCase() === usernameLower;
});
filteredOmbiTvRequests = filteredOmbiTvRequests.filter(req => {
const requestedUser = req.requestedUser || req.userAlias || '';
return requestedUser.toLowerCase() === usernameLower;
});
}
const ombiRequestsFiltered = {
movie: filteredOmbiMovieRequests,
tv: filteredOmbiTvRequests
};
res.write(`data: ${JSON.stringify({ user: user.name, isAdmin, downloads: userDownloads, downloadClients, ombiRequests: ombiRequestsFiltered, ombiBaseUrl })}\n\n`);
} catch (err) {
console.error('[SSE] Error building payload:', sanitizeError(err));
}
+395
View File
@@ -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;
+196 -7
View File
@@ -2,7 +2,7 @@
const express = require('express');
const rateLimit = require('express-rate-limit');
const { logToFile } = require('../utils/logger');
const { getWebhookSecret, getSonarrInstances, getRadarrInstances } = require('../utils/config');
const { getWebhookSecret, getSonarrInstances, getRadarrInstances, getOmbiInstances } = require('../utils/config');
const cache = require('../utils/cache');
const arrRetrieverRegistry = require('../utils/arrRetrievers');
const { pollAllServices, POLL_INTERVAL, POLLING_ENABLED } = require('../utils/poller');
@@ -27,7 +27,9 @@ const VALID_EVENT_TYPES = new Set([
'DownloadFolderImported', 'ImportFailed',
'EpisodeFileRenamed', 'MovieFileRenamed', 'EpisodeFileRenamedBySeries',
'Rename', 'SeriesAdd', 'SeriesDelete', 'MovieAdd', 'MovieDelete',
'MovieFileDelete', 'Health', 'ApplicationUpdate', 'HealthRestored'
'MovieFileDelete', 'Health', 'ApplicationUpdate', 'HealthRestored',
// Ombi notification types
'RequestAvailable', 'RequestApproved', 'RequestDeclined', 'RequestPending', 'RequestProcessing'
]);
// Replay protection — cache recently-seen (eventType+instanceName+timestamp) keys.
@@ -73,6 +75,15 @@ const HISTORY_EVENTS = new Set([
'EpisodeFileRenamedBySeries'
]);
// Ombi event types — all Ombi events refresh the requests cache
const OMBI_EVENTS = new Set([
'RequestAvailable',
'RequestApproved',
'RequestDeclined',
'RequestPending',
'RequestProcessing'
]);
/**
* Validate webhook secret from the X-Sofarr-Webhook-Secret header
* @param {Object} req - Express request object
@@ -107,19 +118,20 @@ function validateWebhookSecret(req) {
*
* Phase 2: lightweight refresh via arrRetrieverRegistry + cache update + SSE broadcast.
*
* @param {string} serviceType - 'sonarr' or 'radarr'
* @param {string} eventType - the eventType from the *arr webhook payload
* @param {string} serviceType - 'sonarr', 'radarr', or 'ombi'
* @param {string} eventType - the eventType from the webhook payload
*/
async function processWebhookEvent(serviceType, eventType) {
const affectsQueue = QUEUE_EVENTS.has(eventType);
const affectsHistory = HISTORY_EVENTS.has(eventType);
const affectsOmbi = OMBI_EVENTS.has(eventType);
if (!affectsQueue && !affectsHistory) {
logToFile(`[Webhook] Event ${eventType} does not affect queue or history, skipping refresh`);
if (!affectsQueue && !affectsHistory && !affectsOmbi) {
logToFile(`[Webhook] Event ${eventType} does not affect queue, history, or requests, skipping refresh`);
return;
}
logToFile(`[Webhook] ${serviceType} event "${eventType}" → queue=${affectsQueue}, history=${affectsHistory}`);
logToFile(`[Webhook] ${serviceType} event "${eventType}" → queue=${affectsQueue}, history=${affectsHistory}, ombi=${affectsOmbi}`);
// Ensure retrievers are initialized (idempotent)
await arrRetrieverRegistry.initialize();
@@ -184,6 +196,14 @@ async function processWebhookEvent(serviceType, eventType) {
}, CACHE_TTL);
logToFile(`[Webhook] Refreshed poll:radarr-history (${radarrHistories.length} instance(s))`);
}
} else if (serviceType === 'ombi') {
const ombiInstances = getOmbiInstances();
if (affectsOmbi) {
const ombiRequests = await arrRetrieverRegistry.getOmbiRequests();
cache.set('poll:ombi-requests', ombiRequests, CACHE_TTL);
logToFile(`[Webhook] Refreshed poll:ombi-requests (${ombiRequests.movie?.length || 0} movies, ${ombiRequests.tv?.length || 0} TV shows)`);
}
}
// Broadcast to all SSE subscribers using the same mechanism poller.js uses.
@@ -512,4 +532,173 @@ router.post('/radarr', webhookLimiter, (req, res) => {
}
});
/**
* @openapi
* /api/webhook/ombi:
* post:
* tags: [Webhook]
* summary: Ombi webhook receiver
* description: |
* Receives webhook events from Ombi instances. Validates the secret, logs the event,
* refreshes cache, broadcasts SSE, and returns 200 immediately (fire-and-forget processing).
*
* **Authentication:** Requires `X-Sofarr-Webhook-Secret` header matching `SOFARR_WEBHOOK_SECRET`.
* No cookie authentication required (webhooks come from Ombi, not browsers).
*
* **Rate Limiting:** 60 requests per minute per IP.
*
* **Validation:**
* - Secret validation via `X-Sofarr-Webhook-Secret` header
* - Payload validation (must be JSON object with notificationType, requestId)
* - Event type must be in allowlist (RequestAvailable, RequestApproved, RequestDeclined, RequestPending, RequestProcessing)
* - Replay protection: rejects duplicate events within 5-minute window
*
* **Event Classification:**
* - OMBI_EVENTS (RequestAvailable, RequestApproved, RequestDeclined, RequestPending, RequestProcessing):
* Refreshes `poll:ombi-requests` cache
*
* **Processing Flow:**
* 1. Validate secret → 401 if invalid
* 2. Validate payload → 400 if invalid
* 3. Check replay cache → 200 with duplicate=true if replay
* 4. Update webhook metrics (enables smart polling skip)
* 5. Return 200 immediately (don't wait for background processing)
* 6. Background: fetch fresh data from Ombi, update cache, broadcast SSE
*
* **x-integration-notes:** Configure Ombi webhook:
* - URL: `{SOFARR_BASE_URL}/api/webhook/ombi`
* - Method: POST
* - Header: `X-Sofarr-Webhook-Secret: {SOFARR_WEBHOOK_SECRET}`
* - Application Token: OMBI_API_KEY
* security: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* notificationType:
* type: string
* example: "RequestAvailable"
* requestId:
* type: integer
* example: 123
* requestedUser:
* type: string
* example: "username"
* title:
* type: string
* example: "Movie Title"
* type:
* type: string
* example: "Movie"
* requestStatus:
* type: string
* example: "Available"
* example:
* notificationType: "RequestAvailable"
* requestId: 123
* requestedUser: "username"
* title: "Movie Title"
* type: "Movie"
* requestStatus: "Available"
* responses:
* '200':
* description: Event received and accepted
* content:
* application/json:
* schema:
* type: object
* properties:
* received:
* type: boolean
* example: true
* duplicate:
* type: boolean
* description: True if this event was already processed (replay protection)
* example: false
* examples:
* newEvent:
* received: true
* duplicate: false
* duplicateEvent:
* received: true
* duplicate: true
* '401':
* description: Invalid or missing webhook secret
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* example:
* error: "Unauthorized"
* '400':
* description: Invalid payload or unknown event type
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* examples:
* invalidPayload:
* error: "Payload must be a JSON object"
* unknownEventType:
* error: "Unknown notificationType: InvalidEvent"
* x-code-samples:
* - lang: curl
* label: cURL (from Ombi)
* source: |
* curl -X POST http://sofarr:3001/api/webhook/ombi \
* -H "Content-Type: application/json" \
* -H "X-Sofarr-Webhook-Secret: your-secret-here" \
* -d '{"notificationType":"RequestAvailable","requestId":123,"requestedUser":"username","title":"Movie Title","type":"Movie","requestStatus":"Available"}'
*/
router.post('/ombi', webhookLimiter, (req, res) => {
if (!validateWebhookSecret(req)) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Ombi uses notificationType instead of eventType
const { notificationType, requestId, requestedUser, applicationUrl } = req.body;
const eventType = notificationType || req.body.eventType;
if (!eventType || !OMBI_EVENTS.has(eventType)) {
logToFile(`[Webhook] Ombi payload rejected: invalid or missing notificationType`);
return res.status(400).json({ error: 'Invalid or missing notificationType' });
}
// Use applicationUrl as instance identifier for replay protection
const instanceName = applicationUrl || 'ombi';
// Use requestId + eventType + current time as replay key
const eventDate = req.body.requestedDate || new Date().toISOString();
if (isReplay(eventType, instanceName, `${requestId}-${eventDate}`)) {
logToFile(`[Webhook] Ombi duplicate event ignored: ${eventType} requestId=${requestId}`);
return res.status(200).json({ received: true, duplicate: true });
}
try {
logToFile(`[Webhook] Ombi event received - Type: ${eventType}, RequestId: ${requestId}, User: ${requestedUser}`);
logToFile(`[Webhook] Ombi payload: ${JSON.stringify(req.body)}`);
// Update webhook metrics for polling optimization
const ombiInstances = getOmbiInstances();
const inst = ombiInstances[0]; // Use first Ombi instance
if (inst) {
cache.updateWebhookMetrics(inst.url);
logToFile(`[Webhook] Updated metrics for Ombi instance: ${inst.name} (${inst.url})`);
}
// Background cache refresh + SSE broadcast (fire-and-forget)
processWebhookEvent('ombi', eventType).catch(err => {
logToFile(`[Webhook] Ombi background refresh error: ${err.message}`);
});
res.status(200).json({ received: true });
} catch (error) {
logToFile(`[Webhook] Ombi error: ${error.message}`);
res.status(200).json({ received: true });
}
});
module.exports = router;
+21 -3
View File
@@ -5,7 +5,8 @@ const { initializeClients, getAllDownloads, getDownloadsByClientType } = require
const arrRetrieverRegistry = require('./arrRetrievers');
const {
getSonarrInstances,
getRadarrInstances
getRadarrInstances,
getOmbiInstances
} = require('./config');
const rawPollInterval = (process.env.POLL_INTERVAL || '').toLowerCase();
@@ -88,13 +89,14 @@ async function pollAllServices() {
const sonarrInstances = getSonarrInstances();
const radarrInstances = getRadarrInstances();
const ombiInstances = getOmbiInstances();
// Check webhook fallback: if no webhook events for WEBHOOK_FALLBACK_TIMEOUT, force full poll
const globalMetrics = cache.getGlobalWebhookMetrics();
const now = Date.now();
const lastWebhookTime = globalMetrics.lastGlobalWebhookTimestamp;
const fallbackTriggered = lastWebhookTime && (now - lastWebhookTime) > WEBHOOK_FALLBACK_TIMEOUT_MS;
if (fallbackTriggered) {
console.log(`[Poller] Webhook fallback triggered: no webhook events for ${WEBHOOK_FALLBACK_TIMEOUT_MINUTES} minutes, forcing full poll`);
}
@@ -102,6 +104,7 @@ async function pollAllServices() {
// Determine which instances should be polled based on webhook activity
const shouldPollSonarr = fallbackTriggered || !shouldSkipInstancePolling(sonarrInstances, 'sonarr');
const shouldPollRadarr = fallbackTriggered || !shouldSkipInstancePolling(radarrInstances, 'radarr');
const shouldPollOmbi = fallbackTriggered || !shouldSkipInstancePolling(ombiInstances, 'ombi');
// All fetches in parallel, each individually timed
const results = await Promise.all([
@@ -133,6 +136,10 @@ async function pollAllServices() {
const tagsByType = await arrRetrieverRegistry.getTagsByType();
return tagsByType.radarr || [];
}) : timed('Radarr Tags', async () => []),
shouldPollOmbi ? timed('Ombi Requests', async () => {
const ombiRequests = await arrRetrieverRegistry.getOmbiRequests();
return ombiRequests;
}) : timed('Ombi Requests', async () => ({ movie: [], tv: [] })),
]);
const [
@@ -140,7 +147,8 @@ async function pollAllServices() {
{ result: sonarrTagsResults }, { result: sonarrQueues },
{ result: sonarrHistories },
{ result: radarrQueues }, { result: radarrHistories },
{ result: radarrTagsResults }
{ result: radarrTagsResults },
{ result: ombiRequests }
] = results;
// Store per-task timings
@@ -282,6 +290,16 @@ async function pollAllServices() {
if (existingRadarrTags) cache.set('poll:radarr-tags', existingRadarrTags, cacheTTL);
}
// Ombi
if (shouldPollOmbi) {
cache.set('poll:ombi-requests', ombiRequests, cacheTTL);
logToFile(`[Poller] Ombi requests cached: ${ombiRequests.movie?.length || 0} movies, ${ombiRequests.tv?.length || 0} TV shows`);
} else {
// Extend TTL of existing cached data when polling is skipped
const existingOmbiRequests = cache.get('poll:ombi-requests');
if (existingOmbiRequests) cache.set('poll:ombi-requests', existingOmbiRequests, cacheTTL);
}
// qBittorrent (already set above in download clients section)
const elapsed = Date.now() - start;