Files
sofarr/client/src/ui/requests.js
T
gronod 95bd703b26
Docs Check / Markdown lint (push) Successful in 1m14s
Build and Push Docker Image / build (push) Successful in 1m52s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m55s
CI / Security audit (push) Successful in 2m25s
Docs Check / Mermaid diagram parse check (push) Successful in 3m6s
CI / Swagger Validation & Coverage (push) Successful in 3m22s
CI / Tests & coverage (push) Successful in 3m45s
chore: bump version to 1.7.22 and update CHANGELOG, tests and docs
2026-05-27 17:42:41 +01:00

254 lines
8.4 KiB
JavaScript

// Copyright (c) 2026 Gordon Bolton. MIT License.
import { state } from '../state.js';
import { escapeHtml } from '../utils/format.js';
import { applyRequestFilters, getRequestStatus } from '../utils/ombiFilters.js';
/**
* Helper function to extract the username from an Ombi request object.
* The Ombi API returns requestedUser as an OmbiStore.Entities.OmbiUser object,
* not a string, so we need to extract the username from the object.
*
* Must stay in sync with server/utils/ombiHelpers.js
*
* @param {Object} request - The Ombi request object
* @returns {string} The extracted username, or empty string if not found
*/
function extractRequestedUser(request) {
if (!request) return '';
// Try to locate a user object or string from various fields common to Ombi Movies and TV shows
const userSource = request.requestedUser || request.RequestedUser ||
request.user || request.User ||
request.requestedBy || request.RequestedBy ||
request.ombiUser || request.OmbiUser ||
request.requestedByUser || request.RequestedByUser;
// If userSource is an object, extract key fields
if (userSource && typeof userSource === 'object') {
const username = userSource.alias || userSource.Alias ||
userSource.userAlias || userSource.UserAlias ||
userSource.userName || userSource.UserName ||
userSource.normalizedUserName || userSource.NormalizedUserName ||
userSource.displayName || userSource.DisplayName ||
userSource.email || userSource.Email;
if (username) return username;
}
// If userSource is a string
if (userSource && typeof userSource === 'string') {
return userSource;
}
// Fallbacks on the request root level
const rootFallback = request.requestedByAlias || request.RequestedByAlias ||
request.requestedByUsername || request.RequestedByUsername ||
request.requester || request.Requester ||
request.requestedByEmail || request.RequestedByEmail;
if (rootFallback) return rootFallback;
// Check seasons / childRequests nested arrays (common for Ombi TV show requests)
if (Array.isArray(request.seasons)) {
for (const season of request.seasons) {
const seasonUser = extractRequestedUser(season);
if (seasonUser) return seasonUser;
}
}
if (Array.isArray(request.childRequests)) {
for (const child of request.childRequests) {
const childUser = extractRequestedUser(child);
if (childUser) return childUser;
}
}
return '';
}
export function renderRequests() {
const requestsList = document.getElementById('requests-list');
const noRequests = document.getElementById('no-requests');
if (!requestsList) return;
const ombiRequests = state.ombiRequests || { movie: [], tv: [] };
const allRequests = [
...ombiRequests.movie.map(r => ({ ...r, mediaType: 'movie' })),
...ombiRequests.tv.map(r => ({ ...r, mediaType: 'tv' }))
];
// Apply client-side filters, sorting, and search
const filtered = applyRequestFilters(allRequests, {
types: state.selectedRequestTypes,
statuses: state.selectedRequestStatuses,
sort: state.requestSortMode,
search: state.requestSearchQuery
});
requestsList.innerHTML = '';
if (filtered.length === 0) {
if (noRequests) {
noRequests.style.display = 'block';
const p = noRequests.querySelector('p');
if (p) {
// Differentiate between no data from Ombi vs filters excluded everything
const hasAnyData = allRequests.length > 0;
p.textContent = hasAnyData
? 'No requests match your filters.'
: 'No requests found.';
}
}
return;
}
if (noRequests) noRequests.style.display = 'none';
filtered.forEach(request => {
const card = createRequestCard(request);
requestsList.appendChild(card);
});
}
function createRequestCard(request) {
if (!request) {
const card = document.createElement('div');
card.className = 'request-card';
card.textContent = 'Invalid request data';
return card;
}
const card = document.createElement('div');
card.className = 'request-card';
const typeIcon = document.createElement('span');
typeIcon.className = `request-type-icon ${request.mediaType || ''}`;
typeIcon.textContent = request.mediaType === 'movie' ? '🎬' : '📺';
const content = document.createElement('div');
content.className = 'request-content';
const title = document.createElement('div');
title.className = 'request-title';
title.textContent = request.title || 'Unknown Title';
const meta = document.createElement('div');
meta.className = 'request-meta';
const statusBadge = createStatusBadge(request);
meta.appendChild(statusBadge);
if (request.year) {
const year = document.createElement('span');
year.className = 'request-year';
year.textContent = request.year;
meta.appendChild(year);
}
const username = extractRequestedUser(request);
const user = document.createElement('span');
user.className = 'request-user';
if (username) {
user.textContent = `Requested by: ${username}`;
} else {
user.textContent = 'Requested by: Unknown (Ombi)';
user.title = 'No user information received from Ombi';
user.style.cursor = 'help';
user.style.textDecoration = 'underline dotted';
}
meta.appendChild(user);
const dateStr = request.requestedDate || request.RequestedDate || request.date || request.Date;
if (dateStr) {
const requestDate = document.createElement('span');
requestDate.className = 'request-date';
try {
const dateObj = new Date(dateStr);
if (!isNaN(dateObj.getTime())) {
const year = dateObj.getFullYear();
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
const day = String(dateObj.getDate()).padStart(2, '0');
const hours = String(dateObj.getHours()).padStart(2, '0');
const minutes = String(dateObj.getMinutes()).padStart(2, '0');
requestDate.textContent = `Date: ${year}-${month}-${day} ${hours}:${minutes}`;
} else {
requestDate.textContent = `Date: ${dateStr}`;
}
} catch (e) {
requestDate.textContent = `Date: ${dateStr}`;
}
meta.appendChild(requestDate);
}
if (request.quality) {
const quality = document.createElement('span');
quality.className = 'request-quality';
quality.textContent = request.quality;
meta.appendChild(quality);
}
content.appendChild(title);
content.appendChild(meta);
const actions = document.createElement('div');
actions.className = 'request-actions';
if (state.ombiBaseUrl && request.theMovieDbId) {
const ombiLink = document.createElement('a');
ombiLink.className = 'request-link ombi-link';
ombiLink.href = `${state.ombiBaseUrl}/details/${request.mediaType || 'movie'}/${request.theMovieDbId}`;
ombiLink.target = '_blank';
ombiLink.title = 'View in Ombi';
const ombiIcon = document.createElement('img');
ombiIcon.src = '/images/ombi.svg';
ombiIcon.alt = 'Ombi';
ombiIcon.className = 'request-icon';
ombiLink.appendChild(ombiIcon);
actions.appendChild(ombiLink);
}
if (state.isAdmin && request.arrLink) {
const arrLink = document.createElement('a');
arrLink.className = `request-link ${request.arrType}-link`;
arrLink.href = request.arrLink;
arrLink.target = '_blank';
arrLink.title = `View in ${request.arrType === 'sonarr' ? 'Sonarr' : 'Radarr'}`;
const arrIcon = document.createElement('img');
arrIcon.src = request.arrType === 'sonarr' ? '/images/sonarr.svg' : '/images/radarr.svg';
arrIcon.alt = request.arrType === 'sonarr' ? 'Sonarr' : 'Radarr';
arrIcon.className = 'request-icon';
arrLink.appendChild(arrIcon);
actions.appendChild(arrLink);
}
card.appendChild(typeIcon);
card.appendChild(content);
card.appendChild(actions);
return card;
}
function createStatusBadge(request) {
const badge = document.createElement('span');
badge.className = 'request-status-badge';
const status = getRequestStatus(request);
const statusTexts = {
available: 'Available',
denied: `Denied: ${request.deniedReason || 'No reason'}`,
approved: 'Approved',
pending: 'Pending',
unknown: 'Unknown'
};
badge.classList.add(status);
badge.textContent = statusTexts[status] || 'Unknown';
return badge;
}