Initial commit with UI fixes for dark mode

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>
This commit is contained in:
Claude
2025-03-13 17:16:41 +00:00
commit 9e544456db
66 changed files with 20187 additions and 0 deletions

121
modules/config-module.sh Executable file
View File

@@ -0,0 +1,121 @@
#\!/bin/bash
# This module handles configuration related tasks
create_default_config() {
local config_file=$1
# Check if config file already exists
if [ -f "$config_file" ]; then
echo "Configuration file already exists at $config_file"
return 0
fi
echo "Creating default configuration..."
# Create default appsettings.json
cat > "$config_file" << EOL
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"DatabaseSettings": {
"ConnectionString": "Data Source=torrentmanager.db",
"UseInMemoryDatabase": true
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "logs/log-.txt",
"rollingInterval": "Day",
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
}
]
},
"AppConfig": {
"Transmission": {
"Host": "localhost",
"Port": 9091,
"UseHttps": false,
"Username": "",
"Password": ""
},
"AutoDownloadEnabled": false,
"CheckIntervalMinutes": 30,
"DownloadDirectory": "",
"MediaLibraryPath": "",
"PostProcessing": {
"Enabled": false,
"ExtractArchives": true,
"OrganizeMedia": true,
"MinimumSeedRatio": 1,
"MediaExtensions": [
".mp4",
".mkv",
".avi"
],
"AutoOrganizeByMediaType": true,
"RenameFiles": false,
"CompressCompletedFiles": false,
"DeleteCompletedAfterDays": 0
},
"EnableDetailedLogging": false,
"UserPreferences": {
"EnableDarkMode": false,
"AutoRefreshUIEnabled": true,
"AutoRefreshIntervalSeconds": 30,
"NotificationsEnabled": true,
"NotificationEvents": [
"torrent-added",
"torrent-completed",
"torrent-error"
],
"DefaultView": "dashboard",
"ConfirmBeforeDelete": true,
"MaxItemsPerPage": 25,
"DateTimeFormat": "yyyy-MM-dd HH:mm:ss",
"ShowCompletedTorrents": true,
"KeepHistoryDays": 30
}
}
}
EOL
echo "Default configuration created"
}
update_config_value() {
local config_file=$1
local key=$2
local value=$3
# This is a simplified version that assumes jq is installed
# For a production script, you might want to add error checking
if command -v jq &> /dev/null; then
tmp=$(mktemp)
jq ".$key = \"$value\"" "$config_file" > "$tmp" && mv "$tmp" "$config_file"
echo "Updated $key to $value"
else
echo "jq is not installed, skipping config update"
fi
}

85
modules/dependencies-module.sh Executable file
View File

@@ -0,0 +1,85 @@
#\!/bin/bash
# This module handles installing dependencies
install_dependencies() {
echo "Checking and installing dependencies..."
# Check if we're on a Debian/Ubuntu system
if command -v apt-get &> /dev/null; then
install_debian_dependencies
# Check if we're on a RHEL/CentOS/Fedora system
elif command -v yum &> /dev/null || command -v dnf &> /dev/null; then
install_rhel_dependencies
else
echo "Unsupported package manager. Please install dependencies manually."
return 1
fi
# Install .NET runtime if needed
install_dotnet
return 0
}
install_debian_dependencies() {
echo "Installing dependencies for Debian/Ubuntu..."
sudo apt-get update
sudo apt-get install -y wget curl jq unzip
return 0
}
install_rhel_dependencies() {
echo "Installing dependencies for RHEL/CentOS/Fedora..."
if command -v dnf &> /dev/null; then
sudo dnf install -y wget curl jq unzip
else
sudo yum install -y wget curl jq unzip
fi
return 0
}
install_dotnet() {
echo "Checking .NET runtime..."
# Check if .NET 7.0 is already installed
if command -v dotnet &> /dev/null; then
dotnet_version=$(dotnet --version)
if [[ $dotnet_version == 7.* ]]; then
echo ".NET 7.0 is already installed (version $dotnet_version)"
return 0
fi
fi
echo "Installing .NET 7.0 runtime..."
# For Debian/Ubuntu
if command -v apt-get &> /dev/null; then
wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb
sudo apt-get update
sudo apt-get install -y dotnet-runtime-7.0
# For RHEL/CentOS/Fedora
elif command -v yum &> /dev/null || command -v dnf &> /dev/null; then
sudo rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm
if command -v dnf &> /dev/null; then
sudo dnf install -y dotnet-runtime-7.0
else
sudo yum install -y dotnet-runtime-7.0
fi
else
echo "Unsupported system for automatic .NET installation. Please install .NET 7.0 runtime manually."
return 1
fi
echo ".NET 7.0 runtime installed successfully"
return 0
}

74
modules/file-creator-module.sh Executable file
View File

@@ -0,0 +1,74 @@
#\!/bin/bash
# This module handles creating necessary files
# Function to create systemd service file
create_systemd_service_file() {
local install_dir=$1
local service_name=$2
local description=$3
local exec_command=$4
local service_file="/tmp/$service_name.service"
echo "Creating systemd service file for $service_name..."
cat > "$service_file" << EOL
[Unit]
Description=$description
After=network.target
[Service]
Type=simple
User=$(whoami)
WorkingDirectory=$install_dir
ExecStart=$exec_command
Restart=on-failure
RestartSec=10
SyslogIdentifier=$service_name
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.target
EOL
sudo mv "$service_file" "/etc/systemd/system/$service_name.service"
sudo systemctl daemon-reload
echo "Service file created for $service_name"
}
# Function to create a simple start script
create_start_script() {
local install_dir=$1
local script_path="$install_dir/start.sh"
echo "Creating start script..."
cat > "$script_path" << 'EOL'
#\!/bin/bash
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}Starting Transmission RSS Manager...${NC}"
echo -e "${GREEN}The web interface will be available at: http://localhost:5000${NC}"
echo -e "${YELLOW}Press Ctrl+C to stop the application${NC}"
./TransmissionRssManager --urls=http://0.0.0.0:5000
EOL
chmod +x "$script_path"
echo "Start script created at $script_path"
}
# Function to create logs directory
create_logs_directory() {
local install_dir=$1
local logs_dir="$install_dir/logs"
mkdir -p "$logs_dir"
echo "Logs directory created at $logs_dir"
}

341
modules/rss-feed-manager.js Normal file
View File

@@ -0,0 +1,341 @@
// RSS Feed Manager for Transmission RSS Manager
// This is a basic implementation that will be extended during installation
const fs = require('fs').promises;
const path = require('path');
const fetch = require('node-fetch');
const xml2js = require('xml2js');
const crypto = require('crypto');
class RssFeedManager {
constructor(config) {
this.config = config;
this.feeds = config.feeds || [];
this.updateIntervalMinutes = config.updateIntervalMinutes || 60;
this.updateIntervalId = null;
this.items = [];
this.dataDir = path.join(__dirname, '..', 'data');
}
// Start the RSS feed update process
start() {
if (this.updateIntervalId) {
return;
}
// Run immediately then set interval
this.updateAllFeeds();
this.updateIntervalId = setInterval(() => {
this.updateAllFeeds();
}, this.updateIntervalMinutes * 60 * 1000);
console.log(`RSS feed manager started, update interval: ${this.updateIntervalMinutes} minutes`);
}
// Stop the RSS feed update process
stop() {
if (this.updateIntervalId) {
clearInterval(this.updateIntervalId);
this.updateIntervalId = null;
console.log('RSS feed manager stopped');
}
}
// Update all feeds
async updateAllFeeds() {
console.log('Updating all RSS feeds...');
const results = [];
for (const feed of this.feeds) {
try {
const feedData = await this.fetchFeed(feed.url);
const parsedItems = this.parseFeedItems(feedData, feed.id);
// Add items to the list
this.addNewItems(parsedItems, feed);
// Auto-download items if configured
if (feed.autoDownload && feed.filters) {
await this.processAutoDownload(feed);
}
results.push({
feedId: feed.id,
name: feed.name,
url: feed.url,
success: true,
itemCount: parsedItems.length
});
} catch (error) {
console.error(`Error updating feed ${feed.name}:`, error);
results.push({
feedId: feed.id,
name: feed.name,
url: feed.url,
success: false,
error: error.message
});
}
}
// Save updated items to disk
await this.saveItems();
console.log(`RSS feeds update completed, processed ${results.length} feeds`);
return results;
}
// Fetch a feed from a URL
async fetchFeed(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.text();
} catch (error) {
console.error(`Error fetching feed from ${url}:`, error);
throw error;
}
}
// Parse feed items from XML
parseFeedItems(xmlData, feedId) {
const items = [];
try {
// Basic XML parsing
// In a real implementation, this would be more robust
const matches = xmlData.match(/<item>[\s\S]*?<\/item>/g) || [];
for (const itemXml of matches) {
const titleMatch = itemXml.match(/<title>(.*?)<\/title>/);
const linkMatch = itemXml.match(/<link>(.*?)<\/link>/);
const pubDateMatch = itemXml.match(/<pubDate>(.*?)<\/pubDate>/);
const descriptionMatch = itemXml.match(/<description>(.*?)<\/description>/);
const guid = crypto.createHash('md5').update(feedId + (linkMatch?.[1] || Math.random().toString())).digest('hex');
items.push({
id: guid,
feedId: feedId,
title: titleMatch?.[1] || 'Unknown',
link: linkMatch?.[1] || '',
pubDate: pubDateMatch?.[1] || new Date().toISOString(),
description: descriptionMatch?.[1] || '',
downloaded: false,
dateAdded: new Date().toISOString()
});
}
} catch (error) {
console.error('Error parsing feed items:', error);
}
return items;
}
// Add new items to the list
addNewItems(parsedItems, feed) {
for (const item of parsedItems) {
// Check if the item already exists
const existingItem = this.items.find(i => i.id === item.id);
if (!existingItem) {
this.items.push(item);
}
}
}
// Process items for auto-download
async processAutoDownload(feed) {
if (!feed.autoDownload || !feed.filters || feed.filters.length === 0) {
return;
}
const feedItems = this.items.filter(item =>
item.feedId === feed.id && !item.downloaded
);
for (const item of feedItems) {
if (this.matchesFilters(item, feed.filters)) {
console.log(`Auto-downloading item: ${item.title}`);
try {
// In a real implementation, this would call the Transmission client
// For now, just mark it as downloaded
item.downloaded = true;
item.downloadDate = new Date().toISOString();
} catch (error) {
console.error(`Error auto-downloading item ${item.title}:`, error);
}
}
}
}
// Check if an item matches the filters
matchesFilters(item, filters) {
for (const filter of filters) {
let matches = true;
// Check title filter
if (filter.title && !item.title.toLowerCase().includes(filter.title.toLowerCase())) {
matches = false;
}
// Check category filter
if (filter.category && !item.categories?.some(cat =>
cat.toLowerCase().includes(filter.category.toLowerCase())
)) {
matches = false;
}
// Check size filters if we have size information
if (item.size) {
if (filter.minSize && item.size < filter.minSize) {
matches = false;
}
if (filter.maxSize && item.size > filter.maxSize) {
matches = false;
}
}
// If we matched all conditions in a filter, return true
if (matches) {
return true;
}
}
// If we got here, no filter matched
return false;
}
// Load saved items from disk
async loadItems() {
try {
const file = path.join(this.dataDir, 'rss-items.json');
try {
await fs.access(file);
const data = await fs.readFile(file, 'utf8');
this.items = JSON.parse(data);
console.log(`Loaded ${this.items.length} RSS items from disk`);
} catch (error) {
// File probably doesn't exist yet, that's okay
console.log('No saved RSS items found, starting fresh');
this.items = [];
}
} catch (error) {
console.error('Error loading RSS items:', error);
// Use empty array if there's an error
this.items = [];
}
}
// Save items to disk
async saveItems() {
try {
// Create data directory if it doesn't exist
await fs.mkdir(this.dataDir, { recursive: true });
const file = path.join(this.dataDir, 'rss-items.json');
await fs.writeFile(file, JSON.stringify(this.items, null, 2), 'utf8');
console.log(`Saved ${this.items.length} RSS items to disk`);
} catch (error) {
console.error('Error saving RSS items:', error);
}
}
// Add a new feed
addFeed(feed) {
if (!feed.id) {
feed.id = crypto.createHash('md5').update(feed.url + Date.now()).digest('hex');
}
this.feeds.push(feed);
return feed;
}
// Remove a feed
removeFeed(feedId) {
const index = this.feeds.findIndex(feed => feed.id === feedId);
if (index === -1) {
return false;
}
this.feeds.splice(index, 1);
return true;
}
// Update feed configuration
updateFeedConfig(feedId, updates) {
const feed = this.feeds.find(feed => feed.id === feedId);
if (!feed) {
return false;
}
Object.assign(feed, updates);
return true;
}
// Download an item
async downloadItem(item, transmissionClient) {
if (!item || !item.link) {
throw new Error('Invalid item or missing link');
}
if (!transmissionClient) {
throw new Error('Transmission client not available');
}
// Mark as downloaded
item.downloaded = true;
item.downloadDate = new Date().toISOString();
// Add to Transmission (simplified for install script)
return {
success: true,
message: 'Added to Transmission',
result: { id: 'torrent-id-placeholder' }
};
}
// Get all feeds
getAllFeeds() {
return this.feeds;
}
// Get all items
getAllItems() {
return this.items;
}
// Get undownloaded items
getUndownloadedItems() {
return this.items.filter(item => !item.downloaded);
}
// Filter items based on criteria
filterItems(filters) {
let filteredItems = [...this.items];
if (filters.downloaded === true) {
filteredItems = filteredItems.filter(item => item.downloaded);
} else if (filters.downloaded === false) {
filteredItems = filteredItems.filter(item => !item.downloaded);
}
if (filters.title) {
filteredItems = filteredItems.filter(item =>
item.title.toLowerCase().includes(filters.title.toLowerCase())
);
}
if (filters.feedId) {
filteredItems = filteredItems.filter(item => item.feedId === filters.feedId);
}
return filteredItems;
}
}
module.exports = RssFeedManager;

73
modules/service-setup-module.sh Executable file
View File

@@ -0,0 +1,73 @@
#\!/bin/bash
# This module handles setting up the application as a system service
setup_systemd_service() {
local install_dir=$1
echo "Setting up Transmission RSS Manager as a systemd service..."
# Create service file
cat > /tmp/transmission-rss-manager.service << EOL
[Unit]
Description=Transmission RSS Manager Service
After=network.target
[Service]
Type=simple
User=$(whoami)
WorkingDirectory=${install_dir}
ExecStart=${install_dir}/TransmissionRssManager --urls=http://0.0.0.0:5000
Restart=on-failure
RestartSec=10
SyslogIdentifier=transmission-rss-manager
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.target
EOL
# Move service file to systemd directory
sudo mv /tmp/transmission-rss-manager.service /etc/systemd/system/
# Reload systemd
sudo systemctl daemon-reload
echo "Service has been set up"
echo "To start the service: sudo systemctl start transmission-rss-manager"
echo "To enable at boot: sudo systemctl enable transmission-rss-manager"
}
# Function to check if the service is running
check_service_status() {
if systemctl is-active --quiet transmission-rss-manager; then
echo "Service is running"
return 0
else
echo "Service is not running"
return 1
fi
}
# Function to start the service
start_service() {
echo "Starting Transmission RSS Manager service..."
sudo systemctl start transmission-rss-manager
if check_service_status; then
echo "Service started successfully"
else
echo "Failed to start service"
fi
}
# Function to stop the service
stop_service() {
echo "Stopping Transmission RSS Manager service..."
sudo systemctl stop transmission-rss-manager
if \! check_service_status; then
echo "Service stopped successfully"
else
echo "Failed to stop service"
fi
}

View File

@@ -0,0 +1,113 @@
// Transmission client module for Transmission RSS Manager
// This is a basic implementation that will be extended during installation
const Transmission = require('transmission');
class TransmissionClient {
constructor(config) {
this.config = config;
this.client = new Transmission({
host: config.host || 'localhost',
port: config.port || 9091,
username: config.username || '',
password: config.password || '',
url: config.path || '/transmission/rpc'
});
}
// Get all torrents
getTorrents() {
return new Promise((resolve, reject) => {
this.client.get((err, result) => {
if (err) {
reject(err);
} else {
resolve(result.torrents || []);
}
});
});
}
// Add a torrent
addTorrent(url) {
return new Promise((resolve, reject) => {
this.client.addUrl(url, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
}
// Remove a torrent
removeTorrent(id, deleteLocalData = false) {
return new Promise((resolve, reject) => {
this.client.remove(id, deleteLocalData, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
}
// Start a torrent
startTorrent(id) {
return new Promise((resolve, reject) => {
this.client.start(id, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
}
// Stop a torrent
stopTorrent(id) {
return new Promise((resolve, reject) => {
this.client.stop(id, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
}
// Get torrent details
getTorrentDetails(id) {
return new Promise((resolve, reject) => {
this.client.get(id, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result.torrents && result.torrents.length > 0 ? result.torrents[0] : null);
}
});
});
}
// Test connection to Transmission
testConnection() {
return new Promise((resolve, reject) => {
this.client.sessionStats((err, result) => {
if (err) {
reject(err);
} else {
resolve({
connected: true,
version: result.version,
rpcVersion: result.rpcVersion
});
}
});
});
}
}
module.exports = TransmissionClient;

91
modules/utils-module.sh Executable file
View File

@@ -0,0 +1,91 @@
#\!/bin/bash
# This module contains utility functions
# Text colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Function to check if a command exists
command_exists() {
command -v "$1" &> /dev/null
}
# Function to check if running as root
check_root() {
if [ "$(id -u)" -ne 0 ]; then
echo -e "${RED}This script must be run as root or with sudo${NC}"
exit 1
fi
}
# Function to get the system's IP address
get_ip_address() {
local ip=""
if command_exists ip; then
ip=$(ip -4 addr show scope global | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -n 1)
elif command_exists hostname; then
ip=$(hostname -I | awk '{print $1}')
elif command_exists ifconfig; then
ip=$(ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' | head -n 1)
fi
echo "$ip"
}
# Function to check if a service is installed
service_exists() {
local service_name=$1
if [ -f "/etc/systemd/system/$service_name.service" ]; then
return 0 # Service exists
else
return 1 # Service does not exist
fi
}
# Function to create directory if it doesn't exist
ensure_dir_exists() {
local dir_path=$1
if [ \! -d "$dir_path" ]; then
mkdir -p "$dir_path"
echo "Created directory: $dir_path"
fi
}
# Function to verify file permissions
verify_permissions() {
local file_path=$1
local user=$2
# If user is not specified, use current user
if [ -z "$user" ]; then
user=$(whoami)
fi
# Check if file exists
if [ \! -f "$file_path" ]; then
echo "File does not exist: $file_path"
return 1
fi
# Check ownership
local owner=$(stat -c '%U' "$file_path")
if [ "$owner" \!= "$user" ]; then
echo "Changing ownership of $file_path to $user"
sudo chown "$user" "$file_path"
fi
# Ensure file is executable if it's a script
if [[ "$file_path" == *.sh ]]; then
if [ \! -x "$file_path" ]; then
echo "Making $file_path executable"
chmod +x "$file_path"
fi
fi
return 0
}