1386 lines
56 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transmission RSS Manager</title>
<style>
/* Base styles */
:root {
--primary-color: #3498db;
--secondary-color: #2ecc71;
--warning-color: #f39c12;
--danger-color: #e74c3c;
--background-color: #f8f9fa;
--card-background: #ffffff;
--text-color: #333333;
--border-color: #dddddd;
}
* {
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--background-color);
margin: 0;
padding: 0;
}
a {
color: var(--primary-color);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
button {
cursor: pointer;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 14px;
background-color: var(--primary-color);
color: white;
transition: background-color 0.3s;
}
button:hover {
opacity: 0.9;
}
button.success {
background-color: var(--secondary-color);
}
button.warning {
background-color: var(--warning-color);
}
button.danger {
background-color: var(--danger-color);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background-color: var(--primary-color);
color: white;
padding: 20px;
margin-bottom: 20px;
}
header h1 {
margin: 0;
font-size: 24px;
}
/* Navigation */
nav ul {
display: flex;
list-style: none;
margin: 10px 0 0 0;
padding: 0;
}
nav li {
margin-right: 20px;
}
nav a {
color: white;
opacity: 0.8;
transition: opacity 0.3s;
}
nav a:hover,
nav a.active {
opacity: 1;
text-decoration: none;
}
/* Tabs */
.tabs {
display: flex;
margin-bottom: 20px;
}
.tab {
padding: 10px 20px;
cursor: pointer;
background-color: #f1f1f1;
border: 1px solid var(--border-color);
border-bottom: none;
margin-right: 5px;
border-radius: 4px 4px 0 0;
}
.tab:hover {
background-color: #e8e8e8;
}
.tab.active {
background-color: white;
border-bottom: 2px solid var(--primary-color);
color: var(--primary-color);
font-weight: bold;
}
.tab-content {
display: none;
padding: 20px;
border: 1px solid var(--border-color);
border-radius: 0 4px 4px 4px;
background-color: white;
}
.tab-content.active {
display: block;
}
/* Cards */
.card {
background-color: var(--card-background);
border-radius: 8px;
border: 1px solid var(--border-color);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
padding: 20px;
}
.card h2 {
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
margin-top: 0;
}
.card h3 {
margin-top: 20px;
margin-bottom: 10px;
}
/* Forms */
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.form-group input[type="text"],
.form-group input[type="number"],
.form-group input[type="password"],
.form-group select {
width: 100%;
padding: 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
}
/* Grids and layouts */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.mt-2 {
margin-top: 20px;
}
.mb-2 {
margin-bottom: 20px;
}
.text-right {
text-align: right;
}
/* Status message */
.status-message {
padding: 10px 15px;
margin: 20px 0;
border-radius: 4px;
background-color: #f8f9fa;
border: 1px solid #ddd;
}
.status-success {
background-color: #d4edda;
border-color: #c3e6cb;
color: #155724;
}
.status-error {
background-color: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}
/* Table */
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
table th,
table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
table th {
background-color: #f5f5f5;
font-weight: 600;
}
table tr:hover {
background-color: #f9f9f9;
}
.actions {
display: flex;
gap: 5px;
}
.actions button {
padding: 4px 8px;
font-size: 12px;
}
/* Responsive */
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr;
}
nav ul {
flex-direction: column;
}
nav li {
margin-bottom: 5px;
}
.tab {
padding: 8px 12px;
font-size: 14px;
}
}
</style>
</head>
<body>
<header>
<h1>Transmission RSS Manager</h1>
<nav>
<ul>
<li><a href="#home" class="active" data-tab="home-tab">Home</a></li>
<li><a href="#rss" data-tab="rss-tab">RSS Feeds</a></li>
<li><a href="#torrents" data-tab="torrents-tab">Torrents</a></li>
<li><a href="#media" data-tab="media-tab">Media Library</a></li>
<li><a href="#settings" data-tab="settings-tab">Settings</a></li>
</ul>
</nav>
</header>
<div class="container">
<div id="status-message" class="status-message" style="display: none;"></div>
<!-- Home Tab -->
<div id="home-tab" class="tab-content active">
<div class="card">
<h2>Dashboard</h2>
<div id="status-container">
<p>Loading system status...</p>
</div>
<h3>Quick Actions</h3>
<div class="grid">
<div class="card">
<h4>RSS Manager</h4>
<p>Manage your RSS feeds and automatic downloads</p>
<button id="update-rss-feeds" class="success">Update Feeds Now</button>
</div>
<div class="card">
<h4>Post-Processing</h4>
<p>Process completed downloads to your media library</p>
<button id="start-processing" class="success">Start Processor</button>
<button id="stop-processing" class="warning">Stop Processor</button>
</div>
<div class="card">
<h4>Add New Torrent</h4>
<p>Add a torrent URL directly to Transmission</p>
<div class="form-group">
<input type="text" id="torrent-url" placeholder="Enter torrent URL or magnet link">
</div>
<button id="add-torrent" class="success">Add Torrent</button>
</div>
</div>
</div>
</div>
<!-- RSS Tab -->
<div id="rss-tab" class="tab-content">
<div class="card">
<h2>RSS Feeds</h2>
<p>Manage your RSS feeds and download filters</p>
<button id="add-feed-button" class="success mb-2">Add New Feed</button>
<div id="feeds-container">
<p>Loading feeds...</p>
</div>
</div>
<div class="card">
<h2>Available Items</h2>
<p>Browse and download items from your RSS feeds</p>
<div class="mb-2">
<label>
<input type="radio" name="item-filter" value="all" checked> All Items
</label>
<label>
<input type="radio" name="item-filter" value="undownloaded"> Undownloaded Only
</label>
</div>
<div id="items-container">
<p>Loading items...</p>
</div>
</div>
</div>
<!-- Torrents Tab -->
<div id="torrents-tab" class="tab-content">
<div class="card">
<h2>Active Torrents</h2>
<div class="mb-2">
<button id="refresh-torrents" class="success">Refresh</button>
</div>
<div id="torrents-container">
<p>Loading torrents...</p>
</div>
</div>
</div>
<!-- Media Library Tab -->
<div id="media-tab" class="tab-content">
<div class="card">
<h2>Media Library</h2>
<div class="mb-2">
<label>
<input type="radio" name="library-filter" value="all" checked> All
</label>
<label>
<input type="radio" name="library-filter" value="movies"> Movies
</label>
<label>
<input type="radio" name="library-filter" value="tvShows"> TV Shows
</label>
<label>
<input type="radio" name="library-filter" value="music"> Music
</label>
<label>
<input type="radio" name="library-filter" value="books"> Books
</label>
<label>
<input type="radio" name="library-filter" value="magazines"> Magazines
</label>
<label>
<input type="radio" name="library-filter" value="software"> Software
</label>
</div>
<div class="form-group">
<input type="text" id="library-search" placeholder="Search your library...">
</div>
<div id="library-container">
<p>Loading media library...</p>
</div>
</div>
</div>
<!-- Settings Tab -->
<div id="settings-tab" class="tab-content">
<div class="card">
<h2>Transmission Settings</h2>
<div class="form-group">
<label for="transmission-host">Host:</label>
<input type="text" id="transmission-host" placeholder="localhost">
</div>
<div class="form-group">
<label for="transmission-port">Port:</label>
<input type="number" id="transmission-port" placeholder="9091">
</div>
<div class="form-group">
<label for="transmission-user">Username:</label>
<input type="text" id="transmission-user">
</div>
<div class="form-group">
<label for="transmission-pass">Password:</label>
<input type="password" id="transmission-pass">
</div>
<div class="form-group">
<button id="test-connection" class="success">Test Connection</button>
<button id="save-transmission-settings" class="success">Save Settings</button>
</div>
</div>
<div class="card">
<h2>Post-Processing Settings</h2>
<h3>Seeding Requirements</h3>
<div class="form-group">
<label for="seeding-ratio">Minimum Seeding Ratio:</label>
<input type="number" id="seeding-ratio" min="0" step="0.1" placeholder="1.0">
</div>
<div class="form-group">
<label for="seeding-time">Minimum Seeding Time (minutes):</label>
<input type="number" id="seeding-time" min="0" placeholder="60">
</div>
<div class="form-group">
<label for="check-interval">Check Interval (seconds):</label>
<input type="number" id="check-interval" min="30" placeholder="300">
</div>
<h3>Media Paths</h3>
<div class="form-group">
<label for="movies-path">Movies Path:</label>
<input type="text" id="movies-path" placeholder="/mnt/media/movies">
</div>
<div class="form-group">
<label for="tvshows-path">TV Shows Path:</label>
<input type="text" id="tvshows-path" placeholder="/mnt/media/tvshows">
</div>
<div class="form-group">
<label for="music-path">Music Path:</label>
<input type="text" id="music-path" placeholder="/mnt/media/music">
</div>
<div class="form-group">
<label for="books-path">Books Path:</label>
<input type="text" id="books-path" placeholder="/mnt/media/books">
</div>
<div class="form-group">
<label for="magazines-path">Magazines Path:</label>
<input type="text" id="magazines-path" placeholder="/mnt/media/magazines">
</div>
<div class="form-group">
<label for="software-path">Software Path:</label>
<input type="text" id="software-path" placeholder="/mnt/media/software">
</div>
<h3>Archive Processing</h3>
<div class="form-group">
<label>
<input type="checkbox" id="extract-archives"> Extract Archives
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="delete-archives"> Delete Archives After Extraction
</label>
</div>
<h3>File Organization</h3>
<div class="form-group">
<label>
<input type="checkbox" id="create-category-folders"> Create Category Folders
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="rename-files"> Rename Files
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="ignore-sample"> Ignore Sample Files
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="ignore-extras"> Ignore Extras Files
</label>
</div>
<button id="save-processing-settings" class="success">Save Settings</button>
<div class="mt-2">
<button id="start-processor" class="success">Start Post-Processor</button>
<button id="stop-processor" class="warning">Stop Post-Processor</button>
</div>
</div>
<div class="card">
<h2>RSS Settings</h2>
<div class="form-group">
<label for="rss-interval">Update Interval (minutes):</label>
<input type="number" id="rss-interval" min="5" placeholder="60">
</div>
<button id="save-rss-settings" class="success">Save Settings</button>
</div>
</div>
</div>
<script src="/js/enhanced-ui.js"></script>
<script>
// Status message handling
const statusMessage = document.getElementById('status-message');
// Tab navigation
const tabLinks = document.querySelectorAll('nav a');
const tabContents = document.querySelectorAll('.tab-content');
tabLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
// Update active tab link
tabLinks.forEach(l => l.classList.remove('active'));
link.classList.add('active');
// Show the corresponding tab content
const tabId = link.getAttribute('data-tab');
tabContents.forEach(tab => {
tab.classList.remove('active');
});
document.getElementById(tabId).classList.add('active');
});
});
// Load data functions
function loadStatusData() {
fetch('/api/status')
.then(response => response.json())
.then(data => {
const statusHtml = `
<div class="grid">
<div class="card">
<h4>System Status</h4>
<p><strong>Version:</strong> ${data.version}</p>
<p><strong>Status:</strong> <span style="color: green;">Running</span></p>
</div>
<div class="card">
<h4>Transmission</h4>
<p><strong>Status:</strong> ${data.transmissionConnected ? '<span style="color: green;">Connected</span>' : '<span style="color: red;">Disconnected</span>'}</p>
</div>
<div class="card">
<h4>Post-Processor</h4>
<p><strong>Status:</strong> ${data.postProcessorActive ? '<span style="color: green;">Running</span>' : '<span style="color: orange;">Stopped</span>'}</p>
<p><strong>Auto-Processing:</strong> ${data.config.autoProcessing ? 'Enabled' : 'Disabled'}</p>
</div>
<div class="card">
<h4>RSS Manager</h4>
<p><strong>Status:</strong> ${data.rssFeedManagerActive ? '<span style="color: green;">Running</span>' : '<span style="color: orange;">Stopped</span>'}</p>
<p><strong>Feeds Active:</strong> ${data.config.rssEnabled ? 'Yes' : 'No'}</p>
</div>
</div>
`;
document.getElementById('status-container').innerHTML = statusHtml;
})
.catch(error => {
document.getElementById('status-container').innerHTML = `
<div class="status-error">
<p>Error loading status data: ${error.message}</p>
</div>
`;
console.error('Error loading items:', error);
});
}
function loadLibraryData() {
fetch('/api/media/library')
.then(response => response.json())
.then(data => {
if (!data.success) {
document.getElementById('library-container').innerHTML = `
<div class="status-error">
<p>${data.message}</p>
</div>
`;
return;
}
if (!data.data || Object.keys(data.data).every(key => data.data[key].length === 0)) {
document.getElementById('library-container').innerHTML = `
<p>No media files in library.</p>
`;
return;
}
const selectedCategory = document.querySelector('input[name="library-filter"]:checked').value;
const categories = selectedCategory === 'all' ? Object.keys(data.data) : [selectedCategory];
let html = '';
categories.forEach(category => {
if (!data.data[category] || data.data[category].length === 0) {
return;
}
const displayName = getCategoryTitle(category);
const items = data.data[category];
html += `
<div class="library-section" data-category="${category}">
<h3>${displayName} (${items.length})</h3>
<div class="grid">
`;
items.forEach(item => {
html += `
<div class="card">
<h4>${item.name}</h4>
<p><small>Added: ${new Date(item.added).toLocaleDateString()}</small></p>
<p class="text-right">
<button class="success" data-action="play" data-path="${item.path}">Open</button>
</p>
</div>
`;
});
html += `
</div>
</div>
`;
});
document.getElementById('library-container').innerHTML = html;
})
.catch(error => {
document.getElementById('library-container').innerHTML = `
<div class="status-error">
<p>Error loading library: ${error.message}</p>
</div>
`;
console.error('Error loading library:', error);
});
}
function loadSettingsData() {
fetch("/api/config")
.then(response => response.json())
.then(config => {
// Transmission settings
document.getElementById("transmission-host").value = config.transmissionConfig.host || '';
document.getElementById("transmission-port").value = config.transmissionConfig.port || '';
document.getElementById("transmission-user").value = config.transmissionConfig.username || '';
// Password is masked in the API response, so we leave it blank
// Post-processing settings
document.getElementById("seeding-ratio").value = config.seedingRequirements.minRatio || '';
document.getElementById("seeding-time").value = config.seedingRequirements.minTimeMinutes || '';
document.getElementById("check-interval").value = config.seedingRequirements.checkIntervalSeconds || '';
// Media paths
document.getElementById("movies-path").value = config.destinationPaths.movies || '';
document.getElementById("tvshows-path").value = config.destinationPaths.tvShows || '';
document.getElementById("music-path").value = config.destinationPaths.music || '';
document.getElementById("books-path").value = config.destinationPaths.books || '';
document.getElementById("magazines-path").value = config.destinationPaths.magazines || '';
document.getElementById("software-path").value = config.destinationPaths.software || '';
// Archive options
document.getElementById("extract-archives").checked = config.processingOptions.extractArchives;
document.getElementById("delete-archives").checked = config.processingOptions.deleteArchives;
// File organization options
document.getElementById("create-category-folders").checked = config.processingOptions.createCategoryFolders;
document.getElementById("rename-files").checked = config.processingOptions.renameFiles;
document.getElementById("ignore-sample").checked = config.processingOptions.ignoreSample;
document.getElementById("ignore-extras").checked = config.processingOptions.ignoreExtras;
// RSS settings
document.getElementById("rss-interval").value = config.rssUpdateIntervalMinutes || '';
})
.catch(error => {
console.error("Error loading configuration:", error);
});
}
function getCategoryTitle(category) {
switch(category) {
case 'movies': return 'Movies';
case 'tvShows': return 'TV Shows';
case 'music': return 'Music';
case 'books': return 'Books';
case 'magazines': return 'Magazines';
case 'software': return 'Software';
default: return category.charAt(0).toUpperCase() + category.slice(1);
}
}
// Torrent actions
function startTorrent(id) {
fetch('/api/transmission/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: id })
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusMessage.textContent = 'Torrent started successfully!';
statusMessage.className = 'status-message status-success';
statusMessage.style.display = 'block';
// Reload torrents
loadTorrentsData();
} else {
statusMessage.textContent = `Error starting torrent: ${data.message}`;
statusMessage.className = 'status-message status-error';
statusMessage.style.display = 'block';
}
})
.catch(error => {
statusMessage.textContent = `Error: ${error.message}`;
statusMessage.className = 'status-message status-error';
statusMessage.style.display = 'block';
console.error('Error starting torrent:', error);
});
}
function stopTorrent(id) {
fetch('/api/transmission/stop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: id })
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusMessage.textContent = 'Torrent stopped successfully!';
statusMessage.className = 'status-message status-success';
statusMessage.style.display = 'block';
// Reload torrents
loadTorrentsData();
} else {
statusMessage.textContent = `Error stopping torrent: ${data.message}`;
statusMessage.className = 'status-message status-error';
statusMessage.style.display = 'block';
}
})
.catch(error => {
statusMessage.textContent = `Error: ${error.message}`;
statusMessage.className = 'status-message status-error';
statusMessage.style.display = 'block';
console.error('Error stopping torrent:', error);
});
}
function removeTorrent(id) {
if (!confirm('Are you sure you want to remove this torrent? This will also delete the local data.')) {
return;
}
fetch('/api/transmission/remove', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: id, deleteLocalData: true })
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusMessage.textContent = 'Torrent removed successfully!';
statusMessage.className = 'status-message status-success';
statusMessage.style.display = 'block';
// Reload torrents
loadTorrentsData();
} else {
statusMessage.textContent = `Error removing torrent: ${data.message}`;
statusMessage.className = 'status-message status-error';
statusMessage.style.display = 'block';
}
})
.catch(error => {
statusMessage.textContent = `Error: ${error.message}`;
statusMessage.className = 'status-message status-error';
statusMessage.style.display = 'block';
console.error('Error removing torrent:', error);
});
}
// Settings actions
function testTransmissionConnection() {
const host = document.getElementById('transmission-host').value;
const port = document.getElementById('transmission-port').value;
const username = document.getElementById('transmission-user').value;
const password = document.getElementById('transmission-pass').value;
statusMessage.textContent = 'Testing connection...';
statusMessage.className = 'status-message';
statusMessage.style.display = 'block';
fetch('/api/transmission/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ host, port, username, password })
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusMessage.textContent = `${data.message} (Version: ${data.data.version})`;
statusMessage.className = 'status-message status-success';
} else {
statusMessage.textContent = data.message;
statusMessage.className = 'status-message status-error';
}
})
.catch(error => {
statusMessage.textContent = `Error: ${error.message}`;
statusMessage.className = 'status-message status-error';
console.error('Error testing connection:', error);
});
}
function saveTransmissionSettings() {
const host = document.getElementById('transmission-host').value;
const port = document.getElementById('transmission-port').value;
const username = document.getElementById('transmission-user').value;
const password = document.getElementById('transmission-pass').value;
statusMessage.textContent = 'Saving transmission settings...';
statusMessage.className = 'status-message';
statusMessage.style.display = 'block';
fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
transmissionConfig: {
host,
port: parseInt(port, 10),
username,
password
}
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusMessage.textContent = 'Transmission settings saved successfully!';
statusMessage.className = 'status-message status-success';
} else {
statusMessage.textContent = `Error saving settings: ${data.message}`;
statusMessage.className = 'status-message status-error';
}
})
.catch(error => {
statusMessage.textContent = `Error: ${error.message}`;
statusMessage.className = 'status-message status-error';
console.error('Error saving settings:', error);
});
}
function saveProcessingSettings() {
const minRatio = parseFloat(document.getElementById('seeding-ratio').value);
const minTimeMinutes = parseInt(document.getElementById('seeding-time').value, 10);
const checkIntervalSeconds = parseInt(document.getElementById('check-interval').value, 10);
const moviesPath = document.getElementById('movies-path').value;
const tvShowsPath = document.getElementById('tvshows-path').value;
const musicPath = document.getElementById('music-path').value;
const booksPath = document.getElementById('books-path').value;
const magazinesPath = document.getElementById('magazines-path').value;
const softwarePath = document.getElementById('software-path').value;
const extractArchives = document.getElementById('extract-archives').checked;
const deleteArchives = document.getElementById('delete-archives').checked;
const createCategoryFolders = document.getElementById('create-category-folders').checked;
const renameFiles = document.getElementById('rename-files').checked;
const ignoreSample = document.getElementById('ignore-sample').checked;
const ignoreExtras = document.getElementById('ignore-extras').checked;
statusMessage.textContent = 'Saving processing settings...';
statusMessage.className = 'status-message';
statusMessage.style.display = 'block';
fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
seedingRequirements: {
minRatio,
minTimeMinutes,
checkIntervalSeconds
},
destinationPaths: {
movies: moviesPath,
tvShows: tvShowsPath,
music: musicPath,
books: booksPath,
magazines: magazinesPath,
software: softwarePath
},
processingOptions: {
extractArchives,
deleteArchives,
createCategoryFolders,
renameFiles,
ignoreSample,
ignoreExtras
}
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusMessage.textContent = 'Processing settings saved successfully!';
statusMessage.className = 'status-message status-success';
} else {
statusMessage.textContent = `Error saving settings: ${data.message}`;
statusMessage.className = 'status-message status-error';
}
})
.catch(error => {
statusMessage.textContent = `Error: ${error.message}`;
statusMessage.className = 'status-message status-error';
console.error('Error saving settings:', error);
});
}
function saveRssSettings() {
const rssInterval = parseInt(document.getElementById('rss-interval').value, 10);
statusMessage.textContent = 'Saving RSS settings...';
statusMessage.className = 'status-message';
statusMessage.style.display = 'block';
fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rssUpdateIntervalMinutes: rssInterval
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusMessage.textContent = 'RSS settings saved successfully!';
statusMessage.className = 'status-message status-success';
} else {
statusMessage.textContent = `Error saving settings: ${data.message}`;
statusMessage.className = 'status-message status-error';
}
})
.catch(error => {
statusMessage.textContent = `Error: ${error.message}`;
statusMessage.className = 'status-message status-error';
console.error('Error saving settings:', error);
});
}
function startPostProcessor() {
statusMessage.textContent = 'Starting post-processor...';
statusMessage.className = 'status-message';
statusMessage.style.display = 'block';
fetch('/api/post-processor/start', { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.success) {
statusMessage.textContent = 'Post-processor started successfully!';
statusMessage.className = 'status-message status-success';
} else {
statusMessage.textContent = `Error starting post-processor: ${data.message}`;
statusMessage.className = 'status-message status-error';
}
})
.catch(error => {
statusMessage.textContent = `Error: ${error.message}`;
statusMessage.className = 'status-message status-error';
console.error('Error starting post-processor:', error);
});
}
function stopPostProcessor() {
statusMessage.textContent = 'Stopping post-processor...';
statusMessage.className = 'status-message';
statusMessage.style.display = 'block';
fetch('/api/post-processor/stop', { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.success) {
statusMessage.textContent = 'Post-processor stopped successfully!';
statusMessage.className = 'status-message status-success';
} else {
statusMessage.textContent = `Error stopping post-processor: ${data.message}`;
statusMessage.className = 'status-message status-error';
}
})
.catch(error => {
statusMessage.textContent = `Error: ${error.message}`;
statusMessage.className = 'status-message status-error';
console.error('Error stopping post-processor:', error);
});
}
// Initialize page
document.addEventListener('DOMContentLoaded', () => {
// Load initial data for each tab
loadStatusData();
// Add event listeners for filter changes
document.querySelectorAll('input[name="item-filter"]').forEach(input => {
input.addEventListener('change', loadRssData);
});
document.querySelectorAll('input[name="library-filter"]').forEach(input => {
input.addEventListener('change', loadLibraryData);
});
// Add event listeners for buttons
document.getElementById('refresh-torrents').addEventListener('click', loadTorrentsData);
document.getElementById('update-rss-feeds').addEventListener('click', () => {
statusMessage.textContent = 'Updating RSS feeds...';
statusMessage.className = 'status-message';
statusMessage.style.display = 'block';
fetch('/api/rss/update', { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.success) {
statusMessage.textContent = 'RSS feeds updated successfully!';
statusMessage.className = 'status-message status-success';
loadRssData();
} else {
statusMessage.textContent = `Error updating RSS feeds: ${data.message}`;
statusMessage.className = 'status-message status-error';
}
})
.catch(error => {
statusMessage.textContent = `Error: ${error.message}`;
statusMessage.className = 'status-message status-error';
console.error('Error updating RSS feeds:', error);
});
});
document.getElementById('add-feed-button').addEventListener('click', addFeed);
document.getElementById('add-torrent').addEventListener('click', () => {
const url = document.getElementById('torrent-url').value.trim();
if (!url) {
alert('Please enter a torrent URL');
return;
}
statusMessage.textContent = 'Adding torrent...';
statusMessage.className = 'status-message';
statusMessage.style.display = 'block';
fetch('/api/transmission/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusMessage.textContent = 'Torrent added successfully!';
statusMessage.className = 'status-message status-success';
document.getElementById('torrent-url').value = '';
loadTorrentsData();
} else {
statusMessage.textContent = `Error adding torrent: ${data.message}`;
statusMessage.className = 'status-message status-error';
}
})
.catch(error => {
statusMessage.textContent = `Error: ${error.message}`;
statusMessage.className = 'status-message status-error';
console.error('Error adding torrent:', error);
});
});
document.getElementById('start-processing').addEventListener('click', startPostProcessor);
document.getElementById('stop-processing').addEventListener('click', stopPostProcessor);
document.getElementById('start-processor').addEventListener('click', startPostProcessor);
document.getElementById('stop-processor').addEventListener('click', stopPostProcessor);
// Load library data after a slight delay (after the page loads)
setTimeout(() => {
loadTorrentsData();
loadRssData();
loadLibraryData();
loadSettingsData();
}, 500);
});
</script>
</body>
</html>('Error loading status:', error);
});
}
function loadTorrentsData() {
fetch('/api/transmission/torrents')
.then(response => response.json())
.then(data => {
if (!data.success) {
document.getElementById('torrents-container').innerHTML = `
<div class="status-error">
<p>${data.message}</p>
</div>
`;
return;
}
if (!data.data || data.data.length === 0) {
document.getElementById('torrents-container').innerHTML = `
<p>No active torrents.</p>
`;
return;
}
let html = `
<table>
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Progress</th>
<th>Size</th>
<th>Ratio</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
`;
data.data.forEach(torrent => {
const status = getTorrentStatus(torrent);
const progressPercent = Math.round(torrent.percentDone * 100);
const size = formatBytes(torrent.totalSize);
html += `
<tr>
<td>${torrent.name}</td>
<td>${status}</td>
<td>${progressPercent}%</td>
<td>${size}</td>
<td>${torrent.uploadRatio.toFixed(2)}</td>
<td class="actions">
${torrent.status === 0 ?
`<button class="success" onclick="startTorrent(${torrent.id})">Start</button>` :
`<button class="warning" onclick="stopTorrent(${torrent.id})">Stop</button>`
}
<button class="danger" onclick="removeTorrent(${torrent.id})">Remove</button>
</td>
</tr>
`;
});
html += `
</tbody>
</table>
`;
document.getElementById('torrents-container').innerHTML = html;
})
.catch(error => {
document.getElementById('torrents-container').innerHTML = `
<div class="status-error">
<p>Error loading torrents: ${error.message}</p>
</div>
`;
console.error('Error loading torrents:', error);
});
}
function getTorrentStatus(torrent) {
const statusMap = {
0: 'Stopped',
1: 'Check Waiting',
2: 'Checking',
3: 'Download Waiting',
4: 'Downloading',
5: 'Seed Waiting',
6: 'Seeding'
};
return statusMap[torrent.status] || 'Unknown';
}
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', '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];
}
function loadRssData() {
// Load RSS feeds
fetch('/api/rss/feeds')
.then(response => response.json())
.then(data => {
if (!data.success) {
document.getElementById('feeds-container').innerHTML = `
<div class="status-error">
<p>${data.message}</p>
</div>
`;
return;
}
if (!data.data || data.data.length === 0) {
document.getElementById('feeds-container').innerHTML = `
<p>No RSS feeds configured. Click "Add New Feed" to add one.</p>
`;
return;
}
let html = `
<table>
<thead>
<tr>
<th>Name</th>
<th>URL</th>
<th>Auto-Download</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
`;
data.data.forEach(feed => {
html += `
<tr>
<td>${feed.name}</td>
<td><a href="${feed.url}" target="_blank">${feed.url}</a></td>
<td>${feed.autoDownload ? 'Yes' : 'No'}</td>
<td class="actions">
<button class="success" onclick="editFeed('${feed.id}')">Edit</button>
<button class="danger" onclick="deleteFeed('${feed.id}')">Delete</button>
</td>
</tr>
`;
});
html += `
</tbody>
</table>
`;
document.getElementById('feeds-container').innerHTML = html;
})
.catch(error => {
document.getElementById('feeds-container').innerHTML = `
<div class="status-error">
<p>Error loading feeds: ${error.message}</p>
</div>
`;
console.error('Error loading feeds:', error);
});
// Load RSS items
const itemFilter = document.querySelector('input[name="item-filter"]:checked').value;
fetch(`/api/rss/items?filter=${itemFilter}`)
.then(response => response.json())
.then(data => {
if (!data.success) {
document.getElementById('items-container').innerHTML = `
<div class="status-error">
<p>${data.message}</p>
</div>
`;
return;
}
if (!data.data || data.data.length === 0) {
document.getElementById('items-container').innerHTML = `
<p>No items found.</p>
`;
return;
}
let html = `
<table>
<thead>
<tr>
<th>Title</th>
<th>Feed</th>
<th>Size</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
`;
data.data.forEach(item => {
const feed = data.data.find(f => f.id === item.feedId);
const feedName = feed ? feed.name : 'Unknown';
const size = item.size ? formatBytes(item.size) : 'Unknown';
const date = new Date(item.pubDate).toLocaleDateString();
html += `
<tr>
<td>${item.title}</td>
<td>${feedName}</td>
<td>${size}</td>
<td>${date}</td>
<td class="actions">
<button class="success" onclick="downloadRssItem('${item.id}')" ${item.downloaded ? 'disabled' : ''}>
${item.downloaded ? 'Downloaded' : 'Download'}
</button>
</td>
</tr>
`;
});
html += `
</tbody>
</table>
`;
document.getElementById('items-container').innerHTML = html;
})
.catch(error => {
document.getElementById('items-container').innerHTML = `
<div class="status-error">
<p>Error loading items: ${error.message}</p>
</div>
`;
console.error