1070 lines
27 KiB
JavaScript
1070 lines
27 KiB
JavaScript
/**
|
|
* Transmission RSS Manager
|
|
* Main Server Application
|
|
*/
|
|
|
|
const express = require('express');
|
|
const path = require('path');
|
|
const fs = require('fs').promises;
|
|
const bodyParser = require('body-parser');
|
|
const cors = require('cors');
|
|
const morgan = require('morgan');
|
|
const http = require('http');
|
|
const https = require('https');
|
|
const jwt = require('jsonwebtoken');
|
|
const bcrypt = require('bcrypt');
|
|
|
|
// Import custom modules
|
|
const RssFeedManager = require('./modules/rss-feed-manager');
|
|
const TransmissionClient = require('./modules/transmission-client');
|
|
const PostProcessor = require('./modules/post-processor');
|
|
|
|
// Constants and configuration
|
|
const DEFAULT_CONFIG_PATH = path.join(__dirname, 'config.json');
|
|
const DEFAULT_PORT = 3000;
|
|
const JWT_SECRET = process.env.JWT_SECRET || 'transmission-rss-manager-secret';
|
|
const JWT_EXPIRY = '24h';
|
|
|
|
// Create Express app
|
|
const app = express();
|
|
|
|
// Middleware
|
|
app.use(cors());
|
|
app.use(bodyParser.json());
|
|
app.use(morgan('combined'));
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
|
|
// Application state
|
|
let config = null;
|
|
let transmissionClient = null;
|
|
let rssFeedManager = null;
|
|
let postProcessor = null;
|
|
let isInitialized = false;
|
|
let server = null;
|
|
|
|
/**
|
|
* Initialize the application with configuration
|
|
*/
|
|
async function initializeApp() {
|
|
try {
|
|
// Load configuration
|
|
config = await loadConfig();
|
|
console.log('Configuration loaded');
|
|
|
|
// Initialize transmission client
|
|
transmissionClient = new TransmissionClient(config);
|
|
console.log('Transmission client initialized');
|
|
|
|
// Initialize RSS feed manager
|
|
rssFeedManager = new RssFeedManager(config);
|
|
await rssFeedManager.start();
|
|
console.log('RSS feed manager started');
|
|
|
|
// Initialize post processor
|
|
postProcessor = new PostProcessor(config, transmissionClient);
|
|
|
|
// Start post processor if auto-processing is enabled
|
|
if (config.autoProcessing) {
|
|
postProcessor.start();
|
|
console.log('Post-processor auto-started');
|
|
} else {
|
|
console.log('Post-processor not auto-starting (autoProcessing disabled)');
|
|
}
|
|
|
|
isInitialized = true;
|
|
console.log('Application initialized successfully');
|
|
} catch (error) {
|
|
console.error('Failed to initialize application:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load configuration from file
|
|
* @returns {Promise<Object>} Configuration object
|
|
*/
|
|
async function loadConfig() {
|
|
try {
|
|
// Define default configuration
|
|
const defaultConfig = {
|
|
version: '1.2.0',
|
|
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',
|
|
magazines: '/mnt/media/magazines',
|
|
software: '/mnt/media/software'
|
|
},
|
|
seedingRequirements: {
|
|
minRatio: 1.0,
|
|
minTimeMinutes: 60,
|
|
checkIntervalSeconds: 300
|
|
},
|
|
processingOptions: {
|
|
enableBookSorting: true,
|
|
extractArchives: true,
|
|
deleteArchives: true,
|
|
createCategoryFolders: true,
|
|
ignoreSample: true,
|
|
ignoreExtras: true,
|
|
renameFiles: true,
|
|
autoReplaceUpgrades: true,
|
|
removeDuplicates: true,
|
|
keepOnlyBestVersion: true
|
|
},
|
|
rssFeeds: [],
|
|
rssUpdateIntervalMinutes: 60,
|
|
autoProcessing: false,
|
|
securitySettings: {
|
|
authEnabled: false,
|
|
httpsEnabled: false,
|
|
sslCertPath: "",
|
|
sslKeyPath: "",
|
|
users: []
|
|
},
|
|
port: DEFAULT_PORT,
|
|
logLevel: "info"
|
|
};
|
|
|
|
try {
|
|
// Try to read existing config
|
|
const configData = await fs.readFile(DEFAULT_CONFIG_PATH, 'utf8');
|
|
const loadedConfig = JSON.parse(configData);
|
|
|
|
// Use recursive merge function to merge configs
|
|
const mergedConfig = mergeConfigs(defaultConfig, loadedConfig);
|
|
|
|
// If version is different, save updated config
|
|
if (loadedConfig.version !== defaultConfig.version) {
|
|
console.log(`Updating config from version ${loadedConfig.version || 'unknown'} to ${defaultConfig.version}`);
|
|
mergedConfig.version = defaultConfig.version;
|
|
await saveConfig(mergedConfig);
|
|
}
|
|
|
|
return mergedConfig;
|
|
} catch (readError) {
|
|
// If file not found, create default config
|
|
if (readError.code === 'ENOENT') {
|
|
console.log('Config file not found, creating default configuration');
|
|
await saveConfig(defaultConfig);
|
|
return defaultConfig;
|
|
}
|
|
|
|
// For other errors, rethrow
|
|
throw readError;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading configuration:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursively merge configurations
|
|
* @param {Object} defaultConfig - Default configuration object
|
|
* @param {Object} userConfig - User configuration object
|
|
* @returns {Object} Merged configuration object
|
|
*/
|
|
function mergeConfigs(defaultConfig, userConfig) {
|
|
// Create a new object to avoid modifying the originals
|
|
const merged = {};
|
|
|
|
// Add all properties from default config
|
|
for (const key in defaultConfig) {
|
|
// If property exists in user config
|
|
if (key in userConfig) {
|
|
// If both are objects (not arrays or null), recursively merge
|
|
if (
|
|
typeof defaultConfig[key] === 'object' &&
|
|
defaultConfig[key] !== null &&
|
|
!Array.isArray(defaultConfig[key]) &&
|
|
typeof userConfig[key] === 'object' &&
|
|
userConfig[key] !== null &&
|
|
!Array.isArray(userConfig[key])
|
|
) {
|
|
merged[key] = mergeConfigs(defaultConfig[key], userConfig[key]);
|
|
} else {
|
|
// Use user value
|
|
merged[key] = userConfig[key];
|
|
}
|
|
} else {
|
|
// Property doesn't exist in user config, use default
|
|
merged[key] = defaultConfig[key];
|
|
}
|
|
}
|
|
|
|
// Add any properties from user config that aren't in default
|
|
for (const key in userConfig) {
|
|
if (!(key in defaultConfig)) {
|
|
merged[key] = userConfig[key];
|
|
}
|
|
}
|
|
|
|
return merged;
|
|
}
|
|
|
|
/**
|
|
* Save configuration to file
|
|
* @param {Object} config - Configuration object
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async function saveConfig(config) {
|
|
try {
|
|
await fs.writeFile(DEFAULT_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
|
|
console.log('Configuration saved');
|
|
} catch (error) {
|
|
console.error('Error saving configuration:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start the server
|
|
*/
|
|
async function startServer() {
|
|
try {
|
|
// Initialize the application
|
|
await initializeApp();
|
|
|
|
// Determine port
|
|
const port = config.port || process.env.PORT || DEFAULT_PORT;
|
|
|
|
// Create server based on configuration
|
|
if (config.securitySettings?.httpsEnabled &&
|
|
config.securitySettings?.sslCertPath &&
|
|
config.securitySettings?.sslKeyPath) {
|
|
try {
|
|
const sslOptions = {
|
|
key: await fs.readFile(config.securitySettings.sslKeyPath),
|
|
cert: await fs.readFile(config.securitySettings.sslCertPath)
|
|
};
|
|
|
|
server = https.createServer(sslOptions, app);
|
|
console.log('HTTPS server created');
|
|
} catch (error) {
|
|
console.error('Error creating HTTPS server, falling back to HTTP:', error);
|
|
server = http.createServer(app);
|
|
}
|
|
} else {
|
|
server = http.createServer(app);
|
|
console.log('HTTP server created');
|
|
}
|
|
|
|
// Start the server
|
|
server.listen(port, () => {
|
|
console.log(`Transmission RSS Manager server running on port ${port}`);
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to start server:', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Authentication middleware
|
|
function authenticateJWT(req, res, next) {
|
|
// Skip authentication if not enabled
|
|
if (!config.securitySettings?.authEnabled) {
|
|
return next();
|
|
}
|
|
|
|
// Get token from header
|
|
const authHeader = req.headers.authorization;
|
|
if (!authHeader) {
|
|
return res.status(401).json({ success: false, message: 'Authentication required' });
|
|
}
|
|
|
|
const token = authHeader.split(' ')[1];
|
|
if (!token) {
|
|
return res.status(401).json({ success: false, message: 'Authentication token required' });
|
|
}
|
|
|
|
// Verify token
|
|
jwt.verify(token, JWT_SECRET, (err, user) => {
|
|
if (err) {
|
|
return res.status(403).json({ success: false, message: 'Invalid or expired token' });
|
|
}
|
|
|
|
req.user = user;
|
|
next();
|
|
});
|
|
}
|
|
|
|
// API Routes
|
|
|
|
// Status endpoint
|
|
app.get('/api/status', authenticateJWT, async (req, res) => {
|
|
try {
|
|
if (!isInitialized) {
|
|
return res.status(503).json({
|
|
success: false,
|
|
message: 'Application is initializing, please try again'
|
|
});
|
|
}
|
|
|
|
// Get Transmission connection status
|
|
const transmissionStatus = await transmissionClient.getStatus();
|
|
|
|
// Return status information
|
|
res.json({
|
|
success: true,
|
|
status: 'running',
|
|
version: '1.2.0',
|
|
transmissionConnected: transmissionStatus.connected,
|
|
transmissionVersion: transmissionStatus.version,
|
|
transmissionStats: {
|
|
downloadSpeed: transmissionStatus.downloadSpeed,
|
|
uploadSpeed: transmissionStatus.uploadSpeed,
|
|
activeTorrentCount: transmissionStatus.activeTorrentCount,
|
|
torrentCount: transmissionStatus.torrentCount
|
|
},
|
|
rssFeedManagerActive: !!rssFeedManager.updateIntervalId,
|
|
postProcessorActive: !!postProcessor.processIntervalId,
|
|
config: {
|
|
autoProcessing: config.autoProcessing,
|
|
rssEnabled: Array.isArray(config.rssFeeds) && config.rssFeeds.length > 0
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Error getting status:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Error getting status: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
// Configuration endpoint
|
|
app.get('/api/config', authenticateJWT, (req, res) => {
|
|
// Return configuration (remove sensitive information)
|
|
const sanitizedConfig = { ...config };
|
|
|
|
// Remove passwords from response
|
|
if (sanitizedConfig.transmissionConfig) {
|
|
delete sanitizedConfig.transmissionConfig.password;
|
|
}
|
|
|
|
if (sanitizedConfig.securitySettings?.users) {
|
|
sanitizedConfig.securitySettings.users = sanitizedConfig.securitySettings.users.map(user => ({
|
|
...user,
|
|
password: undefined
|
|
}));
|
|
}
|
|
|
|
res.json(sanitizedConfig);
|
|
});
|
|
|
|
app.post('/api/config', authenticateJWT, async (req, res) => {
|
|
try {
|
|
// Merge the new config with the existing one
|
|
const newConfig = { ...config, ...req.body };
|
|
|
|
// Keep passwords if they're not provided
|
|
if (newConfig.transmissionConfig && !newConfig.transmissionConfig.password && config.transmissionConfig) {
|
|
newConfig.transmissionConfig.password = config.transmissionConfig.password;
|
|
}
|
|
|
|
// Keep user passwords
|
|
if (newConfig.securitySettings?.users && config.securitySettings?.users) {
|
|
newConfig.securitySettings.users = newConfig.securitySettings.users.map(newUser => {
|
|
const existingUser = config.securitySettings.users.find(u => u.username === newUser.username);
|
|
|
|
if (existingUser && !newUser.password) {
|
|
return { ...newUser, password: existingUser.password };
|
|
}
|
|
|
|
return newUser;
|
|
});
|
|
}
|
|
|
|
// Save the updated config
|
|
await saveConfig(newConfig);
|
|
|
|
// Update the application state
|
|
config = newConfig;
|
|
|
|
// Restart components if necessary
|
|
if (rssFeedManager) {
|
|
rssFeedManager.stop();
|
|
rssFeedManager = new RssFeedManager(config);
|
|
await rssFeedManager.start();
|
|
}
|
|
|
|
if (transmissionClient) {
|
|
transmissionClient = new TransmissionClient(config);
|
|
}
|
|
|
|
if (postProcessor) {
|
|
postProcessor.stop();
|
|
postProcessor = new PostProcessor(config, transmissionClient);
|
|
|
|
if (config.autoProcessing) {
|
|
postProcessor.start();
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Configuration updated successfully'
|
|
});
|
|
} catch (error) {
|
|
console.error('Error updating configuration:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Error updating configuration: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
// RSS Feed API
|
|
app.get('/api/rss/feeds', authenticateJWT, (req, res) => {
|
|
try {
|
|
const feeds = rssFeedManager.getAllFeeds();
|
|
res.json({
|
|
success: true,
|
|
data: feeds
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Error getting RSS feeds: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
app.post('/api/rss/feeds', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const feed = req.body;
|
|
const newFeed = rssFeedManager.addFeed(feed);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'RSS feed added successfully',
|
|
data: newFeed
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Error adding RSS feed: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
app.put('/api/rss/feeds/:id', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const feedId = req.params.id;
|
|
const updates = req.body;
|
|
|
|
const result = rssFeedManager.updateFeedConfig(feedId, updates);
|
|
|
|
if (result) {
|
|
res.json({
|
|
success: true,
|
|
message: 'RSS feed updated successfully'
|
|
});
|
|
} else {
|
|
res.status(404).json({
|
|
success: false,
|
|
message: 'RSS feed not found'
|
|
});
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Error updating RSS feed: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
app.delete('/api/rss/feeds/:id', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const feedId = req.params.id;
|
|
const result = rssFeedManager.removeFeed(feedId);
|
|
|
|
if (result) {
|
|
res.json({
|
|
success: true,
|
|
message: 'RSS feed deleted successfully'
|
|
});
|
|
} else {
|
|
res.status(404).json({
|
|
success: false,
|
|
message: 'RSS feed not found'
|
|
});
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Error deleting RSS feed: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
app.get('/api/rss/items', authenticateJWT, (req, res) => {
|
|
try {
|
|
let items;
|
|
const filter = req.query.filter;
|
|
|
|
if (filter === 'undownloaded') {
|
|
items = rssFeedManager.getUndownloadedItems();
|
|
} else {
|
|
items = rssFeedManager.getAllItems();
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: items
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Error getting RSS items: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
app.post('/api/rss/download', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { itemId } = req.body;
|
|
|
|
if (!itemId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Item ID is required'
|
|
});
|
|
}
|
|
|
|
// Find the item
|
|
const items = rssFeedManager.getAllItems();
|
|
const item = items.find(i => i.id === itemId);
|
|
|
|
if (!item) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Item not found'
|
|
});
|
|
}
|
|
|
|
// Download the item
|
|
const result = await rssFeedManager.downloadItem(item, transmissionClient.client);
|
|
|
|
if (result.success) {
|
|
res.json({
|
|
success: true,
|
|
message: 'Item added to Transmission',
|
|
data: result.result
|
|
});
|
|
} else {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: result.message
|
|
});
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Error downloading item: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
app.post('/api/rss/update', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const result = await rssFeedManager.updateAllFeeds();
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'RSS feeds updated successfully',
|
|
data: result
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Error updating RSS feeds: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
// Transmission API
|
|
app.get('/api/transmission/torrents', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const result = await transmissionClient.getTorrents();
|
|
|
|
if (result.success) {
|
|
res.json({
|
|
success: true,
|
|
data: result.torrents
|
|
});
|
|
} else {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: result.error
|
|
});
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Error getting torrents: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
app.post('/api/transmission/add', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { url } = req.body;
|
|
|
|
if (!url) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'URL is required'
|
|
});
|
|
}
|
|
|
|
const result = await transmissionClient.addTorrent(url);
|
|
|
|
if (result.success) {
|
|
res.json({
|
|
success: true,
|
|
message: 'Torrent added successfully',
|
|
data: result
|
|
});
|
|
} else {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: result.error
|
|
});
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Error adding torrent: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
app.post('/api/transmission/start', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { ids } = req.body;
|
|
|
|
if (!ids) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Torrent ID(s) required'
|
|
});
|
|
}
|
|
|
|
const result = await transmissionClient.startTorrents(ids);
|
|
|
|
if (result.success) {
|
|
res.json({
|
|
success: true,
|
|
message: result.message
|
|
});
|
|
} else {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: result.error
|
|
});
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Error starting torrent: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
app.post('/api/transmission/stop', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { ids } = req.body;
|
|
|
|
if (!ids) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Torrent ID(s) required'
|
|
});
|
|
}
|
|
|
|
const result = await transmissionClient.stopTorrents(ids);
|
|
|
|
if (result.success) {
|
|
res.json({
|
|
success: true,
|
|
message: result.message
|
|
});
|
|
} else {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: result.error
|
|
});
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Error stopping torrent: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
app.post('/api/transmission/remove', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { ids, deleteLocalData } = req.body;
|
|
|
|
if (!ids) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Torrent ID(s) required'
|
|
});
|
|
}
|
|
|
|
const result = await transmissionClient.removeTorrents(ids, deleteLocalData);
|
|
|
|
if (result.success) {
|
|
res.json({
|
|
success: true,
|
|
message: result.message
|
|
});
|
|
} else {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: result.error
|
|
});
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Error removing torrent: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
app.post('/api/transmission/test', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { host, port, username, password } = req.body;
|
|
|
|
// Create a temporary client for testing
|
|
const testConfig = {
|
|
transmissionConfig: {
|
|
host: host || config.transmissionConfig.host,
|
|
port: port || config.transmissionConfig.port,
|
|
username: username || config.transmissionConfig.username,
|
|
password: password || config.transmissionConfig.password,
|
|
path: config.transmissionConfig.path
|
|
}
|
|
};
|
|
|
|
const testClient = new TransmissionClient(testConfig);
|
|
const status = await testClient.getStatus();
|
|
|
|
if (status.connected) {
|
|
res.json({
|
|
success: true,
|
|
message: 'Successfully connected to Transmission server',
|
|
data: {
|
|
version: status.version,
|
|
rpcVersion: status.rpcVersion
|
|
}
|
|
});
|
|
} else {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: `Failed to connect: ${status.error}`
|
|
});
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Connection test failed: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
// Post-Processor API
|
|
app.post('/api/post-processor/start', authenticateJWT, (req, res) => {
|
|
try {
|
|
const result = postProcessor.start();
|
|
|
|
if (result) {
|
|
res.json({
|
|
success: true,
|
|
message: 'Post-processor started successfully'
|
|
});
|
|
} else {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: 'Post-processor is already running'
|
|
});
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Error starting post-processor: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
app.post('/api/post-processor/stop', authenticateJWT, (req, res) => {
|
|
try {
|
|
const result = postProcessor.stop();
|
|
|
|
if (result) {
|
|
res.json({
|
|
success: true,
|
|
message: 'Post-processor stopped successfully'
|
|
});
|
|
} else {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: 'Post-processor is not running'
|
|
});
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Error stopping post-processor: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
// Media Library API
|
|
app.get('/api/media/library', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const query = req.query.query;
|
|
const library = await getMediaLibrary(query);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: library
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Error getting media library: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Get media library content
|
|
* @param {string} searchQuery - Optional search query
|
|
* @returns {Promise<Object>} Media library content
|
|
*/
|
|
async function getMediaLibrary(searchQuery) {
|
|
const library = {
|
|
movies: [],
|
|
tvShows: [],
|
|
music: [],
|
|
books: [],
|
|
magazines: [],
|
|
software: []
|
|
};
|
|
|
|
// Get destination paths from config
|
|
const destinations = config.destinationPaths || {};
|
|
|
|
// Process each category
|
|
for (const [category, destinationPath] of Object.entries(destinations)) {
|
|
if (!destinationPath || typeof destinationPath !== 'string') {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
// Check if directory exists
|
|
await fs.access(destinationPath);
|
|
|
|
// Get directory listing
|
|
const files = await fs.readdir(destinationPath, { withFileTypes: true });
|
|
|
|
// Process each file/directory
|
|
for (const file of files) {
|
|
const fullPath = path.join(destinationPath, file.name);
|
|
|
|
try {
|
|
const stats = await fs.stat(fullPath);
|
|
|
|
// Create an item object
|
|
const item = {
|
|
name: file.name,
|
|
path: fullPath,
|
|
isDirectory: stats.isDirectory(),
|
|
size: stats.size,
|
|
added: stats.ctime.toISOString()
|
|
};
|
|
|
|
// Filter by search query if provided
|
|
if (searchQuery && !file.name.toLowerCase().includes(searchQuery.toLowerCase())) {
|
|
continue;
|
|
}
|
|
|
|
// Add to the appropriate category
|
|
switch (category) {
|
|
case 'movies':
|
|
library.movies.push(item);
|
|
break;
|
|
case 'tvShows':
|
|
library.tvShows.push(item);
|
|
break;
|
|
case 'music':
|
|
library.music.push(item);
|
|
break;
|
|
case 'books':
|
|
library.books.push(item);
|
|
break;
|
|
case 'magazines':
|
|
library.magazines.push(item);
|
|
break;
|
|
case 'software':
|
|
library.software.push(item);
|
|
break;
|
|
}
|
|
} catch (itemError) {
|
|
console.error(`Error processing media library item ${fullPath}:`, itemError);
|
|
}
|
|
}
|
|
|
|
// Sort by most recently added
|
|
const sortKey = category === 'tvShows' ? 'tvShows' : category;
|
|
if (Array.isArray(library[sortKey])) {
|
|
library[sortKey].sort((a, b) => new Date(b.added) - new Date(a.added));
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error accessing media directory ${destinationPath}:`, error);
|
|
}
|
|
}
|
|
|
|
return library;
|
|
}
|
|
|
|
// Authentication API
|
|
app.post('/api/auth/login', async (req, res) => {
|
|
try {
|
|
// Skip authentication if not enabled
|
|
if (!config.securitySettings?.authEnabled) {
|
|
return res.json({
|
|
success: true,
|
|
token: jwt.sign({ username: 'admin' }, JWT_SECRET, { expiresIn: JWT_EXPIRY }),
|
|
user: { username: 'admin', role: 'admin' }
|
|
});
|
|
}
|
|
|
|
const { username, password } = req.body;
|
|
|
|
if (!username || !password) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Username and password are required'
|
|
});
|
|
}
|
|
|
|
// Find user
|
|
const users = config.securitySettings?.users || [];
|
|
const user = users.find(u => u.username === username);
|
|
|
|
if (!user) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
message: 'Invalid credentials'
|
|
});
|
|
}
|
|
|
|
// Check password
|
|
let passwordMatch = false;
|
|
|
|
if (user.password.startsWith('$2')) {
|
|
// Bcrypt hashed password
|
|
passwordMatch = await bcrypt.compare(password, user.password);
|
|
} else {
|
|
// Legacy plain text password
|
|
passwordMatch = password === user.password;
|
|
|
|
// Update to hashed password
|
|
if (passwordMatch) {
|
|
user.password = await bcrypt.hash(password, 10);
|
|
await saveConfig(config);
|
|
}
|
|
}
|
|
|
|
if (!passwordMatch) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
message: 'Invalid credentials'
|
|
});
|
|
}
|
|
|
|
// Generate token
|
|
const token = jwt.sign(
|
|
{ username: user.username, role: user.role || 'user' },
|
|
JWT_SECRET,
|
|
{ expiresIn: JWT_EXPIRY }
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
token,
|
|
user: {
|
|
username: user.username,
|
|
role: user.role || 'user'
|
|
}
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: `Authentication error: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
app.get('/api/auth/validate', authenticateJWT, (req, res) => {
|
|
res.json({
|
|
success: true,
|
|
user: req.user
|
|
});
|
|
});
|
|
|
|
// Catch-all route for SPA
|
|
app.get('*', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
});
|
|
|
|
// Start the server
|
|
startServer();
|
|
|
|
// Handle process termination
|
|
process.on('SIGTERM', () => {
|
|
console.log('SIGTERM received, shutting down gracefully');
|
|
|
|
if (server) {
|
|
server.close(() => {
|
|
console.log('Server closed');
|
|
process.exit(0);
|
|
});
|
|
} else {
|
|
process.exit(0);
|
|
}
|
|
});
|
|
|
|
process.on('SIGINT', () => {
|
|
console.log('SIGINT received, shutting down gracefully');
|
|
|
|
if (server) {
|
|
server.close(() => {
|
|
console.log('Server closed');
|
|
process.exit(0);
|
|
});
|
|
} else {
|
|
process.exit(0);
|
|
}
|
|
});
|
|
|
|
module.exports = app; |