split install up into smaller files due to size of file
This commit is contained in:
parent
94eb96008e
commit
d1483ce581
2956
install-script.sh
2956
install-script.sh
File diff suppressed because it is too large
Load Diff
62
main-installer.sh
Executable file
62
main-installer.sh
Executable file
@ -0,0 +1,62 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Transmission RSS Manager Modular Installer
|
||||||
|
# Main installer script that coordinates the installation process
|
||||||
|
|
||||||
|
# Text formatting
|
||||||
|
BOLD='\033[1m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Print header
|
||||||
|
echo -e "${BOLD}==================================================${NC}"
|
||||||
|
echo -e "${BOLD} Transmission RSS Manager Installer ${NC}"
|
||||||
|
echo -e "${BOLD} Version 1.2.0 - Enhanced Edition ${NC}"
|
||||||
|
echo -e "${BOLD}==================================================${NC}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Check if script is run with sudo
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo -e "${RED}Please run as root (use sudo)${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get current directory
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
|
|
||||||
|
# Source the module files
|
||||||
|
source "${SCRIPT_DIR}/modules/config.sh"
|
||||||
|
source "${SCRIPT_DIR}/modules/utils.sh"
|
||||||
|
source "${SCRIPT_DIR}/modules/dependencies.sh"
|
||||||
|
source "${SCRIPT_DIR}/modules/file_creator.sh"
|
||||||
|
source "${SCRIPT_DIR}/modules/service_setup.sh"
|
||||||
|
|
||||||
|
# Execute the installation steps in sequence
|
||||||
|
echo -e "${YELLOW}Starting installation process...${NC}"
|
||||||
|
|
||||||
|
# Step 1: Gather configuration from user
|
||||||
|
gather_configuration
|
||||||
|
|
||||||
|
# Step 2: Install dependencies
|
||||||
|
install_dependencies
|
||||||
|
|
||||||
|
# Step 3: Create installation directories
|
||||||
|
create_directories
|
||||||
|
|
||||||
|
# Step 4: Create configuration files and scripts
|
||||||
|
create_config_files
|
||||||
|
|
||||||
|
# Step 5: Create service files and install the service
|
||||||
|
setup_service
|
||||||
|
|
||||||
|
# Step 6: Final setup and permissions
|
||||||
|
finalize_setup
|
||||||
|
|
||||||
|
echo -e "${GREEN}Installation completed successfully!${NC}"
|
||||||
|
echo -e "You can access the RSS Manager at ${BOLD}http://localhost:${PORT}${NC} or ${BOLD}http://your-server-ip:${PORT}${NC}"
|
||||||
|
echo
|
||||||
|
echo -e "The service is ${BOLD}automatically started${NC} and will ${BOLD}start on boot${NC}."
|
||||||
|
echo -e "To manually control the service, use: ${BOLD}sudo systemctl [start|stop|restart] ${SERVICE_NAME}${NC}"
|
||||||
|
echo
|
||||||
|
echo -e "${BOLD}Thank you for installing Transmission RSS Manager Enhanced Edition!${NC}"
|
136
modules/config-module.sh
Normal file
136
modules/config-module.sh
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Configuration module for Transmission RSS Manager Installation
|
||||||
|
|
||||||
|
# Configuration variables with defaults
|
||||||
|
INSTALL_DIR="/opt/transmission-rss-manager"
|
||||||
|
SERVICE_NAME="transmission-rss-manager"
|
||||||
|
USER=$(logname || echo $SUDO_USER)
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# Transmission configuration variables
|
||||||
|
TRANSMISSION_REMOTE=false
|
||||||
|
TRANSMISSION_HOST="localhost"
|
||||||
|
TRANSMISSION_PORT=9091
|
||||||
|
TRANSMISSION_USER=""
|
||||||
|
TRANSMISSION_PASS=""
|
||||||
|
TRANSMISSION_RPC_PATH="/transmission/rpc"
|
||||||
|
TRANSMISSION_DOWNLOAD_DIR="/var/lib/transmission-daemon/downloads"
|
||||||
|
TRANSMISSION_DIR_MAPPING="{}"
|
||||||
|
|
||||||
|
# Media path defaults
|
||||||
|
MEDIA_DIR="/mnt/media"
|
||||||
|
ENABLE_BOOK_SORTING=true
|
||||||
|
|
||||||
|
function gather_configuration() {
|
||||||
|
echo -e "${BOLD}Installation Configuration:${NC}"
|
||||||
|
echo -e "Please provide the following configuration parameters:"
|
||||||
|
echo
|
||||||
|
|
||||||
|
read -p "Installation directory [$INSTALL_DIR]: " input_install_dir
|
||||||
|
INSTALL_DIR=${input_install_dir:-$INSTALL_DIR}
|
||||||
|
|
||||||
|
read -p "Web interface port [$PORT]: " input_port
|
||||||
|
PORT=${input_port:-$PORT}
|
||||||
|
|
||||||
|
read -p "Run as user [$USER]: " input_user
|
||||||
|
USER=${input_user:-$USER}
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo -e "${BOLD}Transmission Configuration:${NC}"
|
||||||
|
echo -e "Configure connection to your Transmission client:"
|
||||||
|
echo
|
||||||
|
|
||||||
|
read -p "Is Transmission running on a remote server? (y/n) [n]: " input_remote
|
||||||
|
if [[ $input_remote =~ ^[Yy]$ ]]; then
|
||||||
|
TRANSMISSION_REMOTE=true
|
||||||
|
|
||||||
|
read -p "Remote Transmission host [localhost]: " input_trans_host
|
||||||
|
TRANSMISSION_HOST=${input_trans_host:-$TRANSMISSION_HOST}
|
||||||
|
|
||||||
|
read -p "Remote Transmission port [9091]: " input_trans_port
|
||||||
|
TRANSMISSION_PORT=${input_trans_port:-$TRANSMISSION_PORT}
|
||||||
|
|
||||||
|
read -p "Remote Transmission username []: " input_trans_user
|
||||||
|
TRANSMISSION_USER=${input_trans_user:-$TRANSMISSION_USER}
|
||||||
|
|
||||||
|
read -p "Remote Transmission password []: " input_trans_pass
|
||||||
|
TRANSMISSION_PASS=${input_trans_pass:-$TRANSMISSION_PASS}
|
||||||
|
|
||||||
|
read -p "Remote Transmission RPC path [/transmission/rpc]: " input_trans_path
|
||||||
|
TRANSMISSION_RPC_PATH=${input_trans_path:-$TRANSMISSION_RPC_PATH}
|
||||||
|
|
||||||
|
# Configure directory mapping for remote setup
|
||||||
|
echo
|
||||||
|
echo -e "${YELLOW}Directory Mapping Configuration${NC}"
|
||||||
|
echo -e "When using a remote Transmission server, you need to map paths between servers."
|
||||||
|
echo -e "For each directory on the remote server, specify the corresponding local directory."
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Get remote download directory
|
||||||
|
read -p "Remote Transmission download directory: " REMOTE_DOWNLOAD_DIR
|
||||||
|
REMOTE_DOWNLOAD_DIR=${REMOTE_DOWNLOAD_DIR:-"/var/lib/transmission-daemon/downloads"}
|
||||||
|
|
||||||
|
# Get local directory that corresponds to remote download directory
|
||||||
|
read -p "Local directory that corresponds to the remote download directory: " LOCAL_DOWNLOAD_DIR
|
||||||
|
LOCAL_DOWNLOAD_DIR=${LOCAL_DOWNLOAD_DIR:-"/mnt/transmission-downloads"}
|
||||||
|
|
||||||
|
# Create mapping JSON
|
||||||
|
TRANSMISSION_DIR_MAPPING=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"$REMOTE_DOWNLOAD_DIR": "$LOCAL_DOWNLOAD_DIR"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create the local directory
|
||||||
|
mkdir -p "$LOCAL_DOWNLOAD_DIR"
|
||||||
|
chown -R $USER:$USER "$LOCAL_DOWNLOAD_DIR"
|
||||||
|
|
||||||
|
# Ask if want to add more mappings
|
||||||
|
while true; do
|
||||||
|
read -p "Add another directory mapping? (y/n) [n]: " add_another
|
||||||
|
if [[ ! $add_another =~ ^[Yy]$ ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -p "Remote directory path: " remote_dir
|
||||||
|
read -p "Corresponding local directory path: " local_dir
|
||||||
|
|
||||||
|
if [ -n "$remote_dir" ] && [ -n "$local_dir" ]; then
|
||||||
|
# Update mapping JSON (remove the last "}" and add the new mapping)
|
||||||
|
TRANSMISSION_DIR_MAPPING="${TRANSMISSION_DIR_MAPPING%\}}, \"$remote_dir\": \"$local_dir\" }"
|
||||||
|
|
||||||
|
# Create the local directory
|
||||||
|
mkdir -p "$local_dir"
|
||||||
|
chown -R $USER:$USER "$local_dir"
|
||||||
|
|
||||||
|
echo -e "${GREEN}Mapping added: $remote_dir → $local_dir${NC}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Set Transmission download dir for configuration
|
||||||
|
TRANSMISSION_DOWNLOAD_DIR=$REMOTE_DOWNLOAD_DIR
|
||||||
|
else
|
||||||
|
read -p "Transmission download directory [/var/lib/transmission-daemon/downloads]: " input_trans_dir
|
||||||
|
TRANSMISSION_DOWNLOAD_DIR=${input_trans_dir:-$TRANSMISSION_DOWNLOAD_DIR}
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo -e "${BOLD}Media Destination Configuration:${NC}"
|
||||||
|
|
||||||
|
read -p "Media destination base directory [/mnt/media]: " input_media_dir
|
||||||
|
MEDIA_DIR=${input_media_dir:-$MEDIA_DIR}
|
||||||
|
|
||||||
|
# Ask about enabling book/magazine sorting
|
||||||
|
echo
|
||||||
|
echo -e "${BOLD}Content Type Configuration:${NC}"
|
||||||
|
read -p "Enable book and magazine sorting? (y/n) [y]: " input_book_sorting
|
||||||
|
ENABLE_BOOK_SORTING=true
|
||||||
|
if [[ $input_book_sorting =~ ^[Nn]$ ]]; then
|
||||||
|
ENABLE_BOOK_SORTING=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo -e "${GREEN}Configuration complete!${NC}"
|
||||||
|
echo
|
||||||
|
}
|
60
modules/dependencies-module.sh
Normal file
60
modules/dependencies-module.sh
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Dependencies module for Transmission RSS Manager Installation
|
||||||
|
|
||||||
|
function install_dependencies() {
|
||||||
|
echo -e "${YELLOW}Installing dependencies...${NC}"
|
||||||
|
|
||||||
|
# Update package index
|
||||||
|
apt-get update
|
||||||
|
|
||||||
|
# Install Node.js and npm if not already installed
|
||||||
|
if ! command_exists node; then
|
||||||
|
echo "Installing Node.js and npm..."
|
||||||
|
apt-get install -y ca-certificates curl gnupg
|
||||||
|
mkdir -p /etc/apt/keyrings
|
||||||
|
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||||
|
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main" > /etc/apt/sources.list.d/nodesource.list
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y nodejs
|
||||||
|
else
|
||||||
|
echo "Node.js is already installed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install additional dependencies
|
||||||
|
echo "Installing additional dependencies..."
|
||||||
|
apt-get install -y unrar unzip p7zip-full nginx
|
||||||
|
|
||||||
|
# Check if all dependencies were installed successfully
|
||||||
|
local dependencies=("node" "npm" "unrar" "unzip" "7z" "nginx")
|
||||||
|
local missing_deps=()
|
||||||
|
|
||||||
|
for dep in "${dependencies[@]}"; do
|
||||||
|
if ! command_exists "$dep"; then
|
||||||
|
missing_deps+=("$dep")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#missing_deps[@]} -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}All dependencies installed successfully.${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Failed to install some dependencies: ${missing_deps[*]}${NC}"
|
||||||
|
echo -e "${YELLOW}Please install them manually and rerun this script.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_directories() {
|
||||||
|
echo -e "${YELLOW}Creating installation directories...${NC}"
|
||||||
|
|
||||||
|
# Create main installation directory
|
||||||
|
mkdir -p $INSTALL_DIR
|
||||||
|
mkdir -p $INSTALL_DIR/logs
|
||||||
|
mkdir -p $INSTALL_DIR/public/js
|
||||||
|
mkdir -p $INSTALL_DIR/public/css
|
||||||
|
mkdir -p $INSTALL_DIR/modules
|
||||||
|
|
||||||
|
# Create directory for file storage
|
||||||
|
mkdir -p $INSTALL_DIR/data
|
||||||
|
|
||||||
|
echo -e "${GREEN}Directories created successfully.${NC}"
|
||||||
|
}
|
1699
modules/file-creator-module.sh
Normal file
1699
modules/file-creator-module.sh
Normal file
File diff suppressed because it is too large
Load Diff
456
modules/rss-feed-manager.js
Normal file
456
modules/rss-feed-manager.js
Normal file
@ -0,0 +1,456 @@
|
|||||||
|
// rssFeedManager.js
|
||||||
|
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.items = [];
|
||||||
|
this.updateIntervalId = null;
|
||||||
|
this.updateIntervalMinutes = config.updateIntervalMinutes || 60;
|
||||||
|
this.parser = new xml2js.Parser({ explicitArray: false });
|
||||||
|
this.dataPath = path.join(__dirname, '..', 'data');
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
if (this.updateIntervalId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run update immediately
|
||||||
|
await this.updateAllFeeds();
|
||||||
|
|
||||||
|
// Then set up interval
|
||||||
|
this.updateIntervalId = setInterval(async () => {
|
||||||
|
await this.updateAllFeeds();
|
||||||
|
}, this.updateIntervalMinutes * 60 * 1000);
|
||||||
|
|
||||||
|
console.log(`RSS feed manager started, interval: ${this.updateIntervalMinutes} minutes`);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.updateIntervalId) {
|
||||||
|
clearInterval(this.updateIntervalId);
|
||||||
|
this.updateIntervalId = null;
|
||||||
|
console.log('RSS feed manager stopped');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAllFeeds() {
|
||||||
|
console.log('Updating all RSS feeds...');
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const feed of this.feeds) {
|
||||||
|
try {
|
||||||
|
const result = await this.updateFeed(feed);
|
||||||
|
results.push({
|
||||||
|
feedId: feed.id,
|
||||||
|
success: true,
|
||||||
|
newItems: result.newItems
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error updating feed ${feed.id} (${feed.url}):`, error.message);
|
||||||
|
results.push({
|
||||||
|
feedId: feed.id,
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save updated items
|
||||||
|
await this.saveItems();
|
||||||
|
|
||||||
|
console.log('RSS feed update completed');
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateFeed(feed) {
|
||||||
|
console.log(`Updating feed: ${feed.name} (${feed.url})`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(feed.url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const xml = await response.text();
|
||||||
|
const result = await this.parseXml(xml);
|
||||||
|
|
||||||
|
const rssItems = this.extractItems(result, feed);
|
||||||
|
const newItems = this.processNewItems(rssItems, feed);
|
||||||
|
|
||||||
|
console.log(`Found ${rssItems.length} items, ${newItems.length} new items in feed: ${feed.name}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalItems: rssItems.length,
|
||||||
|
newItems: newItems.length
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error updating feed ${feed.url}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseXml(xml) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.parser.parseString(xml, (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
extractItems(parsedXml, feed) {
|
||||||
|
try {
|
||||||
|
// Handle standard RSS 2.0
|
||||||
|
if (parsedXml.rss && parsedXml.rss.channel) {
|
||||||
|
const channel = parsedXml.rss.channel;
|
||||||
|
const items = Array.isArray(channel.item) ? channel.item : [channel.item].filter(Boolean);
|
||||||
|
return items.map(item => this.normalizeRssItem(item, feed));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Atom
|
||||||
|
if (parsedXml.feed && parsedXml.feed.entry) {
|
||||||
|
const entries = Array.isArray(parsedXml.feed.entry) ? parsedXml.feed.entry : [parsedXml.feed.entry].filter(Boolean);
|
||||||
|
return entries.map(entry => this.normalizeAtomItem(entry, feed));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error extracting items from XML:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeRssItem(item, feed) {
|
||||||
|
// Create a unique ID for the item
|
||||||
|
const idContent = `${feed.id}:${item.title}:${item.pubDate || ''}:${item.link || ''}`;
|
||||||
|
const id = crypto.createHash('md5').update(idContent).digest('hex');
|
||||||
|
|
||||||
|
// Extract enclosure (torrent link)
|
||||||
|
let torrentLink = item.link || '';
|
||||||
|
let fileSize = 0;
|
||||||
|
|
||||||
|
if (item.enclosure) {
|
||||||
|
torrentLink = item.enclosure.$ ? item.enclosure.$.url : item.enclosure.url || torrentLink;
|
||||||
|
fileSize = item.enclosure.$ ? parseInt(item.enclosure.$.length || 0, 10) : parseInt(item.enclosure.length || 0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle custom namespaces (common in torrent feeds)
|
||||||
|
let category = '';
|
||||||
|
let size = fileSize;
|
||||||
|
|
||||||
|
if (item.category) {
|
||||||
|
category = Array.isArray(item.category) ? item.category[0] : item.category;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some feeds use torrent:contentLength
|
||||||
|
if (item['torrent:contentLength']) {
|
||||||
|
size = parseInt(item['torrent:contentLength'], 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
feedId: feed.id,
|
||||||
|
title: item.title || 'Untitled',
|
||||||
|
link: item.link || '',
|
||||||
|
torrentLink: torrentLink,
|
||||||
|
pubDate: item.pubDate || new Date().toISOString(),
|
||||||
|
category: category,
|
||||||
|
description: item.description || '',
|
||||||
|
size: size || 0,
|
||||||
|
downloaded: false,
|
||||||
|
ignored: false,
|
||||||
|
added: new Date().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeAtomItem(entry, feed) {
|
||||||
|
// Create a unique ID for the item
|
||||||
|
const idContent = `${feed.id}:${entry.title}:${entry.updated || ''}:${entry.id || ''}`;
|
||||||
|
const id = crypto.createHash('md5').update(idContent).digest('hex');
|
||||||
|
|
||||||
|
// Extract link
|
||||||
|
let link = '';
|
||||||
|
let torrentLink = '';
|
||||||
|
|
||||||
|
if (entry.link) {
|
||||||
|
if (Array.isArray(entry.link)) {
|
||||||
|
const links = entry.link;
|
||||||
|
link = links.find(l => l.$.rel === 'alternate')?.$.href || links[0]?.$.href || '';
|
||||||
|
torrentLink = links.find(l => l.$.type && l.$.type.includes('torrent'))?.$.href || link;
|
||||||
|
} else {
|
||||||
|
link = entry.link.$.href || '';
|
||||||
|
torrentLink = link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
feedId: feed.id,
|
||||||
|
title: entry.title || 'Untitled',
|
||||||
|
link: link,
|
||||||
|
torrentLink: torrentLink,
|
||||||
|
pubDate: entry.updated || entry.published || new Date().toISOString(),
|
||||||
|
category: entry.category?.$.term || '',
|
||||||
|
description: entry.summary || entry.content || '',
|
||||||
|
size: 0, // Atom doesn't typically include file size
|
||||||
|
downloaded: false,
|
||||||
|
ignored: false,
|
||||||
|
added: new Date().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
processNewItems(rssItems, feed) {
|
||||||
|
const newItems = [];
|
||||||
|
|
||||||
|
for (const item of rssItems) {
|
||||||
|
// Check if item already exists in our list
|
||||||
|
const existingItem = this.items.find(i => i.id === item.id);
|
||||||
|
|
||||||
|
if (!existingItem) {
|
||||||
|
// Add new item to our list
|
||||||
|
this.items.push(item);
|
||||||
|
newItems.push(item);
|
||||||
|
|
||||||
|
// Auto-download if enabled and matches filters
|
||||||
|
if (feed.autoDownload && this.matchesFilters(item, feed.filters)) {
|
||||||
|
this.queueItemForDownload(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
matchesFilters(item, filters) {
|
||||||
|
if (!filters || filters.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the item matches any of the filters
|
||||||
|
return filters.some(filter => {
|
||||||
|
// Title check
|
||||||
|
if (filter.title && !item.title.toLowerCase().includes(filter.title.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category check
|
||||||
|
if (filter.category && !item.category.toLowerCase().includes(filter.category.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size check
|
||||||
|
if (filter.minSize && item.size < filter.minSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.maxSize && item.size > filter.maxSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All checks passed
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
queueItemForDownload(item) {
|
||||||
|
// Mark the item as queued for download
|
||||||
|
console.log(`Auto-downloading item: ${item.title}`);
|
||||||
|
|
||||||
|
// This would be implemented to add to Transmission
|
||||||
|
// But we need a reference to the Transmission client
|
||||||
|
// In a real implementation, this might publish to a queue that's consumed elsewhere
|
||||||
|
item.downloadQueued = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveItems() {
|
||||||
|
try {
|
||||||
|
// Create data directory if it doesn't exist
|
||||||
|
await fs.mkdir(this.dataPath, { recursive: true });
|
||||||
|
|
||||||
|
// Save items to file
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(this.dataPath, 'rss-items.json'),
|
||||||
|
JSON.stringify(this.items, null, 2),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Saved ${this.items.length} RSS items to disk`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving RSS items:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveConfig() {
|
||||||
|
try {
|
||||||
|
// Create data directory if it doesn't exist
|
||||||
|
await fs.mkdir(this.dataPath, { recursive: true });
|
||||||
|
|
||||||
|
// Save feeds to file
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(this.dataPath, 'rss-feeds.json'),
|
||||||
|
JSON.stringify(this.feeds, null, 2),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Saved ${this.feeds.length} RSS feeds to disk`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving RSS feeds:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadItems() {
|
||||||
|
try {
|
||||||
|
const filePath = path.join(this.dataPath, 'rss-items.json');
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
try {
|
||||||
|
await fs.access(filePath);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('No saved RSS items found');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load items from file
|
||||||
|
const data = await fs.readFile(filePath, 'utf8');
|
||||||
|
this.items = JSON.parse(data);
|
||||||
|
|
||||||
|
console.log(`Loaded ${this.items.length} RSS items from disk`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading RSS items:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API methods
|
||||||
|
|
||||||
|
getAllFeeds() {
|
||||||
|
return this.feeds;
|
||||||
|
}
|
||||||
|
|
||||||
|
addFeed(feedData) {
|
||||||
|
// Generate an ID for the feed
|
||||||
|
const id = crypto.randomBytes(8).toString('hex');
|
||||||
|
|
||||||
|
const newFeed = {
|
||||||
|
id,
|
||||||
|
name: feedData.name,
|
||||||
|
url: feedData.url,
|
||||||
|
autoDownload: feedData.autoDownload || false,
|
||||||
|
filters: feedData.filters || [],
|
||||||
|
added: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
this.feeds.push(newFeed);
|
||||||
|
|
||||||
|
console.log(`Added new feed: ${newFeed.name} (${newFeed.url})`);
|
||||||
|
|
||||||
|
return newFeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFeedConfig(feedId, updates) {
|
||||||
|
const feedIndex = this.feeds.findIndex(f => f.id === feedId);
|
||||||
|
|
||||||
|
if (feedIndex === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the feed, preserving the id and added date
|
||||||
|
this.feeds[feedIndex] = {
|
||||||
|
...this.feeds[feedIndex],
|
||||||
|
...updates,
|
||||||
|
id: feedId,
|
||||||
|
added: this.feeds[feedIndex].added
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`Updated feed: ${this.feeds[feedIndex].name}`);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFeed(feedId) {
|
||||||
|
const initialLength = this.feeds.length;
|
||||||
|
this.feeds = this.feeds.filter(f => f.id !== feedId);
|
||||||
|
|
||||||
|
return this.feeds.length !== initialLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllItems() {
|
||||||
|
return this.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUndownloadedItems() {
|
||||||
|
return this.items.filter(item => !item.downloaded && !item.ignored);
|
||||||
|
}
|
||||||
|
|
||||||
|
filterItems(filters) {
|
||||||
|
return this.items.filter(item => this.matchesFilters(item, [filters]));
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadItem(item, transmissionClient) {
|
||||||
|
if (!item || !item.torrentLink) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid item or missing torrent link'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!transmissionClient) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Transmission client not available'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
transmissionClient.addUrl(item.torrentLink, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`Error adding torrent for ${item.title}:`, err);
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
message: `Error adding torrent: ${err.message}`,
|
||||||
|
result: null
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the item as downloaded
|
||||||
|
item.downloaded = true;
|
||||||
|
item.downloadDate = new Date().toISOString();
|
||||||
|
|
||||||
|
// Save the updated items
|
||||||
|
this.saveItems().catch(err => {
|
||||||
|
console.error('Error saving items after download:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Successfully added torrent for item: ${item.title}`);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
message: 'Torrent added successfully',
|
||||||
|
result
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RssFeedManager;
|
83
modules/service-setup-module.sh
Normal file
83
modules/service-setup-module.sh
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Service setup module for Transmission RSS Manager Installation
|
||||||
|
|
||||||
|
# Setup systemd service
|
||||||
|
function setup_service() {
|
||||||
|
echo -e "${YELLOW}Setting up systemd service...${NC}"
|
||||||
|
|
||||||
|
# Create systemd service file
|
||||||
|
cat > /etc/systemd/system/$SERVICE_NAME.service << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Transmission RSS Manager
|
||||||
|
After=network.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=$USER
|
||||||
|
WorkingDirectory=$INSTALL_DIR
|
||||||
|
ExecStart=/usr/bin/node $INSTALL_DIR/server.js
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
Environment=PORT=$PORT
|
||||||
|
Environment=NODE_ENV=production
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create nginx configuration for proxy
|
||||||
|
echo -e "${YELLOW}Setting up Nginx reverse proxy...${NC}"
|
||||||
|
|
||||||
|
# Check if default nginx file exists, back it up if it does
|
||||||
|
if [ -f /etc/nginx/sites-enabled/default ]; then
|
||||||
|
mv /etc/nginx/sites-enabled/default /etc/nginx/sites-enabled/default.bak
|
||||||
|
echo "Backed up default nginx configuration."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create nginx configuration
|
||||||
|
cat > /etc/nginx/sites-available/$SERVICE_NAME << EOF
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:$PORT;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade \$http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_cache_bypass \$http_upgrade;
|
||||||
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create symbolic link to enable the site
|
||||||
|
ln -sf /etc/nginx/sites-available/$SERVICE_NAME /etc/nginx/sites-enabled/
|
||||||
|
|
||||||
|
# Test nginx configuration
|
||||||
|
nginx -t
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
# Reload nginx
|
||||||
|
systemctl reload nginx
|
||||||
|
echo -e "${GREEN}Nginx configuration has been set up successfully.${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Nginx configuration test failed. Please check the configuration manually.${NC}"
|
||||||
|
echo -e "${YELLOW}You may need to correct the configuration before the web interface will be accessible.${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reload systemd
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Enable the service to start on boot
|
||||||
|
systemctl enable $SERVICE_NAME
|
||||||
|
|
||||||
|
echo -e "${GREEN}Systemd service has been created and enabled.${NC}"
|
||||||
|
echo -e "${YELLOW}The service will start automatically after installation.${NC}"
|
||||||
|
}
|
144
modules/utils-module.sh
Normal file
144
modules/utils-module.sh
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Utilities module for Transmission RSS Manager Installation
|
||||||
|
|
||||||
|
# Function to log a message with timestamp
|
||||||
|
function log() {
|
||||||
|
local level=$1
|
||||||
|
local message=$2
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
case $level in
|
||||||
|
"INFO")
|
||||||
|
echo -e "${timestamp} ${GREEN}[INFO]${NC} $message"
|
||||||
|
;;
|
||||||
|
"WARN")
|
||||||
|
echo -e "${timestamp} ${YELLOW}[WARN]${NC} $message"
|
||||||
|
;;
|
||||||
|
"ERROR")
|
||||||
|
echo -e "${timestamp} ${RED}[ERROR]${NC} $message"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${timestamp} [LOG] $message"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check if a command exists
|
||||||
|
function command_exists() {
|
||||||
|
command -v "$1" &> /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to backup a file before modifying it
|
||||||
|
function backup_file() {
|
||||||
|
local file=$1
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
local backup="${file}.bak.$(date +%Y%m%d%H%M%S)"
|
||||||
|
cp "$file" "$backup"
|
||||||
|
log "INFO" "Created backup of $file at $backup"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to create a directory if it doesn't exist
|
||||||
|
function create_dir_if_not_exists() {
|
||||||
|
local dir=$1
|
||||||
|
local owner=$2
|
||||||
|
|
||||||
|
if [ ! -d "$dir" ]; then
|
||||||
|
mkdir -p "$dir"
|
||||||
|
log "INFO" "Created directory: $dir"
|
||||||
|
|
||||||
|
if [ -n "$owner" ]; then
|
||||||
|
chown -R "$owner" "$dir"
|
||||||
|
log "INFO" "Set ownership of $dir to $owner"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to finalize the setup (permissions, etc.)
|
||||||
|
function finalize_setup() {
|
||||||
|
log "INFO" "Setting up final permissions and configurations..."
|
||||||
|
|
||||||
|
# Set proper ownership for the installation directory
|
||||||
|
chown -R $USER:$USER $INSTALL_DIR
|
||||||
|
|
||||||
|
# Create media directories with correct permissions
|
||||||
|
create_dir_if_not_exists "$MEDIA_DIR/movies" "$USER:$USER"
|
||||||
|
create_dir_if_not_exists "$MEDIA_DIR/tvshows" "$USER:$USER"
|
||||||
|
create_dir_if_not_exists "$MEDIA_DIR/music" "$USER:$USER"
|
||||||
|
create_dir_if_not_exists "$MEDIA_DIR/software" "$USER:$USER"
|
||||||
|
|
||||||
|
# Create book/magazine directories if enabled
|
||||||
|
if [ "$ENABLE_BOOK_SORTING" = true ]; then
|
||||||
|
create_dir_if_not_exists "$MEDIA_DIR/books" "$USER:$USER"
|
||||||
|
create_dir_if_not_exists "$MEDIA_DIR/magazines" "$USER:$USER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install NPM packages
|
||||||
|
log "INFO" "Installing NPM packages..."
|
||||||
|
cd $INSTALL_DIR && npm install
|
||||||
|
|
||||||
|
# Start the service
|
||||||
|
log "INFO" "Starting the service..."
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable $SERVICE_NAME
|
||||||
|
systemctl start $SERVICE_NAME
|
||||||
|
|
||||||
|
# Check if service started successfully
|
||||||
|
sleep 2
|
||||||
|
if systemctl is-active --quiet $SERVICE_NAME; then
|
||||||
|
log "INFO" "Service started successfully!"
|
||||||
|
else
|
||||||
|
log "ERROR" "Service failed to start. Check logs with: journalctl -u $SERVICE_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create default configuration if it doesn't exist
|
||||||
|
if [ ! -f "$INSTALL_DIR/config.json" ]; then
|
||||||
|
log "INFO" "Creating default configuration file..."
|
||||||
|
cat > $INSTALL_DIR/config.json << EOF
|
||||||
|
{
|
||||||
|
"transmissionConfig": {
|
||||||
|
"host": "${TRANSMISSION_HOST}",
|
||||||
|
"port": ${TRANSMISSION_PORT},
|
||||||
|
"username": "${TRANSMISSION_USER}",
|
||||||
|
"password": "${TRANSMISSION_PASS}",
|
||||||
|
"path": "${TRANSMISSION_RPC_PATH}"
|
||||||
|
},
|
||||||
|
"remoteConfig": {
|
||||||
|
"isRemote": ${TRANSMISSION_REMOTE},
|
||||||
|
"directoryMapping": ${TRANSMISSION_DIR_MAPPING}
|
||||||
|
},
|
||||||
|
"destinationPaths": {
|
||||||
|
"movies": "${MEDIA_DIR}/movies",
|
||||||
|
"tvShows": "${MEDIA_DIR}/tvshows",
|
||||||
|
"music": "${MEDIA_DIR}/music",
|
||||||
|
"books": "${MEDIA_DIR}/books",
|
||||||
|
"magazines": "${MEDIA_DIR}/magazines",
|
||||||
|
"software": "${MEDIA_DIR}/software"
|
||||||
|
},
|
||||||
|
"seedingRequirements": {
|
||||||
|
"minRatio": 1.0,
|
||||||
|
"minTimeMinutes": 60,
|
||||||
|
"checkIntervalSeconds": 300
|
||||||
|
},
|
||||||
|
"processingOptions": {
|
||||||
|
"enableBookSorting": ${ENABLE_BOOK_SORTING},
|
||||||
|
"extractArchives": true,
|
||||||
|
"deleteArchives": true,
|
||||||
|
"createCategoryFolders": true,
|
||||||
|
"ignoreSample": true,
|
||||||
|
"ignoreExtras": true,
|
||||||
|
"renameFiles": true,
|
||||||
|
"autoReplaceUpgrades": true,
|
||||||
|
"removeDuplicates": true,
|
||||||
|
"keepOnlyBestVersion": true
|
||||||
|
},
|
||||||
|
"rssFeeds": [],
|
||||||
|
"rssUpdateIntervalMinutes": 60,
|
||||||
|
"autoProcessing": false
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
chown $USER:$USER $INSTALL_DIR/config.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "INFO" "Setup finalized!"
|
||||||
|
}
|
1386
public/index.html
Normal file
1386
public/index.html
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user