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
254 lines
8.4 KiB
JavaScript
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;
|
|
}
|
|
|