Files
sofarr/server/utils/ombiFilters.js
T
gronod 33b122d22b
Build and Push Docker Image / build (push) Successful in 1m46s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m33s
CI / Security audit (push) Successful in 1m56s
CI / Swagger Validation & Coverage (push) Successful in 2m35s
CI / Tests & coverage (push) Successful in 2m51s
fix(ombi): resolve TV request status, user, and date display (Issue #53)
Ombi's TV API nests all request data (requestedUser, approved, available,
denied, requested, requestedDate) inside childRequests[] sub-objects.
The application previously only inspected top-level properties, causing
TV shows to consistently display 'unknown' status, 'unknown' user, and
no request date.

Changes:
- OmbiRetriever._hydrateRequest(): hydrate requestedUser on each
  childRequests entry and promote requestedDate to top level
- getRequestStatus() (server + client): aggregate status flags from
  childRequests[] when top-level properties are absent
- Client date display: fallback to childRequests[0].requestedDate
- Add 18 unit tests covering childRequests hydration, status
  aggregation, and date promotion

Closes #53
2026-05-27 21:13:17 +01:00

138 lines
4.1 KiB
JavaScript

// Copyright (c) 2026 Gordon Bolton. MIT License.
/**
* Pure filter / sort / search utilities for Ombi requests.
* Must stay in sync with client/src/utils/ombiFilters.js
*/
/**
* Derive a single status string from an Ombi request object.
* Priority: available > denied > approved > pending > unknown
*
* @param {Object} request
* @returns {string} 'available' | 'denied' | 'approved' | 'pending' | 'unknown'
*/
function getRequestStatus(request) {
if (!request) return 'unknown';
if (request.available) return 'available';
if (request.denied) return 'denied';
if (request.approved) return 'approved';
if (request.requested) return 'pending';
// Ombi TV requests store status flags inside childRequests
if (Array.isArray(request.childRequests) && request.childRequests.length > 0) {
for (const child of request.childRequests) {
if (child && child.available) return 'available';
}
for (const child of request.childRequests) {
if (child && child.denied) return 'denied';
}
for (const child of request.childRequests) {
if (child && child.approved) return 'approved';
}
for (const child of request.childRequests) {
if (child && child.requested) return 'pending';
}
}
return 'unknown';
}
/**
* Filter requests by media type.
*
* @param {Array} requests
* @param {string[]} types - e.g. ['movie', 'tv'] or ['all']
* @returns {Array}
*/
function filterByType(requests, types) {
if (!types || types.length === 0) return requests;
const normalized = types.map(t => t.toLowerCase());
if (normalized.includes('all')) return requests;
return requests.filter(r => normalized.includes(r.mediaType));
}
/**
* Filter requests by status.
*
* @param {Array} requests
* @param {string[]} statuses - e.g. ['pending', 'approved', 'available', 'denied']
* @returns {Array}
*/
function filterByStatus(requests, statuses) {
if (!statuses || statuses.length === 0) return requests;
const normalized = statuses.map(s => s.toLowerCase());
return requests.filter(r => normalized.includes(getRequestStatus(r)));
}
/**
* Filter requests by case-insensitive title substring.
*
* @param {Array} requests
* @param {string} query
* @returns {Array}
*/
function filterBySearch(requests, query) {
if (!query || query.trim() === '') return requests;
const q = query.trim().toLowerCase();
return requests.filter(r => (r.title || '').toLowerCase().includes(q));
}
/**
* Sort requests by the given sort mode.
*
* @param {Array} requests
* @param {string} sortMode - requestedDate_desc | requestedDate_asc | title_asc | title_desc
* @returns {Array} new sorted array
*/
function sortRequests(requests, sortMode) {
const sorted = [...requests];
switch (sortMode) {
case 'requestedDate_asc':
return sorted.sort((a, b) => {
const da = a.requestedDate ? new Date(a.requestedDate).getTime() : 0;
const db = b.requestedDate ? new Date(b.requestedDate).getTime() : 0;
return da - db;
});
case 'title_asc':
return sorted.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
case 'title_desc':
return sorted.sort((a, b) => (b.title || '').localeCompare(a.title || ''));
case 'requestedDate_desc':
default:
return sorted.sort((a, b) => {
const da = a.requestedDate ? new Date(a.requestedDate).getTime() : 0;
const db = b.requestedDate ? new Date(b.requestedDate).getTime() : 0;
return db - da;
});
}
}
/**
* Apply all filters and sorting in one call.
*
* @param {Array} requests
* @param {Object} options
* @param {string[]} options.types
* @param {string[]} options.statuses
* @param {string} options.sort
* @param {string} options.search
* @returns {Array}
*/
function applyRequestFilters(requests, { types, statuses, sort, search } = {}) {
let result = [...requests];
result = filterByType(result, types);
result = filterByStatus(result, statuses);
result = filterBySearch(result, search);
result = sortRequests(result, sort);
return result;
}
module.exports = {
getRequestStatus,
filterByType,
filterByStatus,
filterBySearch,
sortRequests,
applyRequestFilters
};