1386 lines
56 KiB
HTML
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 |