// 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; }