
This repository contains Transmission RSS Manager with the following changes: - Fixed dark mode navigation tab visibility issue - Improved text contrast in dark mode throughout the app - Created dedicated dark-mode.css for better organization - Enhanced JavaScript for dynamic styling in dark mode - Added complete installation scripts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1599 lines
61 KiB
JavaScript
1599 lines
61 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize navigation
|
|
initNavigation();
|
|
|
|
// Initialize event listeners
|
|
initEventListeners();
|
|
|
|
// Load initial dashboard data
|
|
loadDashboardData();
|
|
|
|
// Set up dark mode based on user preference
|
|
initDarkMode();
|
|
|
|
// Set up auto refresh if enabled
|
|
initAutoRefresh();
|
|
|
|
// Initialize Bootstrap tooltips
|
|
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
|
tooltips.forEach(tooltip => new bootstrap.Tooltip(tooltip));
|
|
});
|
|
|
|
function initNavigation() {
|
|
const navLinks = document.querySelectorAll('.navbar-nav .nav-link');
|
|
navLinks.forEach(link => {
|
|
link.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
const page = this.getAttribute('data-page');
|
|
showPage(page);
|
|
});
|
|
});
|
|
|
|
// Set active page from URL hash or default to dashboard
|
|
const hash = window.location.hash.substring(1);
|
|
showPage(hash || 'dashboard');
|
|
|
|
// Add custom styling to active nav link
|
|
updateActiveNavStyles();
|
|
}
|
|
|
|
function updateActiveNavStyles() {
|
|
// Add specific styles for active nav in dark mode
|
|
if (document.body.classList.contains('dark-mode')) {
|
|
const activeNavLink = document.querySelector('.navbar-nav .nav-link.active');
|
|
if (activeNavLink) {
|
|
activeNavLink.style.backgroundColor = '#375a7f';
|
|
activeNavLink.style.color = '#ffffff';
|
|
activeNavLink.style.fontWeight = 'bold';
|
|
}
|
|
}
|
|
}
|
|
|
|
function showPage(page) {
|
|
// Hide all pages
|
|
const pages = document.querySelectorAll('.page-content');
|
|
pages.forEach(p => p.classList.add('d-none'));
|
|
|
|
// Reset styles and remove active class from all nav links
|
|
const navLinks = document.querySelectorAll('.navbar-nav .nav-link');
|
|
navLinks.forEach(link => {
|
|
link.classList.remove('active');
|
|
link.style.backgroundColor = '';
|
|
link.style.color = '';
|
|
link.style.fontWeight = '';
|
|
});
|
|
|
|
// Show selected page
|
|
const selectedPage = document.getElementById(`page-${page}`);
|
|
if (selectedPage) {
|
|
selectedPage.classList.remove('d-none');
|
|
|
|
// Set active class on nav link
|
|
const activeNav = document.querySelector(`.nav-link[data-page="${page}"]`);
|
|
if (activeNav) {
|
|
activeNav.classList.add('active');
|
|
// Apply special styling for active nav in dark mode
|
|
if (document.body.classList.contains('dark-mode')) {
|
|
activeNav.style.backgroundColor = '#375a7f';
|
|
activeNav.style.color = '#ffffff';
|
|
activeNav.style.fontWeight = 'bold';
|
|
}
|
|
}
|
|
|
|
// Update URL hash
|
|
window.location.hash = page;
|
|
|
|
// Load page-specific data
|
|
loadPageData(page);
|
|
}
|
|
}
|
|
|
|
function loadPageData(page) {
|
|
switch (page) {
|
|
case 'dashboard':
|
|
loadDashboardData();
|
|
break;
|
|
case 'feeds':
|
|
loadFeeds();
|
|
loadAllItems();
|
|
loadMatchedItems();
|
|
break;
|
|
case 'torrents':
|
|
loadTorrents();
|
|
break;
|
|
case 'settings':
|
|
loadSettings();
|
|
break;
|
|
}
|
|
}
|
|
|
|
function initEventListeners() {
|
|
// RSS Feeds page
|
|
document.getElementById('btn-add-feed').addEventListener('click', showAddFeedModal);
|
|
document.getElementById('btn-refresh-feeds').addEventListener('click', refreshFeeds);
|
|
document.getElementById('save-feed-btn').addEventListener('click', saveFeed);
|
|
|
|
// Torrents page
|
|
document.getElementById('btn-add-torrent').addEventListener('click', showAddTorrentModal);
|
|
document.getElementById('btn-refresh-torrents').addEventListener('click', loadTorrents);
|
|
document.getElementById('save-torrent-btn').addEventListener('click', saveTorrent);
|
|
|
|
// Logs page
|
|
document.getElementById('btn-refresh-logs').addEventListener('click', refreshLogs);
|
|
document.getElementById('btn-clear-logs').addEventListener('click', clearLogs);
|
|
document.getElementById('btn-apply-log-filters').addEventListener('click', applyLogFilters);
|
|
document.getElementById('btn-reset-log-filters').addEventListener('click', resetLogFilters);
|
|
document.getElementById('btn-export-logs').addEventListener('click', exportLogs);
|
|
|
|
// Settings page
|
|
document.getElementById('settings-form').addEventListener('submit', saveSettings);
|
|
document.getElementById('dark-mode-toggle').addEventListener('click', toggleDarkMode);
|
|
document.getElementById('btn-reset-settings').addEventListener('click', resetSettings);
|
|
|
|
// Configuration operations
|
|
document.getElementById('btn-backup-config').addEventListener('click', backupConfig);
|
|
document.getElementById('btn-reset-config').addEventListener('click', resetConfig);
|
|
|
|
// Additional Transmission Instances
|
|
document.getElementById('add-transmission-instance').addEventListener('click', addTransmissionInstance);
|
|
}
|
|
|
|
// Dashboard
|
|
function loadDashboardData() {
|
|
// Fetch dashboard statistics
|
|
fetch('/api/dashboard/stats')
|
|
.then(response => response.json())
|
|
.then(stats => {
|
|
document.getElementById('active-downloads').textContent = stats.activeDownloads;
|
|
document.getElementById('seeding-torrents').textContent = stats.seedingTorrents;
|
|
document.getElementById('active-feeds').textContent = stats.activeFeeds;
|
|
document.getElementById('completed-today').textContent = stats.completedToday;
|
|
|
|
document.getElementById('added-today').textContent = stats.addedToday;
|
|
document.getElementById('feeds-count').textContent = stats.feedsCount;
|
|
document.getElementById('matched-count').textContent = stats.matchedCount;
|
|
|
|
// Format download/upload speeds
|
|
const downloadSpeed = formatBytes(stats.downloadSpeed) + '/s';
|
|
const uploadSpeed = formatBytes(stats.uploadSpeed) + '/s';
|
|
document.getElementById('download-speed').textContent = downloadSpeed;
|
|
document.getElementById('upload-speed').textContent = uploadSpeed;
|
|
document.getElementById('current-speed').textContent = `↓${downloadSpeed} ↑${uploadSpeed}`;
|
|
|
|
// Set progress bars (max 100%)
|
|
const maxSpeed = Math.max(stats.downloadSpeed, stats.uploadSpeed, 1);
|
|
const dlPercent = Math.min(Math.round((stats.downloadSpeed / maxSpeed) * 100), 100);
|
|
const ulPercent = Math.min(Math.round((stats.uploadSpeed / maxSpeed) * 100), 100);
|
|
document.getElementById('download-speed-bar').style.width = `${dlPercent}%`;
|
|
document.getElementById('upload-speed-bar').style.width = `${ulPercent}%`;
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading dashboard stats:', error);
|
|
});
|
|
|
|
// Load chart data
|
|
loadDownloadHistoryChart();
|
|
|
|
// Load other dashboard components
|
|
loadActiveTorrents();
|
|
loadRecentMatches();
|
|
}
|
|
|
|
function loadSystemStatus() {
|
|
const statusElement = document.getElementById('system-status');
|
|
statusElement.innerHTML = '<div class="d-flex justify-content-center"><div class="spinner-border spinner-border-sm text-primary" role="status"></div></div>';
|
|
|
|
fetch('/api/config')
|
|
.then(response => response.json())
|
|
.then(config => {
|
|
// Create system status HTML
|
|
let html = '<ul class="list-group">';
|
|
html += `<li class="list-group-item d-flex justify-content-between align-items-center">Auto Download <span class="badge ${config.autoDownloadEnabled ? 'bg-success' : 'bg-danger'}">${config.autoDownloadEnabled ? 'Enabled' : 'Disabled'}</span></li>`;
|
|
html += `<li class="list-group-item d-flex justify-content-between align-items-center">Check Interval <span class="badge bg-primary">${config.checkIntervalMinutes} minutes</span></li>`;
|
|
html += `<li class="list-group-item d-flex justify-content-between align-items-center">Transmission Connection <span class="badge ${config.transmission.host ? 'bg-success' : 'bg-warning'}">${config.transmission.host ? config.transmission.host + ':' + config.transmission.port : 'Not configured'}</span></li>`;
|
|
html += `<li class="list-group-item d-flex justify-content-between align-items-center">Post Processing <span class="badge ${config.postProcessing.enabled ? 'bg-success' : 'bg-danger'}">${config.postProcessing.enabled ? 'Enabled' : 'Disabled'}</span></li>`;
|
|
html += '</ul>';
|
|
|
|
statusElement.innerHTML = html;
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading system status:', error);
|
|
statusElement.innerHTML = '<div class="alert alert-danger">Error loading system status</div>';
|
|
});
|
|
}
|
|
|
|
function loadRecentMatches() {
|
|
const matchesElement = document.getElementById('recent-matches');
|
|
matchesElement.innerHTML = '<div class="d-flex justify-content-center"><div class="spinner-border spinner-border-sm text-primary" role="status"></div></div>';
|
|
|
|
fetch('/api/feeds/matched')
|
|
.then(response => response.json())
|
|
.then(items => {
|
|
// Sort by publish date descending and take the first 5
|
|
const recentItems = items.sort((a, b) => new Date(b.publishDate) - new Date(a.publishDate)).slice(0, 5);
|
|
|
|
if (recentItems.length === 0) {
|
|
matchesElement.innerHTML = '<div class="alert alert-info">No matched items yet</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="list-group">';
|
|
recentItems.forEach(item => {
|
|
const date = new Date(item.publishDate);
|
|
html += `<a href="${item.link}" target="_blank" class="list-group-item list-group-item-action">
|
|
<div class="d-flex w-100 justify-content-between">
|
|
<h6 class="mb-1">${item.title}</h6>
|
|
<small>${formatDate(date)}</small>
|
|
</div>
|
|
<small class="d-flex justify-content-between">
|
|
<span>Matched rule: ${item.matchedRule}</span>
|
|
<span class="badge ${item.isDownloaded ? 'bg-success' : 'bg-warning'}">${item.isDownloaded ? 'Downloaded' : 'Not Downloaded'}</span>
|
|
</small>
|
|
</a>`;
|
|
});
|
|
html += '</div>';
|
|
|
|
matchesElement.innerHTML = html;
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading recent matches:', error);
|
|
matchesElement.innerHTML = '<div class="alert alert-danger">Error loading recent matches</div>';
|
|
});
|
|
}
|
|
|
|
function loadActiveTorrents() {
|
|
const torrentsElement = document.getElementById('active-torrents');
|
|
torrentsElement.innerHTML = '<div class="d-flex justify-content-center"><div class="spinner-border spinner-border-sm text-primary" role="status"></div></div>';
|
|
|
|
fetch('/api/torrents')
|
|
.then(response => response.json())
|
|
.then(torrents => {
|
|
console.log('Dashboard torrents:', torrents);
|
|
|
|
// Sort by progress ascending and filter for active torrents
|
|
const activeTorrents = torrents
|
|
.filter(t => t && t.status && (t.status === 'Downloading' || t.status === 'Seeding'))
|
|
.sort((a, b) => (a.percentDone || 0) - (b.percentDone || 0));
|
|
|
|
if (activeTorrents.length === 0) {
|
|
torrentsElement.innerHTML = '<div class="alert alert-info">No active torrents</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="list-group">';
|
|
activeTorrents.forEach(torrent => {
|
|
// Handle potential null or undefined values
|
|
if (!torrent || !torrent.name) {
|
|
return;
|
|
}
|
|
|
|
// Safely calculate percentages and sizes with error handling
|
|
let progressPercent = 0;
|
|
try {
|
|
progressPercent = Math.round((torrent.percentDone || 0) * 100);
|
|
} catch (e) {
|
|
console.warn('Error calculating progress percent:', e);
|
|
}
|
|
|
|
let sizeInGB = '0.00';
|
|
try {
|
|
if (torrent.totalSize && torrent.totalSize > 0) {
|
|
sizeInGB = (torrent.totalSize / 1073741824).toFixed(2);
|
|
}
|
|
} catch (e) {
|
|
console.warn('Error calculating size in GB:', e);
|
|
}
|
|
|
|
const torrentStatus = torrent.status || 'Unknown';
|
|
const statusClass = torrentStatus.toLowerCase().replace(/\s+/g, '-');
|
|
|
|
html += `<div class="list-group-item">
|
|
<div class="d-flex w-100 justify-content-between">
|
|
<h6 class="mb-1">${torrent.name}</h6>
|
|
<span class="badge bg-${statusClass === 'seeding' ? 'success' : 'primary'}">${torrentStatus}</span>
|
|
</div>
|
|
<div class="progress mt-2">
|
|
<div class="progress-bar ${torrentStatus === 'Seeding' ? 'bg-success' : 'bg-primary'}" role="progressbar" style="width: ${progressPercent}%">${progressPercent}%</div>
|
|
</div>
|
|
<small class="d-block mt-1 text-muted">Size: ${sizeInGB} GB</small>
|
|
</div>`;
|
|
});
|
|
html += '</div>';
|
|
|
|
torrentsElement.innerHTML = html;
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading active torrents:', error);
|
|
torrentsElement.innerHTML = '<div class="alert alert-danger">Error loading active torrents</div>';
|
|
});
|
|
}
|
|
|
|
// RSS Feeds
|
|
function loadFeeds() {
|
|
const feedsElement = document.getElementById('feeds-list');
|
|
feedsElement.innerHTML = '<div class="d-flex justify-content-center"><div class="spinner-border spinner-border-sm text-primary" role="status"></div></div>';
|
|
|
|
fetch('/api/feeds')
|
|
.then(response => response.json())
|
|
.then(feeds => {
|
|
if (feeds.length === 0) {
|
|
feedsElement.innerHTML = '<div class="alert alert-info">No feeds added yet</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="list-group">';
|
|
feeds.forEach(feed => {
|
|
const lastChecked = feed.lastChecked ? new Date(feed.lastChecked) : null;
|
|
|
|
html += `<div class="list-group-item list-group-item-action" data-feed-id="${feed.id}">
|
|
<div class="d-flex w-100 justify-content-between">
|
|
<h5 class="mb-1">${feed.name}</h5>
|
|
<small>${lastChecked ? 'Last checked: ' + formatDate(lastChecked) : 'Never checked'}</small>
|
|
</div>
|
|
<p class="mb-1"><a href="${feed.url}" target="_blank">${feed.url}</a></p>
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<small>${feed.rules.length} rules</small>
|
|
<div>
|
|
<button class="btn btn-sm btn-outline-primary me-2 btn-edit-feed" data-feed-id="${feed.id}">Edit</button>
|
|
<button class="btn btn-sm btn-outline-danger btn-delete-feed" data-feed-id="${feed.id}">Delete</button>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
});
|
|
html += '</div>';
|
|
|
|
feedsElement.innerHTML = html;
|
|
|
|
// Add event listeners
|
|
document.querySelectorAll('.btn-edit-feed').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const feedId = this.getAttribute('data-feed-id');
|
|
editFeed(feedId);
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.btn-delete-feed').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const feedId = this.getAttribute('data-feed-id');
|
|
deleteFeed(feedId);
|
|
});
|
|
});
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading feeds:', error);
|
|
feedsElement.innerHTML = '<div class="alert alert-danger">Error loading feeds</div>';
|
|
});
|
|
}
|
|
|
|
function loadAllItems() {
|
|
const itemsElement = document.getElementById('all-items-list');
|
|
itemsElement.innerHTML = '<div class="d-flex justify-content-center"><div class="spinner-border spinner-border-sm text-primary" role="status"></div></div>';
|
|
|
|
fetch('/api/feeds/items')
|
|
.then(response => response.json())
|
|
.then(items => {
|
|
if (items.length === 0) {
|
|
itemsElement.innerHTML = '<div class="alert alert-info">No feed items yet</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="feed-items-container">';
|
|
items.forEach(item => {
|
|
const date = new Date(item.publishDate);
|
|
const classes = `feed-item ${item.isMatched ? 'matched' : ''} ${item.isDownloaded ? 'downloaded' : ''}`;
|
|
|
|
html += `<div class="${classes}" data-item-id="${item.id}">
|
|
<div class="feed-item-title"><a href="${item.link}" target="_blank">${item.title}</a></div>
|
|
<div class="feed-item-date">${formatDate(date)}</div>
|
|
${item.isMatched ? `<div class="text-success small">Matched rule: ${item.matchedRule}</div>` : ''}
|
|
${item.isDownloaded ? '<div class="text-muted small">Downloaded</div>' : ''}
|
|
${!item.isDownloaded && item.isMatched ?
|
|
`<div class="feed-item-buttons">
|
|
<button class="btn btn-sm btn-primary btn-download-item" data-item-id="${item.id}">Download</button>
|
|
</div>` : ''
|
|
}
|
|
</div>`;
|
|
});
|
|
html += '</div>';
|
|
|
|
itemsElement.innerHTML = html;
|
|
|
|
// Add event listeners
|
|
document.querySelectorAll('.btn-download-item').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const itemId = this.getAttribute('data-item-id');
|
|
downloadItem(itemId);
|
|
});
|
|
});
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading feed items:', error);
|
|
itemsElement.innerHTML = '<div class="alert alert-danger">Error loading feed items</div>';
|
|
});
|
|
}
|
|
|
|
function loadMatchedItems() {
|
|
const matchedElement = document.getElementById('matched-items-list');
|
|
matchedElement.innerHTML = '<div class="d-flex justify-content-center"><div class="spinner-border spinner-border-sm text-primary" role="status"></div></div>';
|
|
|
|
fetch('/api/feeds/matched')
|
|
.then(response => response.json())
|
|
.then(items => {
|
|
if (items.length === 0) {
|
|
matchedElement.innerHTML = '<div class="alert alert-info">No matched items yet</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="feed-items-container">';
|
|
items.forEach(item => {
|
|
const date = new Date(item.publishDate);
|
|
const classes = `feed-item matched ${item.isDownloaded ? 'downloaded' : ''}`;
|
|
|
|
html += `<div class="${classes}" data-item-id="${item.id}">
|
|
<div class="feed-item-title"><a href="${item.link}" target="_blank">${item.title}</a></div>
|
|
<div class="feed-item-date">${formatDate(date)}</div>
|
|
<div class="text-success small">Matched rule: ${item.matchedRule}</div>
|
|
${item.isDownloaded ? '<div class="text-muted small">Downloaded</div>' : ''}
|
|
${!item.isDownloaded ?
|
|
`<div class="feed-item-buttons">
|
|
<button class="btn btn-sm btn-primary btn-download-matched-item" data-item-id="${item.id}">Download</button>
|
|
</div>` : ''
|
|
}
|
|
</div>`;
|
|
});
|
|
html += '</div>';
|
|
|
|
matchedElement.innerHTML = html;
|
|
|
|
// Add event listeners
|
|
document.querySelectorAll('.btn-download-matched-item').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const itemId = this.getAttribute('data-item-id');
|
|
downloadItem(itemId);
|
|
});
|
|
});
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading matched items:', error);
|
|
matchedElement.innerHTML = '<div class="alert alert-danger">Error loading matched items</div>';
|
|
});
|
|
}
|
|
|
|
function showAddFeedModal() {
|
|
// Clear form
|
|
document.getElementById('feed-name').value = '';
|
|
document.getElementById('feed-url').value = '';
|
|
document.getElementById('feed-rules').value = '';
|
|
document.getElementById('feed-auto-download').checked = false;
|
|
|
|
// Update modal title and button text
|
|
document.querySelector('#add-feed-modal .modal-title').textContent = 'Add RSS Feed';
|
|
document.getElementById('save-feed-btn').textContent = 'Add Feed';
|
|
|
|
// Remove feed ID data attribute
|
|
document.getElementById('save-feed-btn').removeAttribute('data-feed-id');
|
|
|
|
// Show modal
|
|
const modal = new bootstrap.Modal(document.getElementById('add-feed-modal'));
|
|
modal.show();
|
|
}
|
|
|
|
function editFeed(feedId) {
|
|
// Fetch feed data
|
|
fetch(`/api/feeds`)
|
|
.then(response => response.json())
|
|
.then(feeds => {
|
|
const feed = feeds.find(f => f.id === feedId);
|
|
if (!feed) {
|
|
alert('Feed not found');
|
|
return;
|
|
}
|
|
|
|
// Populate form
|
|
document.getElementById('feed-name').value = feed.name;
|
|
document.getElementById('feed-url').value = feed.url;
|
|
document.getElementById('feed-rules').value = feed.rules.join('\n');
|
|
document.getElementById('feed-auto-download').checked = feed.autoDownload;
|
|
|
|
// Update modal title and button text
|
|
document.querySelector('#add-feed-modal .modal-title').textContent = 'Edit RSS Feed';
|
|
document.getElementById('save-feed-btn').textContent = 'Save Changes';
|
|
|
|
// Add feed ID data attribute
|
|
document.getElementById('save-feed-btn').setAttribute('data-feed-id', feedId);
|
|
|
|
// Show modal
|
|
const modal = new bootstrap.Modal(document.getElementById('add-feed-modal'));
|
|
modal.show();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching feed:', error);
|
|
alert('Error fetching feed');
|
|
});
|
|
}
|
|
|
|
function saveFeed() {
|
|
const name = document.getElementById('feed-name').value.trim();
|
|
const url = document.getElementById('feed-url').value.trim();
|
|
const rulesText = document.getElementById('feed-rules').value.trim();
|
|
const autoDownload = document.getElementById('feed-auto-download').checked;
|
|
|
|
if (!name || !url) {
|
|
alert('Please enter a name and URL');
|
|
return;
|
|
}
|
|
|
|
// Parse rules (split by new line and remove empty lines)
|
|
const rules = rulesText.split('\n').filter(rule => rule.trim() !== '');
|
|
|
|
const feedId = document.getElementById('save-feed-btn').getAttribute('data-feed-id');
|
|
const isEditing = !!feedId;
|
|
|
|
const feedData = {
|
|
name: name,
|
|
url: url,
|
|
rules: rules,
|
|
autoDownload: autoDownload
|
|
};
|
|
|
|
if (isEditing) {
|
|
feedData.id = feedId;
|
|
|
|
// Update existing feed
|
|
fetch(`/api/feeds/${feedId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(feedData)
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to update feed');
|
|
}
|
|
|
|
// Close modal
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('add-feed-modal'));
|
|
modal.hide();
|
|
|
|
// Refresh feeds
|
|
loadFeeds();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error updating feed:', error);
|
|
alert('Error updating feed');
|
|
});
|
|
} else {
|
|
// Add new feed
|
|
fetch('/api/feeds', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(feedData)
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to add feed');
|
|
}
|
|
|
|
// Close modal
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('add-feed-modal'));
|
|
modal.hide();
|
|
|
|
// Refresh feeds
|
|
loadFeeds();
|
|
// Also refresh items since a new feed might have new items
|
|
loadAllItems();
|
|
loadMatchedItems();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error adding feed:', error);
|
|
alert('Error adding feed');
|
|
});
|
|
}
|
|
}
|
|
|
|
function deleteFeed(feedId) {
|
|
if (!confirm('Are you sure you want to delete this feed?')) {
|
|
return;
|
|
}
|
|
|
|
fetch(`/api/feeds/${feedId}`, {
|
|
method: 'DELETE'
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to delete feed');
|
|
}
|
|
|
|
// Refresh feeds
|
|
loadFeeds();
|
|
// Also refresh items since items from this feed should be removed
|
|
loadAllItems();
|
|
loadMatchedItems();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error deleting feed:', error);
|
|
alert('Error deleting feed');
|
|
});
|
|
}
|
|
|
|
function refreshFeeds() {
|
|
const btn = document.getElementById('btn-refresh-feeds');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Refreshing...';
|
|
|
|
fetch('/api/feeds/refresh', {
|
|
method: 'POST'
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to refresh feeds');
|
|
}
|
|
|
|
// Re-enable button
|
|
btn.disabled = false;
|
|
btn.textContent = 'Refresh Feeds';
|
|
|
|
// Refresh feed items
|
|
loadFeeds();
|
|
loadAllItems();
|
|
loadMatchedItems();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error refreshing feeds:', error);
|
|
alert('Error refreshing feeds');
|
|
|
|
// Re-enable button
|
|
btn.disabled = false;
|
|
btn.textContent = 'Refresh Feeds';
|
|
});
|
|
}
|
|
|
|
function downloadItem(itemId) {
|
|
fetch(`/api/feeds/download/${itemId}`, {
|
|
method: 'POST'
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to download item');
|
|
}
|
|
|
|
// Refresh items
|
|
loadAllItems();
|
|
loadMatchedItems();
|
|
// Also refresh torrents since a new torrent should be added
|
|
loadTorrents();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error downloading item:', error);
|
|
alert('Error downloading item');
|
|
});
|
|
}
|
|
|
|
// Torrents
|
|
function loadTorrents() {
|
|
const torrentsElement = document.getElementById('torrents-list');
|
|
torrentsElement.innerHTML = '<div class="d-flex justify-content-center"><div class="spinner-border spinner-border-sm text-primary" role="status"></div></div>';
|
|
|
|
fetch('/api/torrents')
|
|
.then(response => response.json())
|
|
.then(torrents => {
|
|
console.log('Loaded torrents:', torrents);
|
|
if (torrents.length === 0) {
|
|
torrentsElement.innerHTML = '<div class="alert alert-info">No torrents</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="torrents-container">';
|
|
torrents.forEach(torrent => {
|
|
// Handle potential null or undefined values
|
|
if (!torrent || !torrent.name) {
|
|
console.warn('Invalid torrent data:', torrent);
|
|
return;
|
|
}
|
|
|
|
// Safely calculate percentages and sizes with error handling
|
|
let progressPercent = 0;
|
|
try {
|
|
progressPercent = Math.round((torrent.percentDone || 0) * 100);
|
|
} catch (e) {
|
|
console.warn('Error calculating progress percent:', e);
|
|
}
|
|
|
|
let sizeInGB = '0.00';
|
|
try {
|
|
if (torrent.totalSize && torrent.totalSize > 0) {
|
|
sizeInGB = (torrent.totalSize / 1073741824).toFixed(2);
|
|
}
|
|
} catch (e) {
|
|
console.warn('Error calculating size in GB:', e);
|
|
}
|
|
|
|
const torrentStatus = torrent.status || 'Unknown';
|
|
const statusClass = torrentStatus.toLowerCase().replace(/\s+/g, '-');
|
|
|
|
html += `<div class="torrent-item" data-torrent-id="${torrent.id || 0}">
|
|
<div class="torrent-item-header">
|
|
<div class="torrent-item-title">${torrent.name}</div>
|
|
<span class="badge bg-${statusClass === 'seeding' ? 'success' : statusClass === 'downloading' ? 'primary' : statusClass === 'stopped' ? 'secondary' : 'info'}">${torrentStatus}</span>
|
|
</div>
|
|
<div class="torrent-item-progress">
|
|
<div class="progress">
|
|
<div class="progress-bar ${torrentStatus === 'Seeding' ? 'bg-success' : 'bg-primary'}" role="progressbar" style="width: ${progressPercent}%">${progressPercent}%</div>
|
|
</div>
|
|
</div>
|
|
<div class="torrent-item-details">
|
|
<span>Size: ${sizeInGB} GB</span>
|
|
<span>Location: ${torrent.downloadDir || 'Unknown'}</span>
|
|
</div>
|
|
<div class="torrent-item-buttons">
|
|
${torrentStatus === 'Stopped' ?
|
|
`<button class="btn btn-sm btn-success me-2 btn-start-torrent" data-torrent-id="${torrent.id || 0}">Start</button>` :
|
|
`<button class="btn btn-sm btn-warning me-2 btn-stop-torrent" data-torrent-id="${torrent.id || 0}">Stop</button>`
|
|
}
|
|
<button class="btn btn-sm btn-danger me-2 btn-remove-torrent" data-torrent-id="${torrent.id || 0}">Remove</button>
|
|
${progressPercent >= 100 ?
|
|
`<button class="btn btn-sm btn-info btn-process-torrent" data-torrent-id="${torrent.id || 0}">Process</button>` : ''
|
|
}
|
|
</div>
|
|
</div>`;
|
|
});
|
|
html += '</div>';
|
|
|
|
torrentsElement.innerHTML = html;
|
|
|
|
// Add event listeners
|
|
document.querySelectorAll('.btn-start-torrent').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const torrentId = parseInt(this.getAttribute('data-torrent-id'));
|
|
startTorrent(torrentId);
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.btn-stop-torrent').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const torrentId = parseInt(this.getAttribute('data-torrent-id'));
|
|
stopTorrent(torrentId);
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.btn-remove-torrent').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const torrentId = parseInt(this.getAttribute('data-torrent-id'));
|
|
removeTorrent(torrentId);
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.btn-process-torrent').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const torrentId = parseInt(this.getAttribute('data-torrent-id'));
|
|
processTorrent(torrentId);
|
|
});
|
|
});
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading torrents:', error);
|
|
torrentsElement.innerHTML = '<div class="alert alert-danger">Error loading torrents</div>';
|
|
});
|
|
}
|
|
|
|
function showAddTorrentModal() {
|
|
// Clear form
|
|
document.getElementById('torrent-url').value = '';
|
|
document.getElementById('torrent-download-dir').value = '';
|
|
|
|
// Show modal
|
|
const modal = new bootstrap.Modal(document.getElementById('add-torrent-modal'));
|
|
modal.show();
|
|
}
|
|
|
|
function saveTorrent() {
|
|
const url = document.getElementById('torrent-url').value.trim();
|
|
const downloadDir = document.getElementById('torrent-download-dir').value.trim();
|
|
|
|
if (!url) {
|
|
alert('Please enter a torrent URL or magnet link');
|
|
return;
|
|
}
|
|
|
|
const torrentData = {
|
|
url: url
|
|
};
|
|
|
|
if (downloadDir) {
|
|
torrentData.downloadDir = downloadDir;
|
|
}
|
|
|
|
fetch('/api/torrents', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(torrentData)
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to add torrent');
|
|
}
|
|
|
|
// Close modal
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('add-torrent-modal'));
|
|
modal.hide();
|
|
|
|
// Refresh torrents
|
|
loadTorrents();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error adding torrent:', error);
|
|
alert('Error adding torrent');
|
|
});
|
|
}
|
|
|
|
function startTorrent(torrentId) {
|
|
fetch(`/api/torrents/${torrentId}/start`, {
|
|
method: 'POST'
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to start torrent');
|
|
}
|
|
|
|
// Refresh torrents
|
|
loadTorrents();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error starting torrent:', error);
|
|
alert('Error starting torrent');
|
|
});
|
|
}
|
|
|
|
function stopTorrent(torrentId) {
|
|
fetch(`/api/torrents/${torrentId}/stop`, {
|
|
method: 'POST'
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to stop torrent');
|
|
}
|
|
|
|
// Refresh torrents
|
|
loadTorrents();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error stopping torrent:', error);
|
|
alert('Error stopping torrent');
|
|
});
|
|
}
|
|
|
|
function removeTorrent(torrentId) {
|
|
if (!confirm('Are you sure you want to remove this torrent? The downloaded files will be kept.')) {
|
|
return;
|
|
}
|
|
|
|
fetch(`/api/torrents/${torrentId}`, {
|
|
method: 'DELETE'
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to remove torrent');
|
|
}
|
|
|
|
// Refresh torrents
|
|
loadTorrents();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error removing torrent:', error);
|
|
alert('Error removing torrent');
|
|
});
|
|
}
|
|
|
|
function processTorrent(torrentId) {
|
|
fetch(`/api/torrents/${torrentId}/process`, {
|
|
method: 'POST'
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to process torrent');
|
|
}
|
|
|
|
alert('Torrent processing started');
|
|
})
|
|
.catch(error => {
|
|
console.error('Error processing torrent:', error);
|
|
alert('Error processing torrent');
|
|
});
|
|
}
|
|
|
|
// Settings
|
|
function loadSettings() {
|
|
const form = document.getElementById('settings-form');
|
|
|
|
fetch('/api/config')
|
|
.then(response => response.json())
|
|
.then(config => {
|
|
// Transmission settings
|
|
document.getElementById('transmission-host').value = config.transmission.host;
|
|
document.getElementById('transmission-port').value = config.transmission.port;
|
|
document.getElementById('transmission-use-https').checked = config.transmission.useHttps;
|
|
document.getElementById('transmission-username').value = '';
|
|
document.getElementById('transmission-password').value = '';
|
|
|
|
// RSS settings
|
|
document.getElementById('auto-download-enabled').checked = config.autoDownloadEnabled;
|
|
document.getElementById('check-interval').value = config.checkIntervalMinutes;
|
|
|
|
// Directory settings
|
|
document.getElementById('download-directory').value = config.downloadDirectory;
|
|
document.getElementById('media-library').value = config.mediaLibraryPath;
|
|
|
|
// Post processing settings
|
|
document.getElementById('post-processing-enabled').checked = config.postProcessing.enabled;
|
|
document.getElementById('extract-archives').checked = config.postProcessing.extractArchives;
|
|
document.getElementById('organize-media').checked = config.postProcessing.organizeMedia;
|
|
document.getElementById('minimum-seed-ratio').value = config.postProcessing.minimumSeedRatio;
|
|
document.getElementById('media-extensions').value = config.postProcessing.mediaExtensions.join(', ');
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading settings:', error);
|
|
alert('Error loading settings');
|
|
});
|
|
}
|
|
|
|
function saveSettings(e) {
|
|
e.preventDefault();
|
|
|
|
// Show saving indicator
|
|
const saveBtn = document.querySelector('#settings-form button[type="submit"]');
|
|
const originalBtnText = saveBtn.innerHTML;
|
|
saveBtn.disabled = true;
|
|
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Saving...';
|
|
|
|
const config = {
|
|
transmission: {
|
|
host: document.getElementById('transmission-host').value.trim(),
|
|
port: parseInt(document.getElementById('transmission-port').value),
|
|
useHttps: document.getElementById('transmission-use-https').checked,
|
|
username: document.getElementById('transmission-username').value.trim(),
|
|
password: document.getElementById('transmission-password').value.trim()
|
|
},
|
|
autoDownloadEnabled: document.getElementById('auto-download-enabled').checked,
|
|
checkIntervalMinutes: parseInt(document.getElementById('check-interval').value),
|
|
downloadDirectory: document.getElementById('download-directory').value.trim(),
|
|
mediaLibraryPath: document.getElementById('media-library').value.trim(),
|
|
postProcessing: {
|
|
enabled: document.getElementById('post-processing-enabled').checked,
|
|
extractArchives: document.getElementById('extract-archives').checked,
|
|
organizeMedia: document.getElementById('organize-media').checked,
|
|
minimumSeedRatio: parseInt(document.getElementById('minimum-seed-ratio').value),
|
|
mediaExtensions: document.getElementById('media-extensions').value.split(',').map(ext => ext.trim())
|
|
},
|
|
userPreferences: {
|
|
enableDarkMode: document.body.classList.contains('dark-mode'),
|
|
autoRefreshUIEnabled: localStorage.getItem('autoRefresh') !== 'false',
|
|
autoRefreshIntervalSeconds: parseInt(localStorage.getItem('refreshInterval')) || 30,
|
|
notificationsEnabled: true
|
|
}
|
|
};
|
|
|
|
fetch('/api/config', {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(config)
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
return response.text().then(text => {
|
|
throw new Error(`Failed to save settings: ${text}`);
|
|
});
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(result => {
|
|
console.log('Settings saved successfully:', result);
|
|
|
|
// Show success message
|
|
const settingsForm = document.getElementById('settings-form');
|
|
const alertDiv = document.createElement('div');
|
|
alertDiv.className = 'alert alert-success alert-dismissible fade show mt-3';
|
|
alertDiv.role = 'alert';
|
|
alertDiv.innerHTML = `
|
|
<strong>Success!</strong> Your settings have been saved.
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
`;
|
|
settingsForm.appendChild(alertDiv);
|
|
|
|
// Auto dismiss after 3 seconds
|
|
setTimeout(() => {
|
|
const bsAlert = new bootstrap.Alert(alertDiv);
|
|
bsAlert.close();
|
|
}, 3000);
|
|
|
|
// Re-enable save button
|
|
saveBtn.disabled = false;
|
|
saveBtn.innerHTML = originalBtnText;
|
|
})
|
|
.catch(error => {
|
|
console.error('Error saving settings:', error);
|
|
|
|
// Show error message
|
|
const settingsForm = document.getElementById('settings-form');
|
|
const alertDiv = document.createElement('div');
|
|
alertDiv.className = 'alert alert-danger alert-dismissible fade show mt-3';
|
|
alertDiv.role = 'alert';
|
|
alertDiv.innerHTML = `
|
|
<strong>Error!</strong> ${error.message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
`;
|
|
settingsForm.appendChild(alertDiv);
|
|
|
|
// Re-enable save button
|
|
saveBtn.disabled = false;
|
|
saveBtn.innerHTML = originalBtnText;
|
|
});
|
|
}
|
|
|
|
// Helper functions
|
|
function formatDate(date) {
|
|
if (!date) return 'N/A';
|
|
|
|
// Format as "YYYY-MM-DD HH:MM"
|
|
return `${date.getFullYear()}-${padZero(date.getMonth() + 1)}-${padZero(date.getDate())} ${padZero(date.getHours())}:${padZero(date.getMinutes())}`;
|
|
}
|
|
|
|
function padZero(num) {
|
|
return num.toString().padStart(2, '0');
|
|
}
|
|
|
|
// Dark Mode Functions
|
|
function initDarkMode() {
|
|
// Check local storage preference or system preference
|
|
const darkModePreference = localStorage.getItem('darkMode');
|
|
|
|
if (darkModePreference === 'true' ||
|
|
(darkModePreference === null && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
enableDarkMode();
|
|
} else {
|
|
disableDarkMode();
|
|
}
|
|
}
|
|
|
|
function toggleDarkMode() {
|
|
if (document.body.classList.contains('dark-mode')) {
|
|
disableDarkMode();
|
|
} else {
|
|
enableDarkMode();
|
|
}
|
|
}
|
|
|
|
function enableDarkMode() {
|
|
document.body.classList.add('dark-mode');
|
|
document.getElementById('dark-mode-toggle').innerHTML = '<i class="bi bi-sun-fill"></i>';
|
|
localStorage.setItem('darkMode', 'true');
|
|
|
|
// Also update user preferences if on settings page
|
|
const darkModeCheckbox = document.getElementById('enable-dark-mode');
|
|
if (darkModeCheckbox) {
|
|
darkModeCheckbox.checked = true;
|
|
}
|
|
|
|
// Update active nav link styling
|
|
updateActiveNavStyles();
|
|
}
|
|
|
|
function disableDarkMode() {
|
|
document.body.classList.remove('dark-mode');
|
|
document.getElementById('dark-mode-toggle').innerHTML = '<i class="bi bi-moon-fill"></i>';
|
|
localStorage.setItem('darkMode', 'false');
|
|
|
|
// Also update user preferences if on settings page
|
|
const darkModeCheckbox = document.getElementById('enable-dark-mode');
|
|
if (darkModeCheckbox) {
|
|
darkModeCheckbox.checked = false;
|
|
}
|
|
|
|
// Reset active nav styling (let the default styles apply)
|
|
const activeNav = document.querySelector('.navbar-nav .nav-link.active');
|
|
if (activeNav) {
|
|
activeNav.style.backgroundColor = '';
|
|
activeNav.style.color = '';
|
|
activeNav.style.fontWeight = '';
|
|
}
|
|
}
|
|
|
|
// Auto-refresh
|
|
function initAutoRefresh() {
|
|
// Get auto-refresh settings from local storage or use defaults
|
|
const autoRefresh = localStorage.getItem('autoRefresh') !== 'false';
|
|
const refreshInterval = parseInt(localStorage.getItem('refreshInterval')) || 30;
|
|
|
|
if (autoRefresh) {
|
|
startAutoRefresh(refreshInterval);
|
|
}
|
|
}
|
|
|
|
function startAutoRefresh(intervalSeconds) {
|
|
// Clear any existing interval
|
|
if (window.refreshTimer) {
|
|
clearInterval(window.refreshTimer);
|
|
}
|
|
|
|
// Set new interval
|
|
window.refreshTimer = setInterval(() => {
|
|
const currentPage = window.location.hash.substring(1) || 'dashboard';
|
|
loadPageData(currentPage);
|
|
}, intervalSeconds * 1000);
|
|
|
|
localStorage.setItem('autoRefresh', 'true');
|
|
localStorage.setItem('refreshInterval', intervalSeconds.toString());
|
|
}
|
|
|
|
function stopAutoRefresh() {
|
|
if (window.refreshTimer) {
|
|
clearInterval(window.refreshTimer);
|
|
window.refreshTimer = null;
|
|
}
|
|
|
|
localStorage.setItem('autoRefresh', 'false');
|
|
}
|
|
|
|
// Chart functions
|
|
function loadDownloadHistoryChart() {
|
|
fetch('/api/dashboard/history')
|
|
.then(response => response.json())
|
|
.then(history => {
|
|
const ctx = document.getElementById('download-history-chart').getContext('2d');
|
|
|
|
// Extract dates and count values
|
|
const labels = history.map(point => {
|
|
const date = new Date(point.date);
|
|
return `${date.getMonth() + 1}/${date.getDate()}`;
|
|
});
|
|
|
|
const countData = history.map(point => point.count);
|
|
const sizeData = history.map(point => point.totalSize / (1024 * 1024 * 1024)); // Convert to GB
|
|
|
|
// Create or update chart
|
|
if (window.downloadHistoryChart) {
|
|
window.downloadHistoryChart.data.labels = labels;
|
|
window.downloadHistoryChart.data.datasets[0].data = countData;
|
|
window.downloadHistoryChart.data.datasets[1].data = sizeData;
|
|
window.downloadHistoryChart.update();
|
|
} else {
|
|
window.downloadHistoryChart = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [
|
|
{
|
|
label: 'Number of Downloads',
|
|
data: countData,
|
|
backgroundColor: 'rgba(13, 110, 253, 0.5)',
|
|
borderColor: 'rgba(13, 110, 253, 1)',
|
|
borderWidth: 1,
|
|
yAxisID: 'y'
|
|
},
|
|
{
|
|
label: 'Total Size (GB)',
|
|
data: sizeData,
|
|
type: 'line',
|
|
borderColor: 'rgba(25, 135, 84, 1)',
|
|
backgroundColor: 'rgba(25, 135, 84, 0.1)',
|
|
borderWidth: 2,
|
|
fill: true,
|
|
tension: 0.4,
|
|
yAxisID: 'y1'
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'top',
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
let label = context.dataset.label || '';
|
|
if (label) {
|
|
label += ': ';
|
|
}
|
|
if (context.datasetIndex === 0) {
|
|
label += context.parsed.y;
|
|
} else {
|
|
label += context.parsed.y.toFixed(2) + ' GB';
|
|
}
|
|
return label;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
type: 'linear',
|
|
display: true,
|
|
position: 'left',
|
|
title: {
|
|
display: true,
|
|
text: 'Number of Downloads'
|
|
},
|
|
beginAtZero: true
|
|
},
|
|
y1: {
|
|
type: 'linear',
|
|
display: true,
|
|
position: 'right',
|
|
title: {
|
|
display: true,
|
|
text: 'Total Size (GB)'
|
|
},
|
|
beginAtZero: true,
|
|
grid: {
|
|
drawOnChartArea: false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading download history chart:', error);
|
|
});
|
|
}
|
|
|
|
// Logs Management
|
|
function refreshLogs() {
|
|
const logFilters = getLogFilters();
|
|
loadLogs(logFilters);
|
|
}
|
|
|
|
function getLogFilters() {
|
|
return {
|
|
level: document.getElementById('log-level').value,
|
|
search: document.getElementById('log-search').value,
|
|
dateRange: document.getElementById('log-date-range').value,
|
|
skip: 0,
|
|
take: parseInt(document.getElementById('items-per-page')?.value || 25)
|
|
};
|
|
}
|
|
|
|
function loadLogs(filters) {
|
|
const tbody = document.getElementById('logs-table-body');
|
|
tbody.innerHTML = '<tr><td colspan="4" class="text-center py-4">Loading logs...</td></tr>';
|
|
|
|
// Build query string
|
|
const query = new URLSearchParams();
|
|
if (filters.level && filters.level !== 'All') {
|
|
query.append('Level', filters.level);
|
|
}
|
|
if (filters.search) {
|
|
query.append('Search', filters.search);
|
|
}
|
|
|
|
// Handle date range
|
|
const now = new Date();
|
|
let startDate = null;
|
|
|
|
switch (filters.dateRange) {
|
|
case 'today':
|
|
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
|
|
break;
|
|
case 'yesterday':
|
|
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 0, 0, 0);
|
|
break;
|
|
case 'week':
|
|
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7, 0, 0, 0);
|
|
break;
|
|
case 'month':
|
|
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 30, 0, 0, 0);
|
|
break;
|
|
}
|
|
|
|
if (startDate) {
|
|
query.append('StartDate', startDate.toISOString());
|
|
}
|
|
|
|
query.append('Skip', filters.skip.toString());
|
|
query.append('Take', filters.take.toString());
|
|
|
|
fetch(`/api/logs?${query.toString()}`)
|
|
.then(response => response.json())
|
|
.then(logs => {
|
|
if (logs.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="4" class="text-center py-4">No logs found</td></tr>';
|
|
document.getElementById('log-count').textContent = '0 entries';
|
|
document.getElementById('logs-pagination-info').textContent = 'Showing 0 of 0 entries';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
logs.forEach(log => {
|
|
const timestamp = new Date(log.timestamp);
|
|
const levelClass = getLevelClass(log.level);
|
|
|
|
html += `<tr>
|
|
<td class="text-nowrap">${formatDate(timestamp)}</td>
|
|
<td><span class="badge ${levelClass}">${log.level}</span></td>
|
|
<td>${log.message}</td>
|
|
<td>${log.context || ''}</td>
|
|
</tr>`;
|
|
});
|
|
|
|
tbody.innerHTML = html;
|
|
document.getElementById('log-count').textContent = `${logs.length} entries`;
|
|
document.getElementById('logs-pagination-info').textContent = `Showing ${logs.length} entries`;
|
|
|
|
// Update pagination (simplified for now)
|
|
updateLogPagination(filters, logs.length);
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading logs:', error);
|
|
tbody.innerHTML = '<tr><td colspan="4" class="text-center py-4">Error loading logs</td></tr>';
|
|
});
|
|
}
|
|
|
|
function updateLogPagination(filters, count) {
|
|
const pagination = document.getElementById('logs-pagination');
|
|
|
|
// Simplified pagination - just first page for now
|
|
pagination.innerHTML = `
|
|
<li class="page-item active">
|
|
<a class="page-link" href="#">1</a>
|
|
</li>
|
|
`;
|
|
}
|
|
|
|
function getLevelClass(level) {
|
|
switch (level.toLowerCase()) {
|
|
case 'debug':
|
|
return 'bg-secondary';
|
|
case 'information':
|
|
return 'bg-info';
|
|
case 'warning':
|
|
return 'bg-warning';
|
|
case 'error':
|
|
return 'bg-danger';
|
|
case 'critical':
|
|
return 'bg-dark';
|
|
default:
|
|
return 'bg-secondary';
|
|
}
|
|
}
|
|
|
|
function applyLogFilters() {
|
|
const filters = getLogFilters();
|
|
loadLogs(filters);
|
|
}
|
|
|
|
function resetLogFilters() {
|
|
document.getElementById('log-level').value = 'All';
|
|
document.getElementById('log-search').value = '';
|
|
document.getElementById('log-date-range').value = 'week';
|
|
loadLogs(getLogFilters());
|
|
}
|
|
|
|
function clearLogs() {
|
|
if (!confirm('Are you sure you want to clear all logs? This action cannot be undone.')) {
|
|
return;
|
|
}
|
|
|
|
fetch('/api/logs/clear', {
|
|
method: 'POST'
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to clear logs');
|
|
}
|
|
|
|
// Refresh logs
|
|
loadLogs(getLogFilters());
|
|
})
|
|
.catch(error => {
|
|
console.error('Error clearing logs:', error);
|
|
alert('Error clearing logs');
|
|
});
|
|
}
|
|
|
|
function exportLogs() {
|
|
const filters = getLogFilters();
|
|
const query = new URLSearchParams();
|
|
|
|
if (filters.level && filters.level !== 'All') {
|
|
query.append('Level', filters.level);
|
|
}
|
|
if (filters.search) {
|
|
query.append('Search', filters.search);
|
|
}
|
|
|
|
// Create download link
|
|
const link = document.createElement('a');
|
|
link.href = `/api/logs/export?${query.toString()}`;
|
|
link.download = `transmission-rss-logs-${new Date().toISOString().slice(0, 10)}.csv`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
|
|
// Helper function to format file sizes
|
|
function formatBytes(bytes, decimals = 2) {
|
|
if (bytes === 0) return '0 B';
|
|
|
|
const k = 1024;
|
|
const dm = decimals < 0 ? 0 : decimals;
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
}
|
|
|
|
// Configuration Operations
|
|
function backupConfig() {
|
|
if (!confirm('This will create a backup of your configuration files. Do you want to continue?')) {
|
|
return;
|
|
}
|
|
|
|
fetch('/api/config/backup', {
|
|
method: 'POST'
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to backup configuration');
|
|
}
|
|
return response.blob();
|
|
})
|
|
.then(blob => {
|
|
// Create download link for the backup file
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.style.display = 'none';
|
|
a.href = url;
|
|
a.download = `transmission-rss-config-backup-${new Date().toISOString().slice(0, 10)}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
alert('Configuration backup created successfully.');
|
|
})
|
|
.catch(error => {
|
|
console.error('Error backing up configuration:', error);
|
|
alert('Error creating configuration backup.');
|
|
});
|
|
}
|
|
|
|
function resetConfig() {
|
|
if (!confirm('WARNING: This will reset your configuration to default settings. All your feeds, rules, and user preferences will be lost. This cannot be undone. Are you absolutely sure?')) {
|
|
return;
|
|
}
|
|
|
|
if (!confirm('FINAL WARNING: All feeds, rules, and settings will be permanently deleted. Type "RESET" to confirm.')) {
|
|
return;
|
|
}
|
|
|
|
const confirmation = prompt('Type "RESET" to confirm configuration reset:');
|
|
if (confirmation !== 'RESET') {
|
|
alert('Configuration reset cancelled.');
|
|
return;
|
|
}
|
|
|
|
fetch('/api/config/reset', {
|
|
method: 'POST'
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to reset configuration');
|
|
}
|
|
|
|
alert('Configuration has been reset to defaults. The application will now reload.');
|
|
window.location.reload();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error resetting configuration:', error);
|
|
alert('Error resetting configuration.');
|
|
});
|
|
}
|
|
|
|
// Transmission Instance Management
|
|
function addTransmissionInstance() {
|
|
const instancesList = document.getElementById('transmission-instances-list');
|
|
const instanceCount = document.querySelectorAll('.transmission-instance').length;
|
|
const newInstanceIndex = instanceCount + 1;
|
|
|
|
const instanceHtml = `
|
|
<div class="transmission-instance card mb-3" id="transmission-instance-${newInstanceIndex}">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between mb-3">
|
|
<h5 class="card-title">Instance #${newInstanceIndex}</h5>
|
|
<button type="button" class="btn btn-sm btn-danger" onclick="removeTransmissionInstance(${newInstanceIndex})">
|
|
<i class="bi bi-trash me-1"></i>Remove
|
|
</button>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Name</label>
|
|
<input type="text" class="form-control" name="transmissionInstances[${newInstanceIndex}].name" placeholder="Secondary Server">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Host</label>
|
|
<input type="text" class="form-control" name="transmissionInstances[${newInstanceIndex}].host">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Port</label>
|
|
<input type="number" class="form-control" name="transmissionInstances[${newInstanceIndex}].port" value="9091">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Username</label>
|
|
<input type="text" class="form-control" name="transmissionInstances[${newInstanceIndex}].username">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Password</label>
|
|
<input type="password" class="form-control" name="transmissionInstances[${newInstanceIndex}].password">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-check form-switch mt-4">
|
|
<input class="form-check-input" type="checkbox" name="transmissionInstances[${newInstanceIndex}].useHttps">
|
|
<label class="form-check-label">Use HTTPS</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
// If the "no instances" message is showing, remove it
|
|
if (instancesList.querySelector('.text-center.text-muted')) {
|
|
instancesList.innerHTML = '';
|
|
}
|
|
|
|
// Add the new instance
|
|
instancesList.insertAdjacentHTML('beforeend', instanceHtml);
|
|
}
|
|
|
|
function removeTransmissionInstance(index) {
|
|
const instance = document.getElementById(`transmission-instance-${index}`);
|
|
if (instance) {
|
|
instance.remove();
|
|
|
|
// If there are no instances left, show the "no instances" message
|
|
const instancesList = document.getElementById('transmission-instances-list');
|
|
if (instancesList.children.length === 0) {
|
|
instancesList.innerHTML = '<div class="text-center text-muted py-3">No additional instances configured</div>';
|
|
}
|
|
}
|
|
}
|
|
|
|
function resetSettings() {
|
|
if (!confirm('This will reset all settings to their default values. Are you sure?')) {
|
|
return;
|
|
}
|
|
|
|
// Load default settings
|
|
fetch('/api/config/defaults')
|
|
.then(response => response.json())
|
|
.then(defaults => {
|
|
// Apply defaults to form
|
|
loadSettingsIntoForm(defaults);
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading default settings:', error);
|
|
alert('Error loading default settings');
|
|
});
|
|
} |