// 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(/[\s\S]*?<\/item>/g) || []; for (const itemXml of matches) { const titleMatch = itemXml.match(/(.*?)<\/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;