Files
sofarr/client/src/ui/history.js
T
gronod ed4237debb
Docs Check / Markdown lint (push) Successful in 1m43s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 2m1s
CI / Security audit (push) Successful in 2m48s
Docs Check / Mermaid diagram parse check (push) Successful in 3m8s
CI / Tests & coverage (push) Failing after 3m33s
CI / Swagger Validation & Coverage (push) Successful in 3m34s
Build and Push Docker Image / build (push) Successful in 4m36s
feat(ombi): Add Ombi PALDRA integration for request management
- Add OmbiRetriever extending ArrRetriever for PALDRA compliance
- Add OmbiClient for low-level Ombi API communication
- Add getOmbiInstances() to config.js following multi-instance pattern
- Register Ombi in PALDRA registry with Ombi-specific methods
- Add external ID matching (TMDB/TVDB/IMDB) to Ombi requests
- Update DownloadMatcher to be async and enrich downloads with Ombi links
- Add getOmbiLink/getOmbiSearchLink helpers to DownloadAssembler
- Implement new service icon layout (Ombi + Sonarr/Radarr icons)
- Add CSS styling for service icons
- Update dashboard routes to include Ombi configuration
- Extend OpenAPI with Ombi tag and NormalizedDownload properties
- Update documentation (README, ARCHITECTURE, SECURITY, CHANGELOG)
- Add Ombi configuration to .env.sample
2026-05-21 17:00:04 +01:00

284 lines
9.2 KiB
JavaScript

// Copyright (c) 2026 Gordon Bolton. MIT License.
import { state, HISTORY_REFRESH_MS } from '../state.js';
import { loadHistory as apiLoadHistory } from '../api.js';
import { saveHistoryDays, saveIgnoreAvailable } from '../utils/storage.js';
import { formatDate, formatEpisodeInfo, escapeHtml } from '../utils/format.js';
import { renderTagBadges } from './downloads.js';
function createServiceIcons(item) {
const container = document.createElement('span');
container.className = 'service-icons-container';
// Add Ombi icon for all users if ombiLink exists
if (item.ombiLink) {
const ombiIcon = document.createElement('img');
ombiIcon.className = 'service-icon ombi';
ombiIcon.src = '/images/ombi.svg';
ombiIcon.alt = 'Ombi';
ombiIcon.title = item.ombiTooltip || 'Ombi';
const ombiLink = document.createElement('a');
ombiLink.href = item.ombiLink;
ombiLink.target = '_blank';
ombiLink.appendChild(ombiIcon);
container.appendChild(ombiLink);
}
// Add Sonarr/Radarr icon for admin users if arrLink exists
if (state.isAdmin && item.arrLink) {
const arrIcon = document.createElement('img');
if (item.arrType === 'sonarr') {
arrIcon.className = 'service-icon sonarr';
arrIcon.src = '/images/sonarr.svg';
arrIcon.alt = 'Sonarr';
} else if (item.arrType === 'radarr') {
arrIcon.className = 'service-icon radarr';
arrIcon.src = '/images/radarr.svg';
arrIcon.alt = 'Radarr';
}
arrIcon.title = item.arrType === 'sonarr' ? 'Sonarr' : 'Radarr';
const arrLink = document.createElement('a');
arrLink.href = item.arrLink;
arrLink.target = '_blank';
arrLink.appendChild(arrIcon);
container.appendChild(arrLink);
}
return container;
}
export function initHistoryControls() {
const daysInput = document.getElementById('history-days');
const refreshBtn = document.getElementById('history-refresh-btn');
const ignoreToggle = document.getElementById('ignore-available-toggle');
if (daysInput) {
daysInput.addEventListener('change', () => {
const v = parseInt(daysInput.value, 10);
if (v > 0 && v <= 90) {
historyDays = v;
saveHistoryDays(v);
loadHistory(true);
}
});
}
if (refreshBtn) {
refreshBtn.addEventListener('click', () => loadHistory(true));
}
if (ignoreToggle) {
ignoreToggle.checked = state.ignoreAvailable;
ignoreToggle.addEventListener('change', () => {
state.ignoreAvailable = ignoreToggle.checked;
saveIgnoreAvailable(state.ignoreAvailable);
renderHistory(state.lastHistoryItems);
});
}
// Listen for history reload events from other modules
document.addEventListener('historyReload', () => {
loadHistory(true);
});
}
export function startHistoryRefresh() {
stopHistoryRefresh();
state.historyRefreshHandle = setInterval(() => loadHistory(), HISTORY_REFRESH_MS);
}
export function stopHistoryRefresh() {
if (state.historyRefreshHandle) {
clearInterval(state.historyRefreshHandle);
state.historyRefreshHandle = null;
}
}
export function clearHistory() {
state.lastHistoryItems = [];
document.getElementById('history-list').innerHTML = '';
document.getElementById('no-history').classList.add('hidden');
document.getElementById('history-error').classList.add('hidden');
}
export async function loadHistory(forceRefresh = false) {
const listEl = document.getElementById('history-list');
const loadingEl = document.getElementById('history-loading');
const errorEl = document.getElementById('history-error');
const noHistoryEl = document.getElementById('no-history');
loadingEl.classList.remove('hidden');
errorEl.classList.add('hidden');
noHistoryEl.classList.add('hidden');
try {
const result = await apiLoadHistory(forceRefresh);
loadingEl.classList.add('hidden');
if (result.success) {
state.lastHistoryItems = result.history;
renderHistory(state.lastHistoryItems);
} else {
errorEl.textContent = result.error || 'Failed to load history.';
errorEl.classList.remove('hidden');
}
} catch (err) {
loadingEl.classList.add('hidden');
errorEl.textContent = 'Failed to load history.';
errorEl.classList.remove('hidden');
console.error('[History] Load error:', err);
}
}
export function renderHistory(items) {
const listEl = document.getElementById('history-list');
const noHistoryEl = document.getElementById('no-history');
listEl.innerHTML = '';
const visible = state.ignoreAvailable
? items.filter(item => !(item.outcome === 'failed' && item.availableForUpgrade))
: items;
if (!visible.length) {
noHistoryEl.classList.remove('hidden');
return;
}
noHistoryEl.classList.add('hidden');
visible.forEach(item => listEl.appendChild(createHistoryCard(item)));
}
export function createHistoryCard(item) {
const card = document.createElement('div');
card.className = `history-card ${item.type} ${item.outcome}`;
if (item.coverArt) {
const coverDiv = document.createElement('div');
coverDiv.className = 'history-cover';
const img = document.createElement('img');
img.src = '/api/dashboard/cover-art?url=' + encodeURIComponent(item.coverArt);
img.alt = item.movieName || item.seriesName || item.title;
img.loading = 'lazy';
coverDiv.appendChild(img);
card.appendChild(coverDiv);
}
const info = document.createElement('div');
info.className = 'history-info';
// Header row: type badge + outcome badge
const header = document.createElement('div');
header.className = 'history-card-header';
const typeBadge = document.createElement('span');
typeBadge.className = `history-type-badge ${item.type}`;
typeBadge.textContent = item.type === 'series' ? '📺 Series' : '🎬 Movie';
header.appendChild(typeBadge);
const outcomeBadge = document.createElement('span');
outcomeBadge.className = `history-outcome-badge ${item.outcome}`;
outcomeBadge.textContent = item.outcome === 'imported' ? '✓ Imported' : '✗ Failed';
header.appendChild(outcomeBadge);
if (item.availableForUpgrade) {
const upgradeBadge = document.createElement('span');
upgradeBadge.className = 'history-upgrade-badge';
upgradeBadge.title = 'A previous version of this item is available. An upgrade download has failed.';
upgradeBadge.textContent = '⬆ Available';
header.appendChild(upgradeBadge);
}
if (item.instanceName) {
const instBadge = document.createElement('span');
instBadge.className = 'history-instance-badge';
instBadge.textContent = item.instanceName;
header.appendChild(instBadge);
}
const badges = renderTagBadges(item.tagBadges, state.showAll, item.matchedUserTag);
header.appendChild(badges);
info.appendChild(header);
// Title
const title = document.createElement('h3');
title.className = 'history-title';
title.textContent = item.title;
info.appendChild(title);
// Series/movie name with service icons
if (item.seriesName) {
const p = document.createElement('p');
p.className = 'history-media-name';
// Add service icons
const serviceIcons = createServiceIcons(item);
if (serviceIcons.hasChildNodes()) {
p.appendChild(serviceIcons);
p.appendChild(document.createTextNode(' '));
}
// Series name is now plain text for all users (no link)
const seriesText = document.createElement('span');
seriesText.textContent = 'Series: ' + item.seriesName;
p.appendChild(seriesText);
info.appendChild(p);
const epEl = formatEpisodeInfo(item.episodes);
if (epEl) info.appendChild(epEl);
}
if (item.movieName) {
const p = document.createElement('p');
p.className = 'history-media-name';
// Add service icons
const serviceIcons = createServiceIcons(item);
if (serviceIcons.hasChildNodes()) {
p.appendChild(serviceIcons);
p.appendChild(document.createTextNode(' '));
}
// Movie name is now plain text for all users (no link)
const movieText = document.createElement('span');
movieText.textContent = 'Movie: ' + item.movieName;
p.appendChild(movieText);
info.appendChild(p);
}
// Detail pills
const details = document.createElement('div');
details.className = 'history-details';
if (item.completedAt) {
details.appendChild(createDetailItem('Completed', formatDate(item.completedAt)));
}
if (item.quality) {
details.appendChild(createDetailItem('Quality', item.quality));
}
// Failed imports: show failure message
if (item.outcome === 'failed' && item.failureMessage) {
const failItem = document.createElement('div');
failItem.className = 'history-failure-message';
failItem.textContent = item.failureMessage;
details.appendChild(failItem);
}
info.appendChild(details);
card.appendChild(info);
return card;
}
function createDetailItem(label, value) {
const item = document.createElement('div');
item.className = 'detail-item';
item.dataset.label = label;
const labelSpan = document.createElement('span');
labelSpan.className = 'detail-label';
labelSpan.textContent = label;
const valueSpan = document.createElement('span');
valueSpan.className = 'detail-value';
valueSpan.textContent = value;
item.appendChild(labelSpan);
item.appendChild(valueSpan);
return item;
}