feat: Add Ombi request filtering and search
Build and Push Docker Image / build (push) Successful in 1m29s
Docs Check / Markdown lint (push) Successful in 1m51s
Licence Check / Licence compatibility and copyright header verification (push) Failing after 2m3s
CI / Security audit (push) Successful in 2m54s
CI / Swagger Validation & Coverage (push) Successful in 3m6s
Docs Check / Mermaid diagram parse check (push) Successful in 3m13s
CI / Tests & coverage (push) Successful in 3m31s

- Add request filters UI (type, status, sort, search)
- Implement dual-layer filtering (server + client)
- Add ombiFilters utility for consistent filtering logic
- Persist filter preferences in localStorage
- Add SSE support for real-time Ombi request updates
- Add webhook endpoints for Ombi integration
- Update OpenAPI spec for new endpoints
- Add unit tests for filter logic and UI
- Add integration tests for Ombi routes
This commit is contained in:
2026-05-22 12:31:31 +01:00
parent dbf45ec31d
commit d3d085d614
17 changed files with 1695 additions and 83 deletions
+85 -15
View File
@@ -5,6 +5,7 @@ const cache = require('../utils/cache');
const { getOmbiInstances, getWebhookSecret, getSofarrBaseUrl } = require('../utils/config');
const requireAuth = require('../middleware/requireAuth');
const { extractRequestedUser } = require('../utils/ombiHelpers');
const { applyRequestFilters } = require('../utils/ombiFilters');
const router = express.Router();
@@ -18,9 +19,52 @@ const router = express.Router();
* Returns Ombi movie and TV requests. Non-admin users only see their own requests
* (filtered by Emby user mapping), while admins see all requests.
*
* Supports server-side filtering by media type, request status, title search,
* and sorting by requested date or title.
*
* **Authentication:** Requires cookie authentication.
* security:
* - cookieAuth: []
* parameters:
* - name: type
* in: query
* schema:
* type: array
* items:
* type: string
* enum: [movie, tv, all]
* default: [all]
* description: Filter by media type. Omit or use `all` for both.
* style: form
* explode: true
* - name: status
* in: query
* schema:
* type: array
* items:
* type: string
* enum: [pending, approved, available, denied]
* description: Filter by request status. Omit for all statuses.
* style: form
* explode: true
* - name: sort
* in: query
* schema:
* type: string
* enum: [requestedDate_desc, requestedDate_asc, title_asc, title_desc]
* default: requestedDate_desc
* description: Sort mode.
* - name: search
* in: query
* schema:
* type: string
* description: Case-insensitive substring match on title.
* - name: showAll
* in: query
* schema:
* type: string
* enum: ['true', 'false']
* description: Admin only. Show all users' requests.
* responses:
* '200':
* description: Ombi requests retrieved successfully
@@ -35,17 +79,20 @@ const router = express.Router();
* isAdmin:
* type: boolean
* example: false
* showAll:
* type: boolean
* example: false
* requests:
* type: object
* properties:
* movie:
* type: array
* items:
* type: object
* $ref: '#/components/schemas/OmbiRequest'
* tv:
* type: array
* items:
* type: object
* $ref: '#/components/schemas/OmbiRequest'
* total:
* type: integer
* example: 5
@@ -59,11 +106,12 @@ const router = express.Router();
router.get('/requests', requireAuth, async (req, res) => {
try {
const user = req.user;
const isAdmin = req.isAdmin;
const isAdmin = user.isAdmin;
const username = user.name;
const showAll = isAdmin && req.query.showAll === 'true';
const arrRetrieverRegistry = require('../utils/arrRetrieverRegistry');
const arrRetrieverRegistry = require('../utils/arrRetrievers');
// initialize() is idempotent - cheap no-op if already initialized
await arrRetrieverRegistry.initialize();
const ombiRequests = await arrRetrieverRegistry.getOmbiRequests();
@@ -73,31 +121,51 @@ router.get('/requests', requireAuth, async (req, res) => {
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);
// Filter to show only the current user's requests.
// Requests with no identifiable user (empty string) are hidden from non-admins.
filteredMovieRequests = filteredMovieRequests.filter(reqItem => {
const requestedUser = extractRequestedUser(reqItem);
return requestedUser.toLowerCase() === usernameLower;
});
filteredTvRequests = filteredTvRequests.filter(req => {
const requestedUser = extractRequestedUser(req);
filteredTvRequests = filteredTvRequests.filter(reqItem => {
const requestedUser = extractRequestedUser(reqItem);
return requestedUser.toLowerCase() === usernameLower;
});
}
const total = filteredMovieRequests.length + filteredTvRequests.length;
// Tag with mediaType and flatten for filtering/sorting
const allRequests = [
...filteredMovieRequests.map(r => ({ ...r, mediaType: 'movie' })),
...filteredTvRequests.map(r => ({ ...r, mediaType: 'tv' }))
];
// Parse query params
let types = req.query.type;
let statuses = req.query.status;
const sort = req.query.sort || 'requestedDate_desc';
const search = req.query.search || '';
// Normalise to arrays
if (typeof types === 'string') types = [types];
if (typeof statuses === 'string') statuses = [statuses];
// Apply filters and sorting
const filtered = applyRequestFilters(allRequests, { types, statuses, sort, search });
// Split back into movie/tv
const movie = filtered.filter(r => r.mediaType === 'movie');
const tv = filtered.filter(r => r.mediaType === 'tv');
const total = filtered.length;
res.json({
user: username,
isAdmin,
showAll,
requests: {
movie: filteredMovieRequests,
tv: filteredTvRequests
},
requests: { movie, tv },
total
});
} catch (error) {
@@ -330,6 +398,8 @@ router.get('/webhook/status', requireAuth, async (req, res) => {
enabled: webhookConfig.enabled || false,
webhookUrl: webhookConfig.webhookUrl || null,
applicationToken: webhookConfig.applicationToken || null,
// Note: Ombi may support per-trigger toggles, but we currently treat
// them as all-on or all-off based on webhookConfig.enabled
triggers: {
requestAvailable: webhookConfig.enabled || false,
requestApproved: webhookConfig.enabled || false,