This commit addresses multiple code consistency and reliability issues across the codebase: 1. Version consistency - use package.json version (2.0.9) throughout 2. Improved module loading with better error handling and consistent symlinks 3. Enhanced data directory handling with better error checking 4. Fixed redundant code in main-installer.sh 5. Improved error handling in transmission-client.js 6. Added extensive module symlink creation 7. Better file path handling and permission checks 8. Enhanced API response handling 💡 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
787 lines
22 KiB
JavaScript
787 lines
22 KiB
JavaScript
// rss-feed-manager.js - Handles RSS feed fetching, parsing, and torrent management
|
|
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) {
|
|
if (!config) {
|
|
throw new Error('Configuration is required');
|
|
}
|
|
|
|
this.config = config;
|
|
this.feeds = config.feeds || [];
|
|
this.items = [];
|
|
this.updateIntervalId = null;
|
|
this.updateIntervalMinutes = config.updateIntervalMinutes || 60;
|
|
this.parser = new xml2js.Parser({ explicitArray: false });
|
|
|
|
// Set up the data path - first check if a data path was provided in the config
|
|
if (config.dataPath) {
|
|
this.dataPath = config.dataPath;
|
|
} else {
|
|
// Otherwise, use the default path relative to this module
|
|
this.dataPath = path.join(__dirname, '..', 'data');
|
|
|
|
// Log the data path for debugging
|
|
console.log(`Data directory path set to: ${this.dataPath}`);
|
|
}
|
|
|
|
// We'll always ensure the data directory exists regardless of where it's set
|
|
// Use synchronous operation to ensure directory exists immediately upon construction
|
|
try {
|
|
const fsSync = require('fs');
|
|
if (!fsSync.existsSync(this.dataPath)) {
|
|
fsSync.mkdirSync(this.dataPath, { recursive: true });
|
|
console.log(`Created data directory synchronously: ${this.dataPath}`);
|
|
}
|
|
} catch (err) {
|
|
console.warn(`Warning: Could not create data directory synchronously: ${err.message}`);
|
|
// Will try again asynchronously in ensureDataDirectory when start() is called
|
|
}
|
|
|
|
// Maximum items to keep in memory to prevent memory leaks
|
|
this.maxItemsInMemory = config.maxItemsInMemory || 5000;
|
|
}
|
|
|
|
async start() {
|
|
if (this.updateIntervalId) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Make sure the data directory exists first
|
|
await this.ensureDataDirectory();
|
|
console.log(`Using data directory: ${this.dataPath}`);
|
|
|
|
// Load existing feeds and items
|
|
await this.loadFeeds();
|
|
await this.loadItems();
|
|
|
|
// Run update immediately
|
|
await this.updateAllFeeds().catch(error => {
|
|
console.error('Error in initial feed update:', error);
|
|
});
|
|
|
|
// Then set up interval
|
|
this.updateIntervalId = setInterval(async () => {
|
|
await this.updateAllFeeds().catch(error => {
|
|
console.error('Error in scheduled feed update:', error);
|
|
});
|
|
}, this.updateIntervalMinutes * 60 * 1000);
|
|
|
|
console.log(`RSS feed manager started, interval: ${this.updateIntervalMinutes} minutes`);
|
|
} catch (error) {
|
|
console.error('Failed to start RSS feed manager:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
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 = [];
|
|
|
|
// Check if feeds array is valid
|
|
if (!Array.isArray(this.feeds)) {
|
|
console.error('Feeds is not an array:', this.feeds);
|
|
this.feeds = [];
|
|
return results;
|
|
}
|
|
|
|
for (const feed of this.feeds) {
|
|
if (!feed || !feed.id || !feed.url) {
|
|
console.error('Invalid feed object:', feed);
|
|
continue;
|
|
}
|
|
|
|
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
|
|
});
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Save updated items and truncate if necessary
|
|
this.trimItemsIfNeeded();
|
|
await this.saveItems();
|
|
await this.saveFeeds();
|
|
} catch (error) {
|
|
console.error('Error saving data after feed update:', error);
|
|
}
|
|
|
|
console.log('RSS feed update completed');
|
|
return results;
|
|
}
|
|
|
|
// Trim items to prevent memory bloat
|
|
trimItemsIfNeeded() {
|
|
if (this.items.length > this.maxItemsInMemory) {
|
|
console.log(`Trimming items from ${this.items.length} to ${this.maxItemsInMemory}`);
|
|
|
|
// Sort by date (newest first) and keep only the newest maxItemsInMemory items
|
|
this.items.sort((a, b) => new Date(b.added) - new Date(a.added));
|
|
this.items = this.items.slice(0, this.maxItemsInMemory);
|
|
}
|
|
}
|
|
|
|
async updateFeed(feed) {
|
|
if (!feed || !feed.url) {
|
|
throw new Error('Invalid feed configuration');
|
|
}
|
|
|
|
console.log(`Updating feed: ${feed.name || 'Unnamed'} (${feed.url})`);
|
|
|
|
try {
|
|
// Get version from package.json if available, fallback to environment or hardcoded
|
|
const version = process.env.APP_VERSION || require('../package.json').version || '2.0.9';
|
|
|
|
const response = await fetch(feed.url, {
|
|
timeout: 30000, // 30 second timeout
|
|
headers: {
|
|
'User-Agent': `Transmission-RSS-Manager/${version}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const xml = await response.text();
|
|
|
|
if (!xml || xml.trim() === '') {
|
|
throw new Error('Empty feed content');
|
|
}
|
|
|
|
const result = await this.parseXml(xml);
|
|
|
|
if (!result) {
|
|
throw new Error('Failed to parse XML feed');
|
|
}
|
|
|
|
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 || 'Unnamed'}`);
|
|
|
|
return {
|
|
totalItems: rssItems.length,
|
|
newItems: newItems.length
|
|
};
|
|
} catch (error) {
|
|
console.error(`Error updating feed ${feed.url}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
parseXml(xml) {
|
|
if (!xml || typeof xml !== 'string') {
|
|
return Promise.reject(new Error('Invalid XML input'));
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.parser.parseString(xml, (error, result) => {
|
|
if (error) {
|
|
reject(error);
|
|
} else {
|
|
resolve(result);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
extractItems(parsedXml, feed) {
|
|
if (!parsedXml || !feed) {
|
|
console.error('Invalid parsed XML or feed');
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
// Handle standard RSS 2.0
|
|
if (parsedXml.rss && parsedXml.rss.channel) {
|
|
const channel = parsedXml.rss.channel;
|
|
|
|
if (!channel.item) {
|
|
return [];
|
|
}
|
|
|
|
const items = Array.isArray(channel.item)
|
|
? channel.item.filter(Boolean)
|
|
: (channel.item ? [channel.item] : []);
|
|
|
|
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.filter(Boolean)
|
|
: (parsedXml.feed.entry ? [parsedXml.feed.entry] : []);
|
|
|
|
return entries.map(entry => this.normalizeAtomItem(entry, feed));
|
|
}
|
|
|
|
return [];
|
|
} catch (error) {
|
|
console.error('Error extracting items from XML:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
normalizeRssItem(item, feed) {
|
|
if (!item || !feed) {
|
|
console.error('Invalid RSS item or feed');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
// Create a unique ID for the item
|
|
const title = item.title || 'Untitled';
|
|
const pubDate = item.pubDate || '';
|
|
const link = item.link || '';
|
|
const idContent = `${feed.id}:${title}:${pubDate}:${link}`;
|
|
const id = crypto.createHash('md5').update(idContent).digest('hex');
|
|
|
|
// Extract enclosure (torrent link)
|
|
let torrentLink = link;
|
|
let fileSize = 0;
|
|
|
|
if (item.enclosure) {
|
|
if (item.enclosure.$) {
|
|
torrentLink = item.enclosure.$.url || torrentLink;
|
|
fileSize = parseInt(item.enclosure.$.length || 0, 10);
|
|
} else if (typeof item.enclosure === 'object') {
|
|
torrentLink = item.enclosure.url || torrentLink;
|
|
fileSize = 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;
|
|
// Handle if category is an object with a value property
|
|
if (typeof category === 'object' && category._) {
|
|
category = category._;
|
|
}
|
|
}
|
|
|
|
// Some feeds use torrent:contentLength
|
|
if (item['torrent:contentLength']) {
|
|
const contentLength = parseInt(item['torrent:contentLength'], 10);
|
|
if (!isNaN(contentLength)) {
|
|
size = contentLength;
|
|
}
|
|
}
|
|
|
|
return {
|
|
id,
|
|
feedId: feed.id,
|
|
title,
|
|
link,
|
|
torrentLink,
|
|
pubDate: pubDate || new Date().toISOString(),
|
|
category: category || '',
|
|
description: item.description || '',
|
|
size: !isNaN(size) ? size : 0,
|
|
downloaded: false,
|
|
ignored: false,
|
|
added: new Date().toISOString()
|
|
};
|
|
} catch (error) {
|
|
console.error('Error normalizing RSS item:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
normalizeAtomItem(entry, feed) {
|
|
if (!entry || !feed) {
|
|
console.error('Invalid Atom entry or feed');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
// Create a unique ID for the item
|
|
const title = entry.title || 'Untitled';
|
|
const updated = entry.updated || '';
|
|
const entryId = entry.id || '';
|
|
const idContent = `${feed.id}:${title}:${updated}:${entryId}`;
|
|
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.filter(l => l && l.$);
|
|
const alternateLink = links.find(l => l.$ && l.$.rel === 'alternate');
|
|
const torrentTypeLink = links.find(l => l.$ && l.$.type && l.$.type.includes('torrent'));
|
|
|
|
link = alternateLink && alternateLink.$ && alternateLink.$.href ?
|
|
alternateLink.$.href :
|
|
(links[0] && links[0].$ && links[0].$.href ? links[0].$.href : '');
|
|
|
|
torrentLink = torrentTypeLink && torrentTypeLink.$ && torrentTypeLink.$.href ?
|
|
torrentTypeLink.$.href : link;
|
|
} else if (entry.link.$ && entry.link.$.href) {
|
|
link = entry.link.$.href;
|
|
torrentLink = link;
|
|
}
|
|
}
|
|
|
|
// Extract category
|
|
let category = '';
|
|
if (entry.category && entry.category.$ && entry.category.$.term) {
|
|
category = entry.category.$.term;
|
|
}
|
|
|
|
// Extract content
|
|
let description = '';
|
|
if (entry.summary) {
|
|
description = entry.summary;
|
|
} else if (entry.content) {
|
|
description = entry.content;
|
|
}
|
|
|
|
return {
|
|
id,
|
|
feedId: feed.id,
|
|
title,
|
|
link,
|
|
torrentLink,
|
|
pubDate: entry.updated || entry.published || new Date().toISOString(),
|
|
category,
|
|
description,
|
|
size: 0, // Atom doesn't typically include file size
|
|
downloaded: false,
|
|
ignored: false,
|
|
added: new Date().toISOString()
|
|
};
|
|
} catch (error) {
|
|
console.error('Error normalizing Atom item:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
processNewItems(rssItems, feed) {
|
|
if (!Array.isArray(rssItems) || !feed) {
|
|
console.error('Invalid RSS items array or feed');
|
|
return [];
|
|
}
|
|
|
|
const newItems = [];
|
|
|
|
// Filter out null items
|
|
const validItems = rssItems.filter(item => item !== null);
|
|
|
|
for (const item of validItems) {
|
|
// 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 (!item) return false;
|
|
|
|
if (!filters || !Array.isArray(filters) || filters.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
// Check if the item matches any of the filters
|
|
return filters.some(filter => {
|
|
if (!filter) return true;
|
|
|
|
// Title check
|
|
if (filter.title && typeof item.title === 'string' &&
|
|
!item.title.toLowerCase().includes(filter.title.toLowerCase())) {
|
|
return false;
|
|
}
|
|
|
|
// Category check
|
|
if (filter.category && typeof item.category === 'string' &&
|
|
!item.category.toLowerCase().includes(filter.category.toLowerCase())) {
|
|
return false;
|
|
}
|
|
|
|
// Size checks
|
|
if (filter.minSize && typeof item.size === 'number' && item.size < filter.minSize) {
|
|
return false;
|
|
}
|
|
|
|
if (filter.maxSize && typeof item.size === 'number' && item.size > filter.maxSize) {
|
|
return false;
|
|
}
|
|
|
|
// All checks passed
|
|
return true;
|
|
});
|
|
}
|
|
|
|
queueItemForDownload(item) {
|
|
if (!item) return;
|
|
|
|
// 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 {
|
|
// Ensure data directory exists
|
|
await this.ensureDataDirectory();
|
|
|
|
// 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 saveFeeds() {
|
|
try {
|
|
// Ensure data directory exists
|
|
await this.ensureDataDirectory();
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensures the data directory exists, using a consistent approach across the application
|
|
* @returns {Promise<boolean>} true if directory exists or was created
|
|
*/
|
|
async ensureDataDirectory() {
|
|
try {
|
|
// Create data directory with recursive option (creates all parent directories if they don't exist)
|
|
await fs.mkdir(this.dataPath, { recursive: true });
|
|
console.log(`Ensured data directory exists at: ${this.dataPath}`);
|
|
return true;
|
|
} catch (error) {
|
|
// Log the error details for debugging
|
|
console.error(`Error creating data directory ${this.dataPath}:`, error);
|
|
|
|
// Try an alternate approach if the first method fails
|
|
try {
|
|
const { execSync } = require('child_process');
|
|
execSync(`mkdir -p "${this.dataPath}"`);
|
|
console.log(`Created data directory using fallback method: ${this.dataPath}`);
|
|
return true;
|
|
} catch (fallbackError) {
|
|
console.error('All methods for creating data directory failed:', fallbackError);
|
|
throw new Error(`Failed to create data directory: ${this.dataPath}. Original error: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
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');
|
|
this.items = [];
|
|
return false;
|
|
}
|
|
|
|
// Load items from file
|
|
const data = await fs.readFile(filePath, 'utf8');
|
|
|
|
if (!data || data.trim() === '') {
|
|
console.log('Empty RSS items file');
|
|
this.items = [];
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const items = JSON.parse(data);
|
|
|
|
if (Array.isArray(items)) {
|
|
this.items = items;
|
|
console.log(`Loaded ${this.items.length} RSS items from disk`);
|
|
return true;
|
|
} else {
|
|
console.error('RSS items file does not contain an array');
|
|
this.items = [];
|
|
return false;
|
|
}
|
|
} catch (parseError) {
|
|
console.error('Error parsing RSS items JSON:', parseError);
|
|
this.items = [];
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading RSS items:', error);
|
|
this.items = [];
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async loadFeeds() {
|
|
try {
|
|
const filePath = path.join(this.dataPath, 'rss-feeds.json');
|
|
|
|
// Check if file exists
|
|
try {
|
|
await fs.access(filePath);
|
|
} catch (error) {
|
|
console.log('No saved RSS feeds found, using config feeds');
|
|
return false;
|
|
}
|
|
|
|
// Load feeds from file
|
|
const data = await fs.readFile(filePath, 'utf8');
|
|
|
|
if (!data || data.trim() === '') {
|
|
console.log('Empty RSS feeds file, using config feeds');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const feeds = JSON.parse(data);
|
|
|
|
if (Array.isArray(feeds)) {
|
|
this.feeds = feeds;
|
|
console.log(`Loaded ${this.feeds.length} RSS feeds from disk`);
|
|
return true;
|
|
} else {
|
|
console.error('RSS feeds file does not contain an array');
|
|
return false;
|
|
}
|
|
} catch (parseError) {
|
|
console.error('Error parsing RSS feeds JSON:', parseError);
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading RSS feeds:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Public API methods
|
|
|
|
getAllFeeds() {
|
|
return Array.isArray(this.feeds) ? this.feeds : [];
|
|
}
|
|
|
|
addFeed(feedData) {
|
|
if (!feedData || !feedData.url) {
|
|
throw new Error('Feed URL is required');
|
|
}
|
|
|
|
// Generate an ID for the feed
|
|
const id = crypto.randomBytes(8).toString('hex');
|
|
|
|
const newFeed = {
|
|
id,
|
|
name: feedData.name || 'Unnamed Feed',
|
|
url: feedData.url,
|
|
autoDownload: !!feedData.autoDownload,
|
|
filters: Array.isArray(feedData.filters) ? feedData.filters : [],
|
|
added: new Date().toISOString()
|
|
};
|
|
|
|
if (!Array.isArray(this.feeds)) {
|
|
this.feeds = [];
|
|
}
|
|
|
|
this.feeds.push(newFeed);
|
|
|
|
// Save the updated feeds
|
|
this.saveFeeds().catch(err => {
|
|
console.error('Error saving feeds after adding new feed:', err);
|
|
});
|
|
|
|
console.log(`Added new feed: ${newFeed.name} (${newFeed.url})`);
|
|
|
|
return newFeed;
|
|
}
|
|
|
|
updateFeedConfig(feedId, updates) {
|
|
if (!feedId || !updates) {
|
|
return false;
|
|
}
|
|
|
|
if (!Array.isArray(this.feeds)) {
|
|
console.error('Feeds is not an array');
|
|
return false;
|
|
}
|
|
|
|
const feedIndex = this.feeds.findIndex(f => f && f.id === feedId);
|
|
|
|
if (feedIndex === -1) {
|
|
console.error(`Feed with ID ${feedId} not found`);
|
|
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
|
|
};
|
|
|
|
// Save the updated feeds
|
|
this.saveFeeds().catch(err => {
|
|
console.error('Error saving feeds after updating feed:', err);
|
|
});
|
|
|
|
console.log(`Updated feed: ${this.feeds[feedIndex].name}`);
|
|
|
|
return true;
|
|
}
|
|
|
|
removeFeed(feedId) {
|
|
if (!feedId || !Array.isArray(this.feeds)) {
|
|
return false;
|
|
}
|
|
|
|
const initialLength = this.feeds.length;
|
|
this.feeds = this.feeds.filter(f => f && f.id !== feedId);
|
|
|
|
if (this.feeds.length !== initialLength) {
|
|
// Save the updated feeds
|
|
this.saveFeeds().catch(err => {
|
|
console.error('Error saving feeds after removing feed:', err);
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
getAllItems() {
|
|
return Array.isArray(this.items) ? this.items : [];
|
|
}
|
|
|
|
getUndownloadedItems() {
|
|
if (!Array.isArray(this.items)) {
|
|
return [];
|
|
}
|
|
return this.items.filter(item => item && !item.downloaded && !item.ignored);
|
|
}
|
|
|
|
filterItems(filters) {
|
|
if (!filters || !Array.isArray(this.items)) {
|
|
return [];
|
|
}
|
|
return this.items.filter(item => 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, async (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
|
|
try {
|
|
await 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; |