maybe working release

This commit is contained in:
root 2025-02-26 13:30:39 +01:00
parent 756c6fdd6f
commit 94eb96008e
5 changed files with 2842 additions and 2218 deletions

283
README.md
View File

@ -0,0 +1,283 @@
# Transmission RSS Manager
A comprehensive web-based tool to automate and manage your Transmission torrent downloads with RSS feed integration and intelligent media organization.
## Features
- 🔄 **RSS Feed Integration**: Automatically download torrents from RSS feeds with customizable filters
- 📊 **Torrent Management**: Monitor and control your Transmission torrents from a clean web interface
- 📚 **Intelligent Media Organization**: Automatically categorize and organize downloads by media type
- 📖 **Book & Magazine Sorting**: Specialized processing for e-books and magazines with metadata extraction
- 📂 **Post-Processing**: Extract archives, rename files, and move content to appropriate directories
- 🔄 **Remote Support**: Connect to remote Transmission instances with local path mapping
- 📱 **Mobile-Friendly UI**: Responsive design works on desktop and mobile devices
## Installation
### Prerequisites
- Ubuntu/Debian-based system (may work on other Linux distributions)
- Node.js 14+ and npm
- Transmission daemon installed and running
- Nginx (for reverse proxy)
### Automatic Installation
The easiest way to install Transmission RSS Manager is with the installation script:
```bash
# Download the installation script
wget https://raw.githubusercontent.com/username/transmission-rss-manager/main/install.sh
# Make it executable
chmod +x install.sh
# Run it with sudo
sudo ./install.sh
```
The script will guide you through the configuration process and set up everything you need.
### Manual Installation
If you prefer to install manually:
1. Clone the repository:
```bash
git clone https://github.com/username/transmission-rss-manager.git
cd transmission-rss-manager
```
2. Install dependencies:
```bash
npm install
```
3. Configure settings:
```bash
cp config.example.json config.json
nano config.json
```
4. Start the server:
```bash
node server.js
```
## Configuration
### Main Configuration Options
The system can be configured through the web interface or by editing the `config.json` file:
```json
{
"transmissionConfig": {
"host": "localhost",
"port": 9091,
"username": "transmission",
"password": "password",
"path": "/transmission/rpc"
},
"destinationPaths": {
"movies": "/mnt/media/movies",
"tvShows": "/mnt/media/tvshows",
"music": "/mnt/media/music",
"books": "/mnt/media/books",
"magazines": "/mnt/media/magazines",
"software": "/mnt/media/software"
},
"processingOptions": {
"enableBookSorting": true,
"extractArchives": true,
"renameFiles": true,
"ignoreSample": true
}
}
```
### Remote Transmission Setup
For remote Transmission instances, configure the directory mapping:
```json
"remoteConfig": {
"isRemote": true,
"directoryMapping": {
"/var/lib/transmission-daemon/downloads": "/mnt/transmission-downloads"
}
}
```
This maps paths between your remote Transmission server and the local directories.
## Usage
### Web Interface
The web interface provides access to all functionality and is available at:
```
http://your-server-ip
```
### RSS Feed Management
1. Go to the "RSS Feeds" tab in the web interface
2. Click "Add Feed" and enter the RSS feed URL
3. Configure optional filters for automatic downloads
4. The system will periodically check feeds and download matching items
### Managing Torrents
From the "Torrents" tab, you can:
- Add new torrents via URL or magnet link
- Start, stop, or delete existing torrents
- Monitor download progress and stats
### Media Organization
The post-processor automatically:
1. Waits for torrents to complete and meet seeding requirements
2. Identifies the media type based on content analysis
3. Extracts archives if needed
4. Moves files to the appropriate category directory
5. Renames files according to media type conventions
6. Updates the media library for browsing
### Book & Magazine Sorting
When enabled, the system can:
- Differentiate between books and magazines
- Extract author information from book filenames
- Organize magazines by title and issue number
- Create appropriate folder structures
## Detailed Features
### Automatic Media Detection
The system uses sophisticated detection to categorize downloads:
- **Movies**: Recognizes common patterns such as resolution (1080p, 720p) and release year
- **TV Shows**: Identifies season/episode patterns (S01E01) and TV-specific naming
- **Music**: Detects audio formats like MP3, FLAC, album folders
- **Books**: Identifies e-book formats (EPUB, MOBI, PDF) and author-title patterns
- **Magazines**: Recognizes magazine naming patterns, issues, volumes, and publication dates
- **Software**: Detects software installers, ISOs, and other program files
### RSS Feed Filtering
Powerful filtering options for RSS feeds:
- **Title matching**: Regular expression support for title patterns
- **Category filtering**: Filter by feed categories
- **Size limits**: Set minimum and maximum size requirements
- **Custom rules**: Combine multiple criteria for precise matching
### Remote Transmission Support
Full support for remote Transmission instances:
- **Secure authentication**: Username/password protection
- **Path mapping**: Configure how remote paths map to local directories
- **Full API support**: Complete control via the web interface
## Updating
To update to the latest version:
```bash
wget https://raw.githubusercontent.com/username/transmission-rss-manager/main/update.sh
chmod +x update.sh
sudo ./update.sh
```
## File Structure
```
transmission-rss-manager/
├── server.js # Main application server
├── postProcessor.js # Media processing module
├── rssFeedManager.js # RSS feed management module
├── install.sh # Installation script
├── update.sh # Update script
├── config.json # Configuration file
├── public/ # Web interface files
│ ├── index.html # Main web interface
│ ├── js/ # JavaScript files
│ │ └── enhanced-ui.js # Enhanced UI functionality
│ └── css/ # CSS stylesheets
└── README.md # This file
```
## Modules
### Post-Processor
The Post-Processor module handles:
- Monitoring completed torrents
- Categorizing content by type
- Extracting archives
- Organizing files into the correct directories
- Renaming files according to conventions
### RSS Feed Manager
The RSS Feed Manager module provides:
- Regular checking of configured RSS feeds
- Filtering of feed items based on rules
- Automated downloading of matching content
- History tracking of downloaded items
## Advanced Configuration
### Seeding Requirements
Set minimum seeding requirements before processing:
```json
"seedingRequirements": {
"minRatio": 1.0,
"minTimeMinutes": 60,
"checkIntervalSeconds": 300
}
```
### Processing Options
Customize how files are processed:
```json
"processingOptions": {
"enableBookSorting": true,
"extractArchives": true,
"deleteArchives": true,
"createCategoryFolders": true,
"ignoreSample": true,
"ignoreExtras": true,
"renameFiles": true,
"autoReplaceUpgrades": true,
"removeDuplicates": true,
"keepOnlyBestVersion": true
}
```
## Contributing
Contributions are welcome! Here's how you can help:
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
This project is licensed under the MIT License.
## Acknowledgments
- [Transmission](https://transmissionbt.com/) for the excellent BitTorrent client
- [Node.js](https://nodejs.org/) and the npm community for the foundation libraries
- All contributors who have helped improve this project

View File

@ -1,119 +0,0 @@
// server.js - Main application server file
const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
const fs = require('fs').promises;
const cors = require('cors');
const Transmission = require('transmission');
// Import custom modules
const PostProcessor = require('./postProcessor');
const RssFeedManager = require('./rssFeedManager');
// Initialize Express app
const app = express();
const PORT = process.env.PORT || 3000;
// Load configuration
let config = {
transmissionConfig: {
host: 'localhost',
port: 9091,
username: '',
password: '',
path: '/transmission/rpc'
},
remoteConfig: {
isRemote: false,
directoryMapping: {}
},
destinationPaths: {
movies: '/mnt/media/movies',
tvShows: '/mnt/media/tvshows',
music: '/mnt/media/music',
books: '/mnt/media/books',
software: '/mnt/media/software'
},
seedingRequirements: {
minRatio: 1.0,
minTimeMinutes: 60,
checkIntervalSeconds: 300
},
processingOptions: {
extractArchives: true,
deleteArchives: true,
createCategoryFolders: true,
ignoreSample: true,
ignoreExtras: true,
renameFiles: true,
autoReplaceUpgrades: true,
removeDuplicates: true,
keepOnlyBestVersion: true
},
rssFeeds: [],
rssUpdateIntervalMinutes: 60,
autoProcessing: false
};
// Service instances
let transmissionClient = null;
let postProcessor = null;
let rssFeedManager = null;
// Save config function
async function saveConfig() {
try {
await fs.writeFile(
path.join(__dirname, 'config.json'),
JSON.stringify(config, null, 2),
'utf8'
);
console.log('Configuration saved');
return true;
} catch (err) {
console.error('Error saving config:', err.message);
return false;
}
}
// Load config function
async function loadConfig() {
try {
const data = await fs.readFile(path.join(__dirname, 'config.json'), 'utf8');
const loadedConfig = JSON.parse(data);
config = { ...config, ...loadedConfig };
console.log('Configuration loaded');
return true;
} catch (err) {
console.error('Error loading config, using defaults:', err.message);
// Save default config
await saveConfig();
return false;
}
}
// Initialize Transmission client
function initTransmission() {
transmissionClient = new Transmission({
host: config.transmissionConfig.host,
port: config.transmissionConfig.port,
username: config.transmissionConfig.username,
password: config.transmissionConfig.password,
url: config.transmissionConfig.path
});
console.log(`Transmission client initialized for ${config.transmissionConfig.host}:${config.transmissionConfig.port}`);
return transmissionClient;
}
// Initialize post processor
function initPostProcessor() {
if (postProcessor) {
postProcessor.stop();
}
postProcessor = new PostProcessor({
transmissionConfig: config.transmissionConfig,
remoteConfig: config.remoteConfig,
destinationPaths: config.destinationPaths,
see

File diff suppressed because it is too large Load Diff

View File

@ -1,982 +0,0 @@
// postProcessor.js - Handles post-download processing
const fs = require('fs').promises;
const path = require('path');
const childProcess = require('child_process');
const util = require('util');
const exec = util.promisify(childProcess.exec);
const Transmission = require('transmission');
const AdmZip = require('adm-zip');
class PostProcessor {
constructor(config = {}) {
this.config = {
// Transmission connection details
transmissionConfig: config.transmissionConfig || {},
// Remote configuration
remoteConfig: {
isRemote: false,
directoryMapping: {},
...config.remoteConfig
},
// Destination paths
destinationPaths: {
movies: '/mnt/media/movies',
tvShows: '/mnt/media/tvshows',
music: '/mnt/media/music',
books: '/mnt/media/books',
software: '/mnt/media/software',
...config.destinationPaths
},
// Seeding requirements
seedingRequirements: {
minRatio: 1.0, // Minimum ratio to achieve
minTimeMinutes: 60, // Minimum seeding time in minutes
checkIntervalSeconds: 300, // Check interval in seconds
...config.seedingRequirements
},
// Processing options
processingOptions: {
extractArchives: true, // Extract rar, zip, etc.
deleteArchives: true, // Delete archives after extraction
createCategoryFolders: true, // Create category subfolders
ignoreSample: true, // Ignore sample files
ignoreExtras: true, // Ignore extra/bonus content
renameFiles: true, // Rename files to clean names
autoReplaceUpgrades: true, // Automatically replace with better quality
removeDuplicates: true, // Remove duplicate content
keepOnlyBestVersion: true, // Keep only the best version when duplicates exist
...config.processingOptions
},
// File patterns
filePatterns: {
videoExtensions: ['.mkv', '.mp4', '.avi', '.mov', '.wmv', '.m4v', '.mpg', '.mpeg'],
audioExtensions: ['.mp3', '.flac', '.m4a', '.wav', '.ogg', '.aac'],
archiveExtensions: ['.rar', '.zip', '.7z', '.tar', '.gz'],
ignorePatterns: ['sample', 'trailer', 'extra', 'bonus', 'behind.the.scenes', 'featurette'],
...config.filePatterns
}
};
this.transmissionClient = new Transmission(this.config.transmissionConfig);
this.processQueue = [];
this.isProcessing = false;
this.processingIntervalId = null;
// Initialize the database
this.managedContentDatabase = this.loadManagedContentDatabase();
}
// Start processing monitor
start() {
if (this.processingIntervalId) {
this.stop();
}
this.processingIntervalId = setInterval(() => {
this.checkCompletedTorrents();
}, this.config.seedingRequirements.checkIntervalSeconds * 1000);
console.log('Post-processor started');
this.checkCompletedTorrents(); // Do an initial check
return this;
}
// Stop processing monitor
stop() {
if (this.processingIntervalId) {
clearInterval(this.processingIntervalId);
this.processingIntervalId = null;
}
console.log('Post-processor stopped');
return this;
}
// Check for completed torrents that meet seeding requirements
async checkCompletedTorrents() {
if (this.isProcessing) {
return; // Don't start a new check if already processing
}
try {
this.isProcessing = true;
// Get detailed info about all torrents
const response = await this.getTorrents([
'id', 'name', 'status', 'downloadDir', 'percentDone', 'uploadRatio',
'addedDate', 'doneDate', 'downloadedEver', 'files', 'labels',
'secondsSeeding', 'isFinished'
]);
if (!response || !response.arguments || !response.arguments.torrents) {
throw new Error('Invalid response from Transmission');
}
const torrents = response.arguments.torrents;
// Find completed torrents that are seeding
for (const torrent of torrents) {
// Skip if not finished downloading or already processed
if (!torrent.isFinished || this.processQueue.some(item => item.id === torrent.id)) {
continue;
}
// Calculate seeding time in minutes
const seedingTimeMinutes = torrent.secondsSeeding / 60;
// Check if seeding requirements are met
const ratioMet = torrent.uploadRatio >= this.config.seedingRequirements.minRatio;
const timeMet = seedingTimeMinutes >= this.config.seedingRequirements.minTimeMinutes;
// If either requirement is met, queue for processing
if (ratioMet || timeMet) {
this.processQueue.push({
id: torrent.id,
name: torrent.name,
downloadDir: torrent.downloadDir,
files: torrent.files,
category: this.detectCategory(torrent),
ratioMet,
timeMet,
seedingTimeMinutes
});
console.log(`Queued torrent for processing: ${torrent.name}`);
}
}
// Process the queue
if (this.processQueue.length > 0) {
await this.processNextInQueue();
}
} catch (error) {
console.error('Error checking completed torrents:', error);
} finally {
this.isProcessing = false;
}
}
// Process next item in queue
async processNextInQueue() {
if (this.processQueue.length === 0) {
return;
}
const item = this.processQueue.shift();
try {
console.log(`Processing: ${item.name}`);
// 1. Determine target directory
const targetDir = this.getTargetDirectory(item);
// 2. Process files (copy/extract)
await this.processFiles(item, targetDir);
// 3. Update status to indicate processing is complete
console.log(`Processing complete for: ${item.name}`);
// 4. Optionally remove torrent from Transmission (commented out for safety)
// await this.removeTorrent(item.id, false);
} catch (error) {
console.error(`Error processing ${item.name}:`, error);
// Put back in queue to retry later
this.processQueue.push(item);
}
// Process next item
if (this.processQueue.length > 0) {
await this.processNextInQueue();
}
}
// Get torrents from Transmission
async getTorrents(fields) {
return new Promise((resolve, reject) => {
this.transmissionClient.get(fields, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
}
// Remove torrent from Transmission
async removeTorrent(id, deleteLocalData = false) {
return new Promise((resolve, reject) => {
this.transmissionClient.remove(id, deleteLocalData, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
}
// Determine the category of a torrent
detectCategory(torrent) {
// First check if torrent has labels
if (torrent.labels && torrent.labels.length > 0) {
const label = torrent.labels[0].toLowerCase();
if (label.includes('movie')) return 'movies';
if (label.includes('tv') || label.includes('show')) return 'tvShows';
if (label.includes('music') || label.includes('audio')) return 'music';
if (label.includes('book') || label.includes('ebook')) return 'books';
if (label.includes('software') || label.includes('app')) return 'software';
}
// Then check the name for common patterns
const name = torrent.name.toLowerCase();
// Check for TV show patterns (e.g., S01E01, Season 1, etc.)
if (/s\d{1,2}e\d{1,2}/i.test(name) || /season\s\d{1,2}/i.test(name)) {
return 'tvShows';
}
// Check file extensions to help determine type
const fileExtensions = torrent.files.map(file => {
const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
return ext;
});
const hasVideoFiles = fileExtensions.some(ext =>
this.config.filePatterns.videoExtensions.includes(ext)
);
const hasAudioFiles = fileExtensions.some(ext =>
this.config.filePatterns.audioExtensions.includes(ext)
);
if (hasVideoFiles) {
// Assume movies if has video but not detected as TV show
return 'movies';
}
if (hasAudioFiles) {
return 'music';
}
// Default fallback based on common terms
if (name.includes('movie') || name.includes('film')) return 'movies';
if (name.includes('album') || name.includes('discography')) return 'music';
if (name.includes('book') || name.includes('epub') || name.includes('pdf')) return 'books';
if (name.includes('software') || name.includes('program') || name.includes('app')) return 'software';
// Ultimate fallback
return 'movies';
}
// Get target directory path based on category
getTargetDirectory(item) {
const baseDir = this.config.destinationPaths[item.category] || this.config.destinationPaths.movies;
if (!this.config.processingOptions.createCategoryFolders) {
return baseDir;
}
// For movies, we might want to organize by name
if (item.category === 'movies') {
// Extract name and year if possible
const mediaInfo = this.extractMediaInfo(item.name);
if (mediaInfo.title) {
return `${baseDir}/${mediaInfo.title}${mediaInfo.year ? ` (${mediaInfo.year})` : ''}`;
}
}
// For TV shows, organize by show name
if (item.category === 'tvShows') {
// Try to extract show name from common patterns
const match = item.name.match(/^(.+?)(?:\s*S\d{1,2}|\s*Season\s*\d{1,2}|\s*\(\d{4}\))/i);
if (match) {
const showName = match[1].replace(/\./g, ' ').trim();
return `${baseDir}/${showName}`;
}
}
// Fallback to using the item name directly
return `${baseDir}/${item.name}`;
}
// Map a remote path to a local path
mapRemotePathToLocal(remotePath) {
if (!this.config.remoteConfig.isRemote) {
return remotePath; // Not a remote setup, return as is
}
const mapping = this.config.remoteConfig.directoryMapping;
// Check for exact match
if (mapping[remotePath]) {
return mapping[remotePath];
}
// Check for parent directory match
for (const [remote, local] of Object.entries(mapping)) {
if (remotePath.startsWith(remote)) {
const relativePath = remotePath.slice(remote.length);
return local + relativePath;
}
}
// No mapping found, return original path but log warning
console.warn(`No directory mapping found for remote path: ${remotePath}`);
return remotePath;
}
// Map a local path to a remote path
mapLocalPathToRemote(localPath) {
if (!this.config.remoteConfig.isRemote) {
return localPath; // Not a remote setup, return as is
}
const mapping = this.config.remoteConfig.directoryMapping;
const reversedMapping = {};
// Create reversed mapping (local -> remote)
for (const [remote, local] of Object.entries(mapping)) {
reversedMapping[local] = remote;
}
// Check for exact match
if (reversedMapping[localPath]) {
return reversedMapping[localPath];
}
// Check for parent directory match
for (const [local, remote] of Object.entries(reversedMapping)) {
if (localPath.startsWith(local)) {
const relativePath = localPath.slice(local.length);
return remote + relativePath;
}
}
// No mapping found, return original path but log warning
console.warn(`No directory mapping found for local path: ${localPath}`);
return localPath;
}
// Process files (copy or extract)
async processFiles(item, targetDir) {
// Check for existing versions of the same content
const existingVersion = await this.findExistingVersion(item);
if (existingVersion) {
console.log(`Found existing version: ${existingVersion.path}`);
// Check if new version is better quality
const isUpgrade = await this.isQualityUpgrade(item, existingVersion);
if (isUpgrade) {
console.log(`New version is a quality upgrade. Replacing...`);
// If auto-replace is enabled, remove the old version
if (this.config.processingOptions.autoReplaceUpgrades) {
await this.removeExistingVersion(existingVersion);
}
} else if (this.config.processingOptions.keepOnlyBestVersion) {
// If not an upgrade and we only want to keep the best version, stop processing
console.log(`New version is not a quality upgrade. Skipping...`);
return false;
}
}
// If checking for duplicates, verify this isn't a duplicate
if (this.config.processingOptions.removeDuplicates) {
const duplicates = await this.findDuplicates(item);
if (duplicates.length > 0) {
console.log(`Found ${duplicates.length} duplicate(s).`);
// Keep only the best version if enabled
if (this.config.processingOptions.keepOnlyBestVersion) {
// Determine the best version among all (including the new one)
const allVersions = [...duplicates, { item, quality: this.getQualityRank(item) }];
const bestVersion = this.findBestQualityVersion(allVersions);
// If the new version is the best, remove all others and continue
if (bestVersion.item === item) {
console.log(`New version is the best quality. Removing duplicates.`);
for (const duplicate of duplicates) {
await this.removeExistingVersion(duplicate);
}
} else {
// If the new version is not the best, skip processing
console.log(`A better version already exists. Skipping...`);
return false;
}
}
}
}
try {
// Create target directory
await fs.mkdir(targetDir, { recursive: true });
// For each file in the torrent
for (const file of item.files) {
// Handle remote path mapping if applicable
let sourcePath;
if (this.config.remoteConfig.isRemote) {
// Map the remote download dir to local path
const localDownloadDir = this.mapRemotePathToLocal(item.downloadDir);
sourcePath = path.join(localDownloadDir, file.name);
} else {
sourcePath = path.join(item.downloadDir, file.name);
}
const fileExt = path.extname(file.name).toLowerCase();
// Skip sample files if configured
if (this.config.processingOptions.ignoreSample &&
this.config.filePatterns.ignorePatterns.some(pattern =>
file.name.toLowerCase().includes(pattern))) {
console.log(`Skipping file (matches ignore pattern): ${file.name}`);
continue;
}
// Handle archives if extraction is enabled
if (this.config.processingOptions.extractArchives &&
this.config.filePatterns.archiveExtensions.includes(fileExt)) {
console.log(`Extracting archive: ${sourcePath} to ${targetDir}`);
// Extract the archive
if (fileExt === '.zip') {
await this.extractZipArchive(sourcePath, targetDir);
} else if (fileExt === '.rar') {
await this.extractRarArchive(sourcePath, targetDir);
} else {
console.log(`Unsupported archive type: ${fileExt}`);
// Just copy the file as is
let destFileName = file.name;
if (this.config.processingOptions.renameFiles) {
destFileName = this.cleanFileName(file.name);
}
const destPath = path.join(targetDir, destFileName);
await fs.copyFile(sourcePath, destPath);
}
// Delete archive after extraction if configured
if (this.config.processingOptions.deleteArchives) {
console.log(`Deleting archive after extraction: ${sourcePath}`);
try {
await fs.unlink(sourcePath);
} catch (err) {
console.error(`Error deleting archive: ${err.message}`);
}
}
}
// Copy non-archive files
else {
let destFileName = file.name;
// Rename files if configured
if (this.config.processingOptions.renameFiles) {
destFileName = this.cleanFileName(file.name);
}
// Create subdirectories if needed
const subDir = path.dirname(file.name);
if (subDir !== '.') {
const targetSubDir = path.join(targetDir, subDir);
await fs.mkdir(targetSubDir, { recursive: true });
}
const destPath = path.join(targetDir, destFileName);
console.log(`Copying file: ${sourcePath} to ${destPath}`);
// Copy the file
await fs.copyFile(sourcePath, destPath);
}
}
// After successful processing, update the managed content database
await this.updateManagedContent(item, targetDir);
return true;
} catch (error) {
console.error(`Error processing files: ${error.message}`);
throw error;
}
}
// Extract ZIP archive
async extractZipArchive(archivePath, destination) {
try {
const zip = new AdmZip(archivePath);
zip.extractAllTo(destination, true);
return true;
} catch (error) {
console.error(`Failed to extract ZIP archive: ${error.message}`);
throw error;
}
}
// Extract RAR archive
async extractRarArchive(archivePath, destination) {
try {
// Check if unrar is available
await exec('unrar --version');
// Use unrar to extract the archive
const command = `unrar x -o+ "${archivePath}" "${destination}"`;
await exec(command);
return true;
} catch (error) {
console.error(`Failed to extract RAR archive: ${error.message}`);
throw error;
}
}
// Load managed content database
loadManagedContentDatabase() {
try {
if (typeof window !== 'undefined' && window.localStorage) {
// Browser environment
const savedData = localStorage.getItem('managed-content-database');
return savedData ? JSON.parse(savedData) : [];
} else {
// Node.js environment
try {
const fs = require('fs');
const path = require('path');
const dbPath = path.join(__dirname, 'managed-content-database.json');
if (fs.existsSync(dbPath)) {
const data = fs.readFileSync(dbPath, 'utf8');
return JSON.parse(data);
}
} catch (err) {
console.error('Error reading database file:', err);
}
return [];
}
} catch (error) {
console.error('Error loading managed content database:', error);
return [];
}
}
// Save managed content database
saveManagedContentDatabase(database) {
try {
if (typeof window !== 'undefined' && window.localStorage) {
// Browser environment
localStorage.setItem('managed-content-database', JSON.stringify(database));
} else {
// Node.js environment
const fs = require('fs');
const path = require('path');
const dbPath = path.join(__dirname, 'managed-content-database.json');
fs.writeFileSync(dbPath, JSON.stringify(database, null, 2), 'utf8');
}
} catch (error) {
console.error('Error saving managed content database:', error);
}
}
// Update managed content database with new item
async updateManagedContent(item, targetDir) {
const database = this.loadManagedContentDatabase();
// Extract media info
const mediaInfo = this.extractMediaInfo(item.name);
// Calculate quality rank
const qualityRank = this.getQualityRank(item);
// Check if entry already exists
const existingIndex = database.findIndex(entry =>
entry.title.toLowerCase() === mediaInfo.title.toLowerCase() &&
(!mediaInfo.year || !entry.year || entry.year === mediaInfo.year)
);
// Create new entry
const newEntry = {
id: Date.now().toString(),
title: mediaInfo.title,
year: mediaInfo.year,
category: item.category,
quality: this.detectQuality(item.name),
qualityRank,
path: targetDir,
originalName: item.name,
dateAdded: new Date().toISOString(),
torrentId: item.id
};
// If entry exists, update or add to history
if (existingIndex !== -1) {
// If new version has better quality
if (qualityRank > database[existingIndex].qualityRank) {
// Save the old version to history
if (!database[existingIndex].history) {
database[existingIndex].history = [];
}
database[existingIndex].history.push({
quality: database[existingIndex].quality,
qualityRank: database[existingIndex].qualityRank,
path: database[existingIndex].path,
originalName: database[existingIndex].originalName,
dateAdded: database[existingIndex].dateAdded
});
// Update with new version
database[existingIndex].quality = newEntry.quality;
database[existingIndex].qualityRank = newEntry.qualityRank;
database[existingIndex].path = newEntry.path;
database[existingIndex].originalName = newEntry.originalName;
database[existingIndex].dateUpdated = new Date().toISOString();
database[existingIndex].torrentId = newEntry.torrentId;
}
} else {
// Add new entry
database.push(newEntry);
}
// Save updated database
this.saveManagedContentDatabase(database);
}
// Find existing version of the same content
async findExistingVersion(item) {
const database = this.loadManagedContentDatabase();
const mediaInfo = this.extractMediaInfo(item.name);
// Find matching content
const match = database.find(entry =>
entry.title.toLowerCase() === mediaInfo.title.toLowerCase() &&
(!mediaInfo.year || !entry.year || entry.year === mediaInfo.year)
);
return match;
}
// Find duplicates of the same content
async findDuplicates(item) {
const database = this.loadManagedContentDatabase();
const mediaInfo = this.extractMediaInfo(item.name);
// Find all matching content
const matches = database.filter(entry =>
entry.title.toLowerCase() === mediaInfo.title.toLowerCase() &&
(!mediaInfo.year || !entry.year || entry.year === mediaInfo.year)
);
return matches;
}
// Remove an existing version
async removeExistingVersion(version) {
console.log(`Removing existing version: ${version.path}`);
try {
// Check if directory exists
const stats = await fs.stat(version.path);
if (stats.isDirectory()) {
// Remove directory and all contents
await this.removeDirectory(version.path);
} else {
// Just remove the file
await fs.unlink(version.path);
}
// Remove from managed content database
const database = this.loadManagedContentDatabase();
const updatedDatabase = database.filter(entry => entry.id !== version.id);
this.saveManagedContentDatabase(updatedDatabase);
return true;
} catch (error) {
console.error(`Error removing existing version: ${error.message}`);
return false;
}
}
// Remove a directory recursively
async removeDirectory(dirPath) {
try {
const items = await fs.readdir(dirPath);
for (const item of items) {
const itemPath = path.join(dirPath, item);
const stats = await fs.stat(itemPath);
if (stats.isDirectory()) {
await this.removeDirectory(itemPath);
} else {
await fs.unlink(itemPath);
}
}
await fs.rmdir(dirPath);
} catch (error) {
console.error(`Error removing directory: ${error.message}`);
throw error;
}
}
// Check if a new version is a quality upgrade
async isQualityUpgrade(newItem, existingVersion) {
const newQuality = this.getQualityRank(newItem);
const existingQuality = existingVersion.qualityRank || 0;
return newQuality > existingQuality;
}
// Find the best quality version among multiple versions
findBestQualityVersion(versions) {
return versions.reduce((best, current) => {
return (current.qualityRank > best.qualityRank) ? current : best;
}, versions[0]);
}
// Calculate quality rank for an item
getQualityRank(item) {
const quality = this.detectQuality(item.name);
if (!quality) return 0;
const qualityStr = quality.toLowerCase();
if (qualityStr.includes('2160p') || qualityStr.includes('4k') || qualityStr.includes('uhd')) {
return 5;
}
if (qualityStr.includes('1080p') || qualityStr.includes('fullhd') ||
(qualityStr.includes('bluray') && !qualityStr.includes('720p'))) {
return 4;
}
if (qualityStr.includes('720p') || qualityStr.includes('hd')) {
return 3;
}
if (qualityStr.includes('dvdrip') || qualityStr.includes('sdbd')) {
return 2;
}
if (qualityStr.includes('webrip') || qualityStr.includes('webdl') || qualityStr.includes('web-dl')) {
return 1;
}
// Low quality or unknown
return 0;
}
// Detect quality from item name
detectQuality(name) {
const lowerName = name.toLowerCase();
// Quality patterns
const qualityPatterns = [
{ regex: /\b(2160p|uhd|4k)\b/i, quality: '2160p/4K' },
{ regex: /\b(1080p|fullhd|fhd)\b/i, quality: '1080p' },
{ regex: /\b(720p|hd)\b/i, quality: '720p' },
{ regex: /\b(bluray|bdremux|bdrip)\b/i, quality: 'BluRay' },
{ regex: /\b(webdl|web-dl|webrip)\b/i, quality: 'WebDL' },
{ regex: /\b(dvdrip|dvd-rip)\b/i, quality: 'DVDRip' },
{ regex: /\b(hdtv)\b/i, quality: 'HDTV' },
{ regex: /\b(hdtc|hd-tc)\b/i, quality: 'HDTC' },
{ regex: /\b(hdts|hd-ts|hdcam)\b/i, quality: 'HDTS' },
{ regex: /\b(cam|camrip)\b/i, quality: 'CAM' }
];
for (const pattern of qualityPatterns) {
if (pattern.regex.test(lowerName)) {
return pattern.quality;
}
}
return 'Unknown';
}
// Extract media info from name
extractMediaInfo(name) {
// Year pattern (e.g., "2023" or "(2023)" or "[2023]")
const yearPattern = /[\[\(\s.]+(19|20)\d{2}[\]\)\s.]+/;
const yearMatch = name.match(yearPattern);
const year = yearMatch ? yearMatch[0].replace(/[\[\]\(\)\s.]+/g, '') : null;
// Clean up title to get movie name
let cleanTitle = yearMatch ? name.split(yearPattern)[0].trim() : name;
// Remove quality indicators, scene tags, etc.
const cleanupPatterns = [
/\b(720p|1080p|2160p|4K|UHD|HDTV|WEB-DL|WEBRip|BRRip|BluRay|DVDRip)\b/gi,
/\b(HEVC|AVC|x264|x265|H\.?264|H\.?265|XVID|DIVX)\b/gi,
/\b(AAC|MP3|AC3|DTS|FLAC|OPUS)\b/gi,
/\b(AMZN|DSNP|HULU|HMAX|NF|PCOK)\b/gi,
/\b(EXTENDED|Directors\.?Cut|UNRATED|REMASTERED|PROPER|REMUX)\b/gi,
/\b(IMAX)\b/gi,
/[-\.][A-Za-z0-9]+$/
];
cleanupPatterns.forEach(pattern => {
cleanTitle = cleanTitle.replace(pattern, '');
});
// Final cleanup
cleanTitle = cleanTitle
.replace(/[._-]/g, ' ')
.replace(/\s{2,}/g, ' ')
.trim();
return {
title: cleanTitle,
year: year
};
}
// Clean up file name for nicer presentation
cleanFileName(fileName) {
// Remove common scene tags
let cleaned = fileName;
// Remove stuff in brackets
cleaned = cleaned.replace(/\[[^\]]+\]/g, '');
// Remove scene group names after dash
cleaned = cleaned.replace(/-[A-Za-z0-9]+$/, '');
// Replace dots with spaces
cleaned = cleaned.replace(/\./g, ' ');
// Remove multiple spaces
cleaned = cleaned.replace(/\s{2,}/g, ' ').trim();
// Keep the file extension
const lastDotIndex = fileName.lastIndexOf('.');
if (lastDotIndex !== -1) {
const extension = fileName.substring(lastDotIndex);
cleaned = cleaned + extension;
}
return cleaned;
}
// Update configuration
updateConfig(newConfig) {
this.config = {
...this.config,
...newConfig,
transmissionConfig: {
...this.config.transmissionConfig,
...newConfig.transmissionConfig
},
remoteConfig: {
...this.config.remoteConfig,
...newConfig.remoteConfig
},
destinationPaths: {
...this.config.destinationPaths,
...newConfig.destinationPaths
},
seedingRequirements: {
...this.config.seedingRequirements,
...newConfig.seedingRequirements
},
processingOptions: {
...this.config.processingOptions,
...newConfig.processingOptions
},
filePatterns: {
...this.config.filePatterns,
...newConfig.filePatterns
}
};
// Reinitialize the Transmission client
this.transmissionClient = new Transmission(this.config.transmissionConfig);
return this;
}
// Get library statistics
getLibraryStats() {
const database = this.loadManagedContentDatabase();
const stats = {
totalItems: database.length,
byCategory: {},
byQuality: {
'Unknown': 0,
'CAM/TS': 0,
'SD': 0,
'720p': 0,
'1080p': 0,
'4K/UHD': 0
},
totalUpgrades: 0
};
// Process each item
database.forEach(item => {
// Count by category
stats.byCategory[item.category] = (stats.byCategory[item.category] || 0) + 1;
// Count by quality rank
if (item.qualityRank >= 5) {
stats.byQuality['4K/UHD']++;
} else if (item.qualityRank >= 4) {
stats.byQuality['1080p']++;
} else if (item.qualityRank >= 3) {
stats.byQuality['720p']++;
} else if (item.qualityRank >= 1) {
stats.byQuality['SD']++;
} else if (item.qualityRank === 0 && item.quality && item.quality.toLowerCase().includes('cam')) {
stats.byQuality['CAM/TS']++;
} else {
stats.byQuality['Unknown']++;
}
// Count upgrades
if (item.history && item.history.length > 0) {
stats.totalUpgrades += item.history.length;
}
});
return stats;
}
// Get entire media library
getLibrary() {
return this.loadManagedContentDatabase();
}
// Search for media items
searchLibrary(query) {
if (!query) return this.getLibrary();
const database = this.loadManagedContentDatabase();
const lowerQuery = query.toLowerCase();
return database.filter(item =>
item.title.toLowerCase().includes(lowerQuery) ||
(item.year && item.year.includes(lowerQuery)) ||
(item.category && item.category.toLowerCase().includes(lowerQuery)) ||
(item.quality && item.quality.toLowerCase().includes(lowerQuery))
);
}
}
// Export for Node.js or browser
if (typeof module !== 'undefined' && module.exports) {
module.exports = PostProcessor;
} else if (typeof window !== 'undefined') {
window.PostProcessor = PostProcessor;
}

View File

@ -1,586 +0,0 @@
// rssFeedManager.js - Manages RSS feeds and parsing
const xml2js = require('xml2js');
const fetch = require('node-fetch');
const fs = require('fs').promises;
const path = require('path');
class RssFeedManager {
constructor(config = {}) {
this.config = {
feeds: [],
updateIntervalMinutes: 60,
maxItemsPerFeed: 100,
autoDownload: false,
downloadFilters: {},
...config
};
this.parser = new xml2js.Parser({
explicitArray: false,
normalize: true,
normalizeTags: true
});
this.feedItems = [];
this.updateIntervalId = null;
}
// Start RSS feed monitoring
start() {
if (this.updateIntervalId) {
this.stop();
}
this.updateIntervalId = setInterval(() => {
this.updateAllFeeds();
}, this.config.updateIntervalMinutes * 60 * 1000);
console.log('RSS feed manager started');
this.updateAllFeeds(); // Do an initial update
return this;
}
// Stop RSS feed monitoring
stop() {
if (this.updateIntervalId) {
clearInterval(this.updateIntervalId);
this.updateIntervalId = null;
}
console.log('RSS feed manager stopped');
return this;
}
// Update all feeds
async updateAllFeeds() {
console.log('Updating all RSS feeds');
const results = {
total: this.config.feeds.length,
success: 0,
failed: 0,
newItems: 0
};
for (const feed of this.config.feeds) {
try {
const newItems = await this.updateFeed(feed);
results.success++;
results.newItems += newItems;
} catch (error) {
console.error(`Error updating feed ${feed.name || feed.url}:`, error);
results.failed++;
}
}
console.log(`Feed update complete: ${results.success}/${results.total} feeds updated, ${results.newItems} new items`);
// Save updated feed items to database
await this.saveItems();
return results;
}
// Update a single feed
async updateFeed(feed) {
console.log(`Updating feed: ${feed.name || feed.url}`);
try {
const response = await fetch(feed.url);
if (!response.ok) {
throw new Error(`Failed to fetch RSS feed: ${response.statusText}`);
}
const xml = await response.text();
const result = await this.parseXml(xml);
// Extract and format feed items
const items = this.extractItems(result, feed);
// Count new items (not already in our database)
let newItemCount = 0;
// Process each item
for (const item of items) {
// Generate a unique ID for the item
const itemId = this.generateItemId(item);
// Check if item already exists
const existingItem = this.feedItems.find(i => i.id === itemId);
if (!existingItem) {
// Add feed info to item
item.feedId = feed.id;
item.feedName = feed.name;
item.id = itemId;
item.firstSeen = new Date().toISOString();
item.downloaded = false;
// Add to items array
this.feedItems.push(item);
newItemCount++;
// Auto-download if enabled and item matches filter
if (feed.autoDownload && this.matchesFilter(item, feed.filters)) {
this.downloadItem(item);
}
}
}
console.log(`Feed updated: ${feed.name || feed.url}, ${newItemCount} new items`);
return newItemCount;
} catch (error) {
console.error(`Error updating feed: ${error.message}`);
throw error;
}
}
// Parse XML data
async parseXml(xml) {
return new Promise((resolve, reject) => {
this.parser.parseString(xml, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
}
// Extract items from parsed feed
extractItems(parsedFeed, feedConfig) {
const items = [];
// Handle different feed formats
let feedItems = [];
if (parsedFeed.rss && parsedFeed.rss.channel) {
// Standard RSS
feedItems = parsedFeed.rss.channel.item || [];
if (!Array.isArray(feedItems)) {
feedItems = [feedItems];
}
} else if (parsedFeed.feed && parsedFeed.feed.entry) {
// Atom format
feedItems = parsedFeed.feed.entry || [];
if (!Array.isArray(feedItems)) {
feedItems = [feedItems];
}
}
// Process each item
for (const item of feedItems) {
const processedItem = this.processItem(item, feedConfig);
if (processedItem) {
items.push(processedItem);
}
}
return items;
}
// Process a single feed item
processItem(item, feedConfig) {
// Extract basic fields
const processed = {
title: item.title || '',
description: item.description || item.summary || '',
pubDate: item.pubdate || item.published || item.date || new Date().toISOString(),
size: 0,
seeders: 0,
leechers: 0,
quality: 'Unknown',
category: 'Unknown'
};
// Set download link
if (item.link) {
if (typeof item.link === 'string') {
processed.link = item.link;
} else if (item.link.$ && item.link.$.href) {
processed.link = item.link.$.href;
} else if (Array.isArray(item.link) && item.link.length > 0) {
const magnetLink = item.link.find(link =>
(typeof link === 'string' && link.startsWith('magnet:')) ||
(link.$ && link.$.href && link.$.href.startsWith('magnet:'))
);
if (magnetLink) {
processed.link = typeof magnetLink === 'string' ? magnetLink : magnetLink.$.href;
} else {
processed.link = typeof item.link[0] === 'string' ? item.link[0] : (item.link[0].$ ? item.link[0].$.href : '');
}
}
} else if (item.enclosure && item.enclosure.url) {
processed.link = item.enclosure.url;
}
// Skip item if no link found
if (!processed.link) {
return null;
}
// Try to extract size information
if (item.size) {
processed.size = parseInt(item.size, 10) || 0;
} else if (item.enclosure && item.enclosure.length) {
processed.size = parseInt(item.enclosure.length, 10) || 0;
} else if (item['torrent:contentlength']) {
processed.size = parseInt(item['torrent:contentlength'], 10) || 0;
}
// Convert size to MB if it's in bytes
if (processed.size > 1000000) {
processed.size = Math.round(processed.size / 1000000);
}
// Try to extract seeders/leechers
if (item.seeders || item['torrent:seeds']) {
processed.seeders = parseInt(item.seeders || item['torrent:seeds'], 10) || 0;
}
if (item.leechers || item['torrent:peers']) {
processed.leechers = parseInt(item.leechers || item['torrent:peers'], 10) || 0;
}
// Try to determine category
if (item.category) {
if (Array.isArray(item.category)) {
processed.category = item.category[0] || 'Unknown';
} else {
processed.category = item.category;
}
} else if (feedConfig.defaultCategory) {
processed.category = feedConfig.defaultCategory;
}
// Detect quality from title
processed.quality = this.detectQuality(processed.title);
return processed;
}
// Detect quality from title
detectQuality(title) {
const lowerTitle = title.toLowerCase();
// Quality patterns
const qualityPatterns = [
{ regex: /\b(2160p|uhd|4k)\b/i, quality: '2160p/4K' },
{ regex: /\b(1080p|fullhd|fhd)\b/i, quality: '1080p' },
{ regex: /\b(720p|hd)\b/i, quality: '720p' },
{ regex: /\b(bluray|bdremux|bdrip)\b/i, quality: 'BluRay' },
{ regex: /\b(webdl|web-dl|webrip)\b/i, quality: 'WebDL' },
{ regex: /\b(dvdrip|dvd-rip)\b/i, quality: 'DVDRip' },
{ regex: /\b(hdtv)\b/i, quality: 'HDTV' },
{ regex: /\b(hdtc|hd-tc)\b/i, quality: 'HDTC' },
{ regex: /\b(hdts|hd-ts|hdcam)\b/i, quality: 'HDTS' },
{ regex: /\b(cam|camrip)\b/i, quality: 'CAM' }
];
for (const pattern of qualityPatterns) {
if (pattern.regex.test(lowerTitle)) {
return pattern.quality;
}
}
return 'Unknown';
}
// Generate a unique ID for an item
generateItemId(item) {
// Use hash of title + link as ID
const hash = require('crypto').createHash('md5');
hash.update(item.title + item.link);
return hash.digest('hex');
}
// Check if an item matches a filter
matchesFilter(item, filters) {
if (!filters) return true;
// Name contains filter
if (filters.nameContains && !item.title.toLowerCase().includes(filters.nameContains.toLowerCase())) {
return false;
}
// Size filters
if (filters.minSize && item.size < filters.minSize) {
return false;
}
if (filters.maxSize && filters.maxSize > 0 && item.size > filters.maxSize) {
return false;
}
// Category filter
if (filters.includeCategory && !item.category.toLowerCase().includes(filters.includeCategory.toLowerCase())) {
return false;
}
// Exclude terms
if (filters.excludeTerms) {
const terms = filters.excludeTerms.toLowerCase().split(',').map(t => t.trim());
if (terms.some(term => item.title.toLowerCase().includes(term))) {
return false;
}
}
// Unwanted formats
if (filters.unwantedFormats) {
const formats = filters.unwantedFormats.toLowerCase().split(',').map(f => f.trim());
if (formats.some(format => item.title.toLowerCase().includes(format) ||
(item.quality && item.quality.toLowerCase() === format.toLowerCase()))) {
return false;
}
}
// Minimum quality
if (filters.minimumQuality) {
const qualityRanks = {
'unknown': 0,
'cam': 0,
'ts': 0,
'hdts': 0,
'tc': 0,
'hdtc': 0,
'dvdscr': 1,
'webrip': 1,
'webdl': 1,
'dvdrip': 2,
'hdtv': 2,
'720p': 3,
'hd': 3,
'1080p': 4,
'fullhd': 4,
'bluray': 4,
'2160p': 5,
'4k': 5,
'uhd': 5
};
const minQualityRank = qualityRanks[filters.minimumQuality.toLowerCase()] || 0;
const itemQualityRank = this.getQualityRank(item.quality);
if (itemQualityRank < minQualityRank) {
return false;
}
}
// Minimum seeders
if (filters.minimumSeeders && item.seeders < filters.minimumSeeders) {
return false;
}
// Passed all filters
return true;
}
// Calculate quality rank for an item
getQualityRank(quality) {
if (!quality) return 0;
const qualityStr = quality.toLowerCase();
if (qualityStr.includes('2160p') || qualityStr.includes('4k') || qualityStr.includes('uhd')) {
return 5;
}
if (qualityStr.includes('1080p') || qualityStr.includes('fullhd') ||
(qualityStr.includes('bluray') && !qualityStr.includes('720p'))) {
return 4;
}
if (qualityStr.includes('720p') || qualityStr.includes('hd')) {
return 3;
}
if (qualityStr.includes('dvdrip') || qualityStr.includes('sdbd')) {
return 2;
}
if (qualityStr.includes('webrip') || qualityStr.includes('webdl') || qualityStr.includes('web-dl')) {
return 1;
}
// Low quality or unknown
return 0;
}
// Download an item (add to Transmission)
async downloadItem(item, transmissionClient) {
if (!transmissionClient) {
console.error('No Transmission client provided for download');
return { success: false, message: 'No Transmission client provided' };
}
try {
// Add the torrent to Transmission
const result = await new Promise((resolve, reject) => {
transmissionClient.addUrl(item.link, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
// Mark the item as downloaded
const existingItem = this.feedItems.find(i => i.id === item.id);
if (existingItem) {
existingItem.downloaded = true;
existingItem.downloadDate = new Date().toISOString();
existingItem.transmissionId = result.id || result.hashString || null;
}
// Save updated items
await this.saveItems();
return { success: true, result };
} catch (error) {
console.error(`Error downloading item: ${error.message}`);
return { success: false, message: error.message };
}
}
// Save items to persistent storage
async saveItems() {
try {
// In Node.js environment
const dbPath = path.join(__dirname, 'rss-items.json');
await fs.writeFile(dbPath, JSON.stringify(this.feedItems, null, 2), 'utf8');
return true;
} catch (error) {
console.error('Error saving RSS items:', error);
return false;
}
}
// Load items from persistent storage
async loadItems() {
try {
// In Node.js environment
const dbPath = path.join(__dirname, 'rss-items.json');
try {
const data = await fs.readFile(dbPath, 'utf8');
this.feedItems = JSON.parse(data);
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
// File doesn't exist, use empty array
this.feedItems = [];
}
return true;
} catch (error) {
console.error('Error loading RSS items:', error);
return false;
}
}
// Get all feed items
getAllItems() {
return [...this.feedItems];
}
// Get undownloaded feed items
getUndownloadedItems() {
return this.feedItems.filter(item => !item.downloaded);
}
// Filter items based on criteria
filterItems(filters) {
return this.feedItems.filter(item => this.matchesFilter(item, filters));
}
// Add a new feed
addFeed(feed) {
// Generate an ID if not provided
if (!feed.id) {
feed.id = `feed-${Date.now()}`;
}
// Add the feed to config
this.config.feeds.push(feed);
// Update the feed immediately
this.updateFeed(feed);
return feed;
}
// Remove a feed
removeFeed(feedId) {
const index = this.config.feeds.findIndex(f => f.id === feedId);
if (index !== -1) {
this.config.feeds.splice(index, 1);
return true;
}
return false;
}
// Update feed configuration
updateFeedConfig(feedId, updates) {
const feed = this.config.feeds.find(f => f.id === feedId);
if (feed) {
Object.assign(feed, updates);
return true;
}
return false;
}
// Get all feeds
getAllFeeds() {
return [...this.config.feeds];
}
// Save configuration
async saveConfig() {
try {
// In Node.js environment
const configPath = path.join(__dirname, 'rss-config.json');
await fs.writeFile(configPath, JSON.stringify(this.config, null, 2), 'utf8');
return true;
} catch (error) {
console.error('Error saving RSS config:', error);
return false;
}
}
// Load configuration
async loadConfig() {
try {
// In Node.js environment
const configPath = path.join(__dirname, 'rss-config.json');
try {
const data = await fs.readFile(configPath, 'utf8');
const loadedConfig = JSON.parse(data);
this.config = { ...this.config, ...loadedConfig };
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
// Config file doesn't exist, use defaults
}
return true;
} catch (error) {
console.error('Error loading RSS config:', error);
return false;
}
}
}
// Export for Node.js or browser
if (typeof module !== 'undefined' && module.exports) {
module.exports = RssFeedManager;
} else if (typeof window !== 'undefined') {
window.RssFeedManager = RssFeedManager;
}