torrent-man/modules/rss-feed-manager.js
Claude 9e544456db 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>
2025-03-13 17:16:41 +00:00

342 lines
9.1 KiB
JavaScript

// 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;