48 Commits

Author SHA1 Message Date
897862184f fix: Remove persistent floating update notification
- Completely removed floating notification element from DOM
- Fixed issue where notification would not disappear
- Added aggressive cleanup of localStorage to prevent state persistence
- Implemented DOM observer to prevent notification reappearance
- Simplified update alert system to use dashboard-only notifications
- Updated version to 2.0.12

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 18:55:42 +00:00
90a6e5e16b Fix version mismatch in test mode update notification
- Updated test mode to use current version number from system status
- Added version synchronization between displayed and stored values
- Enhanced forceShowUpdateButton to use the most current version
- Added version comparison to detect and fix mismatches
- Used the displayed version as the source of truth
- Implemented automatic version correction in localStorage
- Fixed hardcoded version number in test mode
- Improved version consistency across the application

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 18:49:03 +00:00
cbae1d57fe Bump version to 2.0.11 with updated documentation
- Updated version number to 2.0.11 in package.json
- Updated README.md with changelog for 2.0.11
- Updated About modal in index.html to show current version
- Added detailed version history entry in About modal
- Updated copyright and version information
- Enhanced documentation with complete feature list

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 18:46:13 +00:00
d2d2ea976b Fix conflict between test mode and actual update status
- Added automatic test mode disabling when update attempt is made
- Added clear test mode indicator in the floating notification
- Implemented automatic test mode disabling when real update check shows no update
- Added visual distinction between test and real update notifications
- Added warning message for test mode to prevent confusion
- Updated update status text to clearly indicate when in test mode
- Fixed potential conflict in test toggle handler
- Improved usability with clearer notification messages

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 18:36:58 +00:00
2705989ff6 Fix version display with direct package.json reading
- Added direct package.json file reading instead of require caching
- Enhanced system status endpoint to always return current version
- Added manual refresh button to floating notification
- Implemented aggressive cache-busting for all version checks
- Added double-check system status after update completion
- Added detailed logging for version retrieval for debugging
- Improved error handling for package.json reading
- Added immediate user feedback for refresh operations

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 18:33:53 +00:00
8589a0833e Fix update status handling with cache busting and better response parsing
- Added detection of 'already have the latest version' scenario
- Implemented proper handling when no update is available with friendly message
- Added cache busting parameters to ensure fresh data from server
- Enhanced test mode toggle to properly check real update status
- Added specific handling for the case where update script runs but no update is needed
- Improved logging to show actual update check response
- Modified reload mechanics to force cache refresh with timestamp parameter
- Added special handling to avoid unnecessary page reloads

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 18:30:40 +00:00
467979971a Fix update process to properly handle completion
- Enhanced applyUpdate function to handle both original and floating buttons
- Added immediate update notification removal after successful update
- Implemented proper countdown display on both update buttons
- Added localStorage cleanup to ensure clean page reload
- Fixed error handling to re-enable both buttons on failure
- Improved page reload with forced clean URL (no hash fragments)
- Added consistent handling across success and error cases
- Created updateCountdown function for cleaner code reuse

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 18:27:32 +00:00
5ce348d61e Implement floating notification for update alerts
- Created an entirely new floating notification system independent of the main UI
- Positioned notification as fixed element attached directly to the body
- Used bright red styling with high contrast to ensure visibility
- Added redundant notification alongside original update alert
- Implemented try/catch blocks for robust error handling
- Added event listeners for update button in floating notification
- Enhanced force show function to handle both notification types
- Applied fixed position and high z-index to ensure visibility
- Added console logging for better debugging

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 18:24:29 +00:00
dc4131f04c Add extreme measures to ensure update button visibility
- Implemented an aggressive 1-second interval to force-check & show update button
- Added bold red styling with \!important flags to make button impossible to miss
- Implemented MutationObserver to detect & counteract any hiding attempts
- Added detailed console logging for debugging visibility issues
- Applied inline styles with \!important flags to override any CSS cascades
- Increased z-index to 9999 to ensure button appears above all other elements
- Set multiple CSS properties (display, visibility, opacity) to ensure visibility

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 17:51:01 +00:00
5261f7b4f4 Fix update button persistence with more robust implementation
- Completely redesigned update notification system with dedicated functions
- Added showUpdateAlert and hideUpdateAlert functions for better control
- Improved update status persistence with namespaced localStorage keys
- Added custom CSS styles with \!important to prevent style overrides
- Modified toggle test functionality to directly show/hide the update alert
- Prevented update notifications from being cleared on errors
- Added forced display styles and higher z-index for visibility
- Added debugging logs to verify update detection

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 17:38:27 +00:00
b8818a9bec Fix browser compatibility and update button persistence
- Replaced AbortSignal.timeout() with AbortController for broader browser support
- Fixed promise chaining for proper cleanup of timeout handlers
- Added localStorage persistence for update information
- Made update button display more reliable with forced display styles
- Added ID to version elements for easier targeting
- Improved version number synchronization across UI elements

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 17:35:58 +00:00
3ff0a50553 Fix dynamic version display and update button issues
- Updated footer version to dynamically display current running version
- Fixed update button disappearing when refreshing status
- Added version tracking to prevent update button from hiding
- Updated About modal to show current version and added v2.0.10 to version history
- Fixed error handling in update check process

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 17:28:50 +00:00
c0a7362226 Fix fs references in server.js
- Updated all fs.readFile, fs.writeFile, fs.mkdir and other fs calls to use fsPromises instead
- Resolved conflict between renamed fs import and function calls
- Ensures consistent use of Promise-based fs API throughout the codebase

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 17:25:25 +00:00
dd08278e28 Fix fsPromises import in server.js
- Fixed import for fsPromises to ensure correct module is available
- Added explicit import for regular fs module for synchronous operations

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 17:24:05 +00:00
980a6ca3a4 Fix fs.existsSync error in update check
- Fixed TypeError: fs.existsSync is not a function error by using fsPromises instead of fs
- Updated version to 2.0.10
- Updated README with changelog
- Added better error handling for git repository checks
- Improved file system operations for update detection

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 17:23:07 +00:00
1ff479a3cf Fix fs.existsSync error in update check
- Correctly import both fs and fs.promises modules
- Update all fs method calls to use appropriate module
- Fix update check error by using consistent promise-based fs API

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 17:06:44 +00:00
313c85ee4b Fix Transmission installation detection in update mode
- Skip Transmission daemon installation prompt when in update mode
- Properly detect remote/local status from existing config file
- Add better logging for Transmission configuration detection

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 17:04:13 +00:00
eaed045323 Fix configuration detection in update mode
- Skip Transmission configuration prompt when updating an existing installation
- Automatically detect remote/local setting from existing config
- Properly export update-related variables to environment file
- Fix indentation issues in conditional statements

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 17:01:51 +00:00
2dcd4becef Fix update functionality and improve documentation
- Fixed update detection in install scripts
- Added Git availability checks to update system
- Improved error handling for update endpoint
- Added detailed Git requirements to README
- Added troubleshooting section for update issues

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 16:57:47 +00:00
9b45e669e2 Fix installation issue with missing server.js
Fixed a critical installation issue where server.js and server-endpoints.js were not being
copied to the install directory. This caused the service to fail with 'Cannot find module'
errors when trying to start.

Changes:
- Updated copy_module_files in file-creator-module.sh to also copy main server files
- Added error handling if server.js is missing
- Added better logging during file copying

💡 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-07 11:51:09 +00:00
f2b217ad84 Fix syntax error in main-installer.sh
Fixed a syntax error in the update script setup section of main-installer.sh:
- Fixed incorrect brace closure (using '}' instead of 'fi')
- Fixed indentation for better code readability
- Ensured proper nesting of if conditions
- This should resolve the error during installation when copying module files

💡 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-07 11:46:33 +00:00
5a1318bbf2 Fix syntax errors in utils-module.sh
Fixed multiple syntax errors in utils-module.sh:
- Replaced compound commands with  syntax with clearer if statements
- This addresses issues with bash syntax in older versions of bash
- Improved error handling with explicit 2 checks

💡 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-07 11:43:28 +00:00
3aee416cda Fix syntax error in utils-module.sh
Fixed a syntax error on line 134 in utils-module.sh that was causing installation to fail when setting up with a remote Transmission server.

💡 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-07 11:41:22 +00:00
6dc2df3cee Fix code consistency and reliability issues
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>
2025-03-07 11:38:14 +00:00
83222078d9 Fix npm installation issues during setup
- Created reusable ensure_npm_packages function for consistency
- Fixed npm install being called in wrong directory during installation
- Added proper directory context preservation for npm operations
- Ensured package.json file is always copied to installation directory
- Added checks to prevent redundant npm installations
- Improved error handling and reporting for npm operations

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-07 10:18:45 +00:00
16c73bca70 Fix module loading issues with require extension compatibility
- Added robust module loading in server.js with multiple fallback paths
- Created bidirectional symlinks for modules with different naming styles
- Added extension-less symlinks for Node.js CommonJS compatibility
- Updated file copying logic to create all necessary symlinks
- Added symlink creation script that runs on startup
- Improved module error reporting with detailed path information

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-07 10:15:47 +00:00
852de32907 Fix systemd service startup issues
- Updated test-and-start.sh to work with systemd services
- Added proper node executable path detection
- Fixed issue with shebang line in startup script
- Updated service module to use absolute paths correctly
- Improved robustness of startup script with better error handling

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-07 10:11:20 +00:00
35420335d7 Fix data directory creation issue on clean install
- Added improved data directory handling in RSSFeedManager
- Added synchronous creation of data directory in constructor
- Created test-and-start.sh script to ensure data directory exists
- Updated service module to use the startup script
- Added fallback methods for data directory creation

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-07 09:31:19 +00:00
302c75c534 Bump version to 2.0.9 and update README
- Updated version to 2.0.9
- Added new changelog entries for all fixes
- Documented remote connection fixes
- Documented update button and status endpoint additions

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-07 09:25:19 +00:00
8887f6fda1 Add system status and update endpoints
- Added /api/system/status endpoint to report application status
- Added /api/system/check-updates endpoint to check for updates via git
- Added /api/system/update endpoint for applying updates
- Fixed update button in dashboard
- Added proper error handling for git operations

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-07 09:24:27 +00:00
70ccb8f4fd Fix Transmission connection testing and API compatibility
- Updated TransmissionClient to use correct method names from transmission-promise
- Changed sessionGet to session() and sessionSet to sessionUpdate()
- Added robust error handling in connection test
- Improved logging for connection debugging
- Fixed error handling in TransmissionClient constructor

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-07 09:19:27 +00:00
301684886f Fix Transmission remote connection issues
- Prevent remote host from defaulting to localhost
- Preserve remote connection settings during config updates
- Handle empty values correctly to avoid overriding good config

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-07 09:16:28 +00:00
f28d49284e Fix module import issues on fresh installations
- Ensure server.js uses consistent .js extensions for module imports
- Create compatibility symlinks for different module naming styles
- Update file-creator-module.sh to handle module paths correctly
- Bump version to 2.0.8

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-07 09:13:02 +00:00
54871518fc Fix module import issue on fresh installs
- Added create_directories function to properly set up directory structure
- Added copy_module_files function to ensure JS modules are copied correctly
- Updated server.js to handle module imports more resiliently
- Fixed imports to work with both .js and no extension module references

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-07 08:59:01 +00:00
72d230706a Fix log command not found error in main-installer
- Move utils-module.sh sourcing before any log function calls
- Remove duplicate sourcing line

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-07 08:44:00 +00:00
0bce35d899 Fix installation directory handling and clarify defaults
- Fixed bootstrap-installer to prompt for installation directory with /opt/trans-install as default
- Updated main-installer to detect and use existing installation path from service file
- Modified config-module to use installation directory from environment or default to /opt/trans-install
- Updated README with clear information about the default installation path
- Bumped version to 2.0.7

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-07 08:38:55 +00:00
484a021936 Fix version check in server.js template with error handling
Added robust error handling for dynamic version retrieval in the server.js template:
- Added try/catch block around package.json require
- Added fallback version if package.json can't be loaded
- Ensures server will start even if there's an issue loading the version

This prevents connection errors related to the version check functionality.

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-05 10:33:01 +00:00
16a7c0c0b6 Make version number dynamic in server.js template
Instead of hardcoding the version number in the server.js template,
this change reads the version dynamically from package.json.
This ensures that the dashboard always displays the correct version
number without requiring manual updates to the template file.

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-05 10:28:36 +00:00
e7076859b7 Fix version display for fresh installs
The server.js template in file-creator-module.sh had a hardcoded version of 1.2.0,
causing fresh installations to display the wrong version number on the dashboard.
Updated to display 2.0.6.

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-05 10:26:50 +00:00
4196914fbd Update version number to 2.0.6 across codebase
- Fix version inconsistencies in server.js API status endpoint
- Update User-Agent string in RSS feed manager
- Update version in config file templates
- Update dashboard display with version 2.0.6
- Add version 2.0.6 to version history with changelog

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-05 10:16:48 +00:00
36ac18c998 Update README with known issue about remote config
- Adjust changelog to be more accurate about remote configuration
- Add 'Known Issues' section to README.md
- Document issue with remote settings not being applied in update mode
- Provide workaround by suggesting manual configuration via web interface
2025-03-05 10:03:25 +00:00
626b24d35e Bump version to 2.0.6 and update changelog
- Update version in package.json to 2.0.6
- Add changelog entries for the latest fixes:
  - Remote configuration settings properly collected and applied
  - Remote host and credentials now correctly stored in config
  - Non-interactive mode support for scripted installations
  - Better handling of piped input for remote configuration
  - Added debug output to help troubleshoot configuration issues
2025-03-05 10:02:42 +00:00
a5afa1bb80 Improve environment variable handling in main installer
- Add logic to set input_remote based on TRANSMISSION_REMOTE from environment
- Add detailed debug logging for environment variables
- Ensure consistent behavior between direct mode and piped input
- Fix issue where environment variables weren't correctly propagating

This ensures that when remote mode is selected in install-script.sh, it's correctly respected in the main installer even when running in update mode.
2025-03-05 10:02:23 +00:00
40878c7d3a Fix non-interactive mode for remote Transmission configuration
- Save all piped input to a temporary file for sequential processing
- Read remote mode selection from first line of input
- Use remaining lines for remote Transmission configuration details
- Add proper handling of defaults for empty or missing values
- Improve debug output for configuration parameters

This allows users to pipe all configuration values to the installer in a single command, without requiring interactive prompt responses.
2025-03-05 10:01:27 +00:00
588ba1ea01 Collect Transmission remote details in install-script.sh
- Move Transmission remote details collection to install-script.sh
- Pass all remote details to main-installer.sh via environment file
- Avoid duplicate prompting for remote details
- Improve non-interactive mode with proper default values
- Add check in main-installer.sh to use provided remote details

This solves the problem where remote Transmission details were collected in install-script.sh but not properly used in the main installer when updating an existing installation.
2025-03-05 10:00:25 +00:00
a3924912f1 Improve handling of non-interactive and piped input
- Add detection for non-interactive terminal (piped input)
- Properly read input from pipe in install-script.sh
- Use pre-set environment variables when available
- Add better fallbacks for script usage
- Fix syntax error in main installer script
- More detailed logging of input handling process

This ensures the installer can be used both interactively and in scripts/automated setups.
2025-03-05 09:59:41 +00:00
4e4fd09811 Fix input handling for remote Transmission selection
- Add debug logging to track input values
- Replace regex matching with direct string comparison for better reliability
- Add explicit checks for 'y' and 'Y' input values
- This fixes an issue where remote mode wasn't correctly detected from user input
2025-03-05 09:58:50 +00:00
8af47ed35c Fix remote transmission configuration in update mode
This commit addresses an issue where the remote Transmission settings weren't properly applied when running in update mode:

1. Add prompt for remote Transmission details in update mode
2. Add code to modify the config.json file directly with the user's remote Transmission settings
3. Set default CONFIG_DIR and USER variables early in the script
4. Add detailed configuration dialog including:
   - Remote host and port
   - Username and password
   - RPC path
   - Directory mapping between remote and local paths

This ensures that users selecting remote Transmission mode will always be prompted for the connection details, even when updating an existing installation.
2025-03-05 09:57:30 +00:00
28 changed files with 2787 additions and 308 deletions

8
.env.install Normal file
View File

@@ -0,0 +1,8 @@
export TRANSMISSION_REMOTE=true
export TRANSMISSION_HOST="192.168.5.19"
export TRANSMISSION_PORT="9091"
export TRANSMISSION_USER=""
export TRANSMISSION_PASS=""
export TRANSMISSION_RPC_PATH="/transmission/rpc"
export REMOTE_DOWNLOAD_DIR="/downloads"
export LOCAL_DOWNLOAD_DIR="/media"

108
README.md Executable file → Normal file
View File

@@ -1,9 +1,67 @@
# Transmission RSS Manager v2.0.5
# Transmission RSS Manager v2.0.12
A comprehensive web-based tool to automate and manage your Transmission torrent downloads with RSS feed integration, intelligent media organization, and enhanced security features. Now with automatic updates and easy installation!
## Update System Requirements
To use the automatic update system, the following requirements must be met:
1. **Git must be installed:** The update system uses Git to fetch the latest version.
2. **Installation must be a Git repository:** Your installation directory must be a Git repository clone.
3. **Internet connectivity:** The server must be able to connect to the Git repository.
If you installed using the bootstrap installer, these requirements should be met automatically. If you experience issues with the update system, please ensure Git is properly installed and accessible to the application.
## Changelog
### v2.0.12 (2025-03-10)
- **Fixed**: Removed persistent floating update notification that wouldn't disappear
- **Fixed**: Major localStorage cleanup to prevent notification state persistence
- **Improved**: Complete rewrite of update notification system to use dashboard-only alerts
- **Improved**: Added DOM cleanup to remove any rogue notification elements
### v2.0.11 (2025-03-10)
- **Fixed**: Fixed update button persistence with advanced floating notification
- **Fixed**: Resolved version display issues with direct package.json reading
- **Fixed**: Improved update process for better version reporting
- **Fixed**: Resolved conflict between test mode and actual update status
- **Added**: Refresh button on update notification for easier version checking
- **Added**: Clear test mode indicators to prevent confusion
- **Improved**: Enhanced cache busting for more reliable version checking
- **Improved**: Better user feedback during update process
### v2.0.10 (2025-03-10)
- **Fixed**: Fixed "fs.existsSync is not a function" error in update check
- **Improved**: Better error handling for git repository checks
- **Improved**: More robust file system operations for update detection
### v2.0.9 (2025-03-07)
- **Fixed**: Update button now appears properly on dashboard
- **Fixed**: Remote Transmission connection issues resolved
- **Fixed**: Improved connection test with better error handling
- **Added**: System status and update endpoints for version checking
- **Improved**: Update detection and notification on dashboard
- **Improved**: Better error handling for git operations
### v2.0.8 (2025-03-07)
- **Fixed**: Module import issues on fresh installations with proper file extension handling
- **Fixed**: Adding compatibility symlinks for different module naming styles
- **Improved**: Server.js now uses consistent module import paths with .js extensions
- **Improved**: More robust module file handling in the installer
### v2.0.7 (2025-03-07)
- **Fixed**: Installation directory handling with prompt for choosing install path
- **Fixed**: Bootstrap-installer now defaults to /opt/trans-install with user configuration option
- **Improved**: Better detection and usage of existing installation directories during updates
- **Improved**: Updated documentation to clarify default installation paths
### v2.0.6 (2025-03-05)
- **Added**: Non-interactive mode support for scripted installations
- **Improved**: Remote Transmission configuration collection in install-script.sh
- **Improved**: Better handling of piped input for remote configuration details
- **Improved**: Added debug output to help troubleshoot configuration issues
- **Note**: When updating an existing installation, manual configuration of remote settings through the web interface may still be required
### v2.0.5 (2025-03-05)
- **Fixed**: Config file now properly stored in /etc/transmission-rss-manager directory
- **Fixed**: Remote Transmission detection in install-script.sh
@@ -69,11 +127,21 @@ A comprehensive web-based tool to automate and manage your Transmission torrent
## Installation
### Known Issues
- When updating an existing installation with Remote Transmission settings, you may need to manually configure the remote settings through the web interface after installation.
- The installer will properly detect and collect remote Transmission settings, but they may not be applied to the configuration file in update mode.
### Prerequisites
- Ubuntu/Debian-based system (may work on other Linux distributions)
- Git (will be automatically installed by the bootstrap installer if needed)
- Git 2.25.0 or later (will be automatically installed by the bootstrap installer if needed)
- Required for the automatic update system
- Must be available in the PATH of the user running the application
- Must have proper permissions to access the installation directory
- Internet connection (for downloading and updates)
- Required for Git operations during updates (fetching latest code)
- Outbound access to git.powerdata.dk repository server
### System Requirements
@@ -82,6 +150,10 @@ A comprehensive web-based tool to automate and manage your Transmission torrent
- Disk: At least 200MB for the application, plus storage space for your media
- Network: Internet connection for RSS feed fetching and torrent downloading
### Installation Directory
**Note**: By default, the application will be installed to `/opt/trans-install`. During installation, you'll be prompted to choose a different directory if needed. If you're updating an existing installation, the installer will detect and use your current installation path.
### Automatic Installation
The easiest way to install Transmission RSS Manager is with the bootstrap installer:
@@ -302,6 +374,38 @@ The system will:
- Restore your configuration
- Restart the service automatically
### Troubleshooting Update Issues
If you encounter "Failed to connect to server" or similar errors when checking for updates:
1. **Verify Git Installation**: Ensure Git is properly installed
```bash
which git
git --version # Should be 2.25.0 or higher
```
2. **Check Repository Status**: Verify the installation directory is a Git repository
```bash
cd /opt/transmission-rss-manager # Or your installation directory
git status
```
3. **Check Internet Connectivity**: Make sure the server can connect to git.powerdata.dk
```bash
ping git.powerdata.dk
curl -I https://git.powerdata.dk
```
4. **Check Permissions**: Ensure the application user has access to the Git repository
```bash
# Check ownership of the .git directory
ls -la /opt/transmission-rss-manager/.git
# If needed, fix permissions
sudo chown -R www-data:www-data /opt/transmission-rss-manager # Adjust user as needed
```
5. **Manual Update**: If the web update still fails, try the manual update method from the command line
### Using the Installer
You can also update by running the installer again:

View File

@@ -11,9 +11,14 @@ NC='\033[0m' # No Color
BOLD='\033[1m'
# Installation directory
INSTALL_DIR="/opt/trans-install"
DEFAULT_INSTALL_DIR="/opt/trans-install"
REPO_URL="https://git.powerdata.dk/masterdraco/transmission-rss-manager.git"
# Ask for installation directory
echo -e "${YELLOW}Where would you like to install Transmission RSS Manager?${NC}"
read -p "Installation directory [$DEFAULT_INSTALL_DIR]: " input_install_dir
INSTALL_DIR=${input_install_dir:-$DEFAULT_INSTALL_DIR}
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}This script must be run as root or with sudo privileges.${NC}"
@@ -23,6 +28,7 @@ fi
# Display welcome message
echo -e "${GREEN}${BOLD}Transmission RSS Manager - Bootstrap Installer${NC}"
echo -e "This script will install the latest version from the git repository."
echo -e "The default installation directory is ${BOLD}/opt/trans-install${NC}, but you can choose a different location."
echo
# Check for git installation

View File

@@ -28,14 +28,44 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# Create modules directory if it doesn't exist
mkdir -p "${SCRIPT_DIR}/modules"
# Check for installation type
# Check for installation type in multiple locations
IS_UPDATE=false
if [ -f "${SCRIPT_DIR}/config.json" ]; then
POSSIBLE_CONFIG_LOCATIONS=(
"${SCRIPT_DIR}/config.json"
"/opt/transmission-rss-manager/config.json"
"/etc/transmission-rss-manager/config.json"
)
# Also check for service file - secondary indicator
if [ -f "/etc/systemd/system/transmission-rss-manager.service" ]; then
# Extract install directory from service file if it exists
SERVICE_INSTALL_DIR=$(grep "WorkingDirectory=" "/etc/systemd/system/transmission-rss-manager.service" | cut -d'=' -f2)
if [ -n "$SERVICE_INSTALL_DIR" ]; then
echo -e "${YELLOW}Found existing service at: $SERVICE_INSTALL_DIR${NC}"
POSSIBLE_CONFIG_LOCATIONS+=("$SERVICE_INSTALL_DIR/config.json")
fi
fi
# Check all possible locations
for CONFIG_PATH in "${POSSIBLE_CONFIG_LOCATIONS[@]}"; do
if [ -f "$CONFIG_PATH" ]; then
IS_UPDATE=true
echo -e "${YELLOW}Existing installation detected. Running in update mode.${NC}"
echo -e "${YELLOW}Existing installation detected at: $CONFIG_PATH${NC}"
echo -e "${YELLOW}Running in update mode.${NC}"
echo -e "${GREEN}Your existing configuration will be preserved.${NC}"
else
echo -e "${GREEN}Fresh installation. Will create new configuration.${NC}"
# If the config is not in the current directory, store its location
if [ "$CONFIG_PATH" != "${SCRIPT_DIR}/config.json" ]; then
export EXISTING_CONFIG_PATH="$CONFIG_PATH"
export EXISTING_INSTALL_DIR="$(dirname "$CONFIG_PATH")"
echo -e "${YELLOW}Will update installation at: $EXISTING_INSTALL_DIR${NC}"
fi
break
fi
done
if [ "$IS_UPDATE" = "false" ]; then
echo -e "${GREEN}No existing installation detected. Will create new configuration.${NC}"
fi
# Check if modules exist, if not, extract them
@@ -1167,29 +1197,152 @@ fi
# Launch the main installer
echo -e "${GREEN}Launching main installer...${NC}"
# Skip Transmission configuration if we're in update mode
if [ "$IS_UPDATE" = "true" ] && [ -n "$EXISTING_CONFIG_PATH" ]; then
echo -e "${GREEN}Existing configuration detected, skipping Transmission configuration...${NC}"
# Extract Transmission remote setting from existing config
if [ -f "$EXISTING_CONFIG_PATH" ]; then
# Try to extract remoteConfig.isRemote value from config.json
if command -v grep &> /dev/null && command -v sed &> /dev/null; then
IS_REMOTE=$(grep -o '"isRemote":[^,}]*' "$EXISTING_CONFIG_PATH" | sed 's/"isRemote"://; s/[[:space:]]//g')
if [ "$IS_REMOTE" = "true" ]; then
export TRANSMISSION_REMOTE=true
echo -e "${GREEN}Using existing remote Transmission configuration.${NC}"
else
export TRANSMISSION_REMOTE=false
echo -e "${GREEN}Using existing local Transmission configuration.${NC}"
fi
else
# Default to false if we can't extract it
export TRANSMISSION_REMOTE=false
echo -e "${YELLOW}Could not determine Transmission remote setting, using local configuration.${NC}"
fi
fi
else
# Ask about remote Transmission before launching main installer
# This ensures the TRANSMISSION_REMOTE variable is set correctly
echo -e "${BOLD}Transmission Configuration:${NC}"
echo -e "Configure connection to your Transmission client:"
echo
# If stdin is not a terminal (pipe or redirect), read from stdin
if [ ! -t 0 ]; then
# Save all input to a temporary file
INPUT_FILE=$(mktemp)
cat > "$INPUT_FILE"
# Read the first line as the remote selection
input_remote=$(awk 'NR==1{print}' "$INPUT_FILE")
echo "DEBUG: Non-interactive mode detected, read input: '$input_remote'"
# Keep the rest of the input for later use
tail -n +2 "$INPUT_FILE" > "${INPUT_FILE}.rest"
mv "${INPUT_FILE}.rest" "$INPUT_FILE"
else
read -p "Is Transmission running on a remote server? (y/n) [n]: " input_remote
fi
echo "DEBUG: Input received for remote in install-script.sh: '$input_remote'"
# Explicitly check for "y" or "Y" response
if [[ "$input_remote" == "y" ]] || [[ "$input_remote" == "Y" ]]; then
if [ "$input_remote" = "y" ] || [ "$input_remote" = "Y" ]; then
export TRANSMISSION_REMOTE=true
echo -e "${GREEN}Remote Transmission selected.${NC}"
else
export TRANSMISSION_REMOTE=false
echo -e "${GREEN}Local Transmission selected.${NC}"
fi
fi
# Create a direct environment file for the main installer
# If remote mode is selected and not an update, collect remote details here and pass to main installer
if [ "$TRANSMISSION_REMOTE" = "true" ] && [ "$IS_UPDATE" != "true" ]; then
# Get remote transmission details
if [ ! -t 0 ]; then
# Non-interactive mode - we already have input saved to INPUT_FILE
# from the previous step
# Read each line from the input file
TRANSMISSION_HOST=$(awk 'NR==1{print}' "$INPUT_FILE")
TRANSMISSION_PORT=$(awk 'NR==2{print}' "$INPUT_FILE")
TRANSMISSION_USER=$(awk 'NR==3{print}' "$INPUT_FILE")
TRANSMISSION_PASS=$(awk 'NR==4{print}' "$INPUT_FILE")
TRANSMISSION_RPC_PATH=$(awk 'NR==5{print}' "$INPUT_FILE")
REMOTE_DOWNLOAD_DIR=$(awk 'NR==6{print}' "$INPUT_FILE")
LOCAL_DOWNLOAD_DIR=$(awk 'NR==7{print}' "$INPUT_FILE")
# Use defaults for empty values
TRANSMISSION_HOST=${TRANSMISSION_HOST:-"localhost"}
TRANSMISSION_PORT=${TRANSMISSION_PORT:-"9091"}
TRANSMISSION_USER=${TRANSMISSION_USER:-""}
TRANSMISSION_PASS=${TRANSMISSION_PASS:-""}
TRANSMISSION_RPC_PATH=${TRANSMISSION_RPC_PATH:-"/transmission/rpc"}
REMOTE_DOWNLOAD_DIR=${REMOTE_DOWNLOAD_DIR:-"/var/lib/transmission-daemon/downloads"}
LOCAL_DOWNLOAD_DIR=${LOCAL_DOWNLOAD_DIR:-"/mnt/transmission-downloads"}
# Clean up
rm -f "$INPUT_FILE"
echo "DEBUG: Non-interactive mode with remote details:"
echo "DEBUG: Host: $TRANSMISSION_HOST, Port: $TRANSMISSION_PORT"
echo "DEBUG: Remote dir: $REMOTE_DOWNLOAD_DIR, Local dir: $LOCAL_DOWNLOAD_DIR"
else
# Interactive mode - ask for details
read -p "Remote Transmission host [localhost]: " TRANSMISSION_HOST
TRANSMISSION_HOST=${TRANSMISSION_HOST:-"localhost"}
read -p "Remote Transmission port [9091]: " TRANSMISSION_PORT
TRANSMISSION_PORT=${TRANSMISSION_PORT:-"9091"}
read -p "Remote Transmission username []: " TRANSMISSION_USER
TRANSMISSION_USER=${TRANSMISSION_USER:-""}
read -s -p "Remote Transmission password []: " TRANSMISSION_PASS
echo # Add a newline after password input
TRANSMISSION_PASS=${TRANSMISSION_PASS:-""}
read -p "Remote Transmission RPC path [/transmission/rpc]: " TRANSMISSION_RPC_PATH
TRANSMISSION_RPC_PATH=${TRANSMISSION_RPC_PATH:-"/transmission/rpc"}
# Configure directory mapping for remote setup
echo
echo -e "${YELLOW}Directory Mapping Configuration${NC}"
echo -e "When using a remote Transmission server, you need to map paths between servers."
echo -e "For each directory on the remote server, specify the corresponding local directory."
echo
read -p "Remote Transmission download directory [/var/lib/transmission-daemon/downloads]: " REMOTE_DOWNLOAD_DIR
REMOTE_DOWNLOAD_DIR=${REMOTE_DOWNLOAD_DIR:-"/var/lib/transmission-daemon/downloads"}
read -p "Local directory that corresponds to the remote download directory [/mnt/transmission-downloads]: " LOCAL_DOWNLOAD_DIR
LOCAL_DOWNLOAD_DIR=${LOCAL_DOWNLOAD_DIR:-"/mnt/transmission-downloads"}
fi
# Create the environment file with all remote details
cat > "${SCRIPT_DIR}/.env.install" << EOF
export TRANSMISSION_REMOTE=$TRANSMISSION_REMOTE
export TRANSMISSION_HOST="$TRANSMISSION_HOST"
export TRANSMISSION_PORT="$TRANSMISSION_PORT"
export TRANSMISSION_USER="$TRANSMISSION_USER"
export TRANSMISSION_PASS="$TRANSMISSION_PASS"
export TRANSMISSION_RPC_PATH="$TRANSMISSION_RPC_PATH"
export REMOTE_DOWNLOAD_DIR="$REMOTE_DOWNLOAD_DIR"
export LOCAL_DOWNLOAD_DIR="$LOCAL_DOWNLOAD_DIR"
EOF
else
# Local mode - simpler environment file
echo "export TRANSMISSION_REMOTE=$TRANSMISSION_REMOTE" > "${SCRIPT_DIR}/.env.install"
fi
chmod +x "${SCRIPT_DIR}/.env.install"
# Ensure the environment file is world-readable to avoid permission issues
chmod 644 "${SCRIPT_DIR}/.env.install"
# If we're in update mode, add the existing installation path to the environment file
if [ "$IS_UPDATE" = "true" ] && [ -n "$EXISTING_CONFIG_PATH" ]; then
echo "export EXISTING_CONFIG_PATH=\"$EXISTING_CONFIG_PATH\"" >> "${SCRIPT_DIR}/.env.install"
echo "export EXISTING_INSTALL_DIR=\"$EXISTING_INSTALL_DIR\"" >> "${SCRIPT_DIR}/.env.install"
echo "export IS_UPDATE=true" >> "${SCRIPT_DIR}/.env.install"
fi
# Force inclusion in the main installer - modify the main installer temporarily if needed
if ! grep -q "source.*\.env\.install" "${SCRIPT_DIR}/main-installer.sh"; then
# Backup the main installer

View File

@@ -21,10 +21,17 @@ NC='\033[0m' # No Color
# Get current directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# Source the utils module first to make the log function available
source "${SCRIPT_DIR}/modules/utils-module.sh"
# Print header
echo -e "${BOLD}==================================================${NC}"
echo -e "${BOLD} Transmission RSS Manager Installer ${NC}"
VERSION=$(grep -oP '"version": "\K[^"]+' "${SCRIPT_DIR}/package.json" 2>/dev/null || echo "Unknown")
# Check if package.json exists, if not suggest creating it
if [ ! -f "${SCRIPT_DIR}/package.json" ]; then
echo -e "${YELLOW}Warning: package.json not found. You may need to run 'npm init' first.${NC}"
fi
echo -e "${BOLD} Version ${VERSION} - Git Edition ${NC}"
echo -e "${BOLD}==================================================${NC}"
echo
@@ -39,27 +46,74 @@ fi
IS_UPDATE=false
INSTALLATION_DETECTED=false
# Check if we have existing config info from install-script.sh
if [ -n "$EXISTING_INSTALL_DIR" ] && [ -n "$EXISTING_CONFIG_PATH" ]; then
INSTALLATION_DETECTED=true
IS_UPDATE=true
# Use the existing installation directory as our target
INSTALL_DIR="$EXISTING_INSTALL_DIR"
CONFIG_FILE="$EXISTING_CONFIG_PATH"
log "INFO" "Using existing installation at $INSTALL_DIR detected by install-script.sh"
export INSTALL_DIR
else
# Check for config.json file (primary indicator)
if [ -f "${SCRIPT_DIR}/config.json" ]; then
INSTALLATION_DETECTED=true
fi
POSSIBLE_CONFIG_LOCATIONS=(
"${SCRIPT_DIR}/config.json"
"/opt/transmission-rss-manager/config.json"
"/etc/transmission-rss-manager/config.json"
)
# Check for service file (secondary indicator)
if [ -f "/etc/systemd/system/transmission-rss-manager.service" ]; then
for CONFIG_PATH in "${POSSIBLE_CONFIG_LOCATIONS[@]}"; do
if [ -f "$CONFIG_PATH" ]; then
INSTALLATION_DETECTED=true
IS_UPDATE=true
INSTALL_DIR="$(dirname "$CONFIG_PATH")"
CONFIG_FILE="$CONFIG_PATH"
log "INFO" "Found existing installation at $INSTALL_DIR"
export INSTALL_DIR
break
fi
done
# Check for service file (secondary indicator) if no config file found
if [ "$INSTALLATION_DETECTED" = "false" ] && [ -f "/etc/systemd/system/transmission-rss-manager.service" ]; then
INSTALLATION_DETECTED=true
IS_UPDATE=true
# Extract the installation directory from the service file
SERVICE_INSTALL_DIR=$(grep "WorkingDirectory=" "/etc/systemd/system/transmission-rss-manager.service" | cut -d'=' -f2)
if [ -n "$SERVICE_INSTALL_DIR" ]; then
INSTALL_DIR="$SERVICE_INSTALL_DIR"
log "INFO" "Found existing installation at $INSTALL_DIR from service file"
export INSTALL_DIR
# Check for config file in the detected installation directory
if [ -f "$INSTALL_DIR/config.json" ]; then
CONFIG_FILE="$INSTALL_DIR/config.json"
fi
fi
fi
# Check for data directory (tertiary indicator)
if [ -d "${SCRIPT_DIR}/data" ] && [ "$(ls -A "${SCRIPT_DIR}/data" 2>/dev/null)" ]; then
if [ "$INSTALLATION_DETECTED" = "false" ] && [ -d "${SCRIPT_DIR}/data" ] && [ "$(ls -A "${SCRIPT_DIR}/data" 2>/dev/null)" ]; then
INSTALLATION_DETECTED=true
fi
fi
if [ "$INSTALLATION_DETECTED" = true ]; then
IS_UPDATE=true
# Provide clear feedback about the installation type
if [ "$IS_UPDATE" = "true" ]; then
log "INFO" "Running in UPDATE mode - will preserve existing configuration"
log "INFO" "Target installation directory: $INSTALL_DIR"
if [ -n "$CONFIG_FILE" ]; then
log "INFO" "Using configuration file: $CONFIG_FILE"
fi
# Make sure the variables are set correctly
echo -e "${YELLOW}Existing installation detected. Running in update mode.${NC}"
echo -e "${GREEN}Your existing configuration will be preserved.${NC}"
echo -e "${GREEN}Only application files will be updated.${NC}"
else
log "INFO" "Running in FRESH INSTALL mode"
echo -e "${GREEN}Fresh installation. Will create new configuration.${NC}"
fi
export IS_UPDATE
@@ -81,11 +135,17 @@ for module in "${REQUIRED_MODULES[@]}"; do
fi
done
# Source the module files
source "${SCRIPT_DIR}/modules/utils-module.sh" # Load utilities first for logging
# Source the remaining module files
source "${SCRIPT_DIR}/modules/config-module.sh"
source "${SCRIPT_DIR}/modules/dependencies-module.sh"
# Check if the updated service module exists, use it if available
if [ -f "${SCRIPT_DIR}/modules/service-setup-module-updated.sh" ]; then
log "INFO" "Using updated service setup module"
source "${SCRIPT_DIR}/modules/service-setup-module-updated.sh"
else
log "INFO" "Using standard service setup module"
source "${SCRIPT_DIR}/modules/service-setup-module.sh"
fi
source "${SCRIPT_DIR}/modules/file-creator-module.sh"
# Function to handle cleanup on error
@@ -105,14 +165,164 @@ trap 'cleanup_on_error "$BASH_COMMAND"' ERR
# Execute the installation steps in sequence
log "INFO" "Starting installation process..."
# Set default for TRANSMISSION_REMOTE
# Set defaults for key variables
export TRANSMISSION_REMOTE=false
export CONFIG_DIR=${CONFIG_DIR:-"/etc/transmission-rss-manager"}
export USER=${USER:-$(logname || echo $SUDO_USER)}
if [ "$IS_UPDATE" = true ]; then
log "INFO" "Running in update mode - preserving existing configuration..."
# When updating, we only need to update core files and dependencies
# Configuration should be preserved
# First, let's check if we already have this value from the environment
# This allows for non-interactive usage in scripts
if [ -n "$TRANSMISSION_REMOTE" ]; then
is_remote=$([ "$TRANSMISSION_REMOTE" = true ] && echo "Remote" || echo "Local")
log "INFO" "Using Transmission mode from environment: $is_remote"
# Set the input_remote variable based on the environment variable
# This ensures consistent behavior with the rest of the script
if [ "$TRANSMISSION_REMOTE" = true ]; then
input_remote="y"
else
input_remote="n"
fi
else
# Directly ask about Transmission
# This is a direct approach that bypasses any potential sourcing issues
log "INFO" "Configuring Transmission connection..."
echo -e "${BOLD}Transmission Configuration:${NC}"
echo -e "Configure connection to your Transmission client:"
echo
# If stdin is not a terminal (pipe or redirect), assume default
if [ ! -t 0 ]; then
input_remote="n" # Default to no
log "INFO" "Non-interactive mode detected, using default: local Transmission"
else
read -p "Is Transmission running on a remote server? (y/n) [n]: " input_remote
fi
log "INFO" "DEBUG: Input received for remote: '$input_remote'"
fi
# More explicit check for "y" or "Y" input
if [ "$input_remote" = "y" ] || [ "$input_remote" = "Y" ]; then
export TRANSMISSION_REMOTE=true
log "INFO" "Remote Transmission selected."
# Update the config file directly to set remote mode
if [ -f "$CONFIG_DIR/config.json" ]; then
log "INFO" "Updating configuration file for remote Transmission..."
# Log all environment variables we have for debugging
log "INFO" "DEBUG: Environment variables for remote configuration:"
log "INFO" "DEBUG: TRANSMISSION_HOST=${TRANSMISSION_HOST:-'not set'}"
log "INFO" "DEBUG: TRANSMISSION_PORT=${TRANSMISSION_PORT:-'not set'}"
log "INFO" "DEBUG: REMOTE_DOWNLOAD_DIR=${REMOTE_DOWNLOAD_DIR:-'not set'}"
log "INFO" "DEBUG: LOCAL_DOWNLOAD_DIR=${LOCAL_DOWNLOAD_DIR:-'not set'}"
# Check if we already have the remote configuration details from the environment
if [ -n "$TRANSMISSION_HOST" ] && [ -n "$TRANSMISSION_PORT" ] && [ -n "$REMOTE_DOWNLOAD_DIR" ] && [ -n "$LOCAL_DOWNLOAD_DIR" ]; then
log "INFO" "Using remote Transmission configuration from environment"
# Values are already set from the environment, no need to ask again
else
# Get and validate hostname
read -p "Remote Transmission host [localhost]: " input_trans_host
TRANSMISSION_HOST=${input_trans_host:-"localhost"}
# Get and validate port
read -p "Remote Transmission port [9091]: " input_trans_port
TRANSMISSION_PORT=${input_trans_port:-9091}
# Get credentials
read -p "Remote Transmission username []: " input_trans_user
TRANSMISSION_USER=${input_trans_user:-""}
# Use read -s for password to avoid showing it on screen
read -s -p "Remote Transmission password []: " input_trans_pass
echo # Add a newline after the password input
TRANSMISSION_PASS=${input_trans_pass:-""}
read -p "Remote Transmission RPC path [/transmission/rpc]: " input_trans_path
TRANSMISSION_RPC_PATH=${input_trans_path:-"/transmission/rpc"}
# Configure directory mapping for remote setup
echo
echo -e "${YELLOW}Directory Mapping Configuration${NC}"
echo -e "When using a remote Transmission server, you need to map paths between servers."
echo -e "For each directory on the remote server, specify the corresponding local directory."
echo
# Get remote download directory
read -p "Remote Transmission download directory [/var/lib/transmission-daemon/downloads]: " REMOTE_DOWNLOAD_DIR
REMOTE_DOWNLOAD_DIR=${REMOTE_DOWNLOAD_DIR:-"/var/lib/transmission-daemon/downloads"}
# Get local directory that corresponds to remote download directory
read -p "Local directory that corresponds to the remote download directory [/mnt/transmission-downloads]: " LOCAL_DOWNLOAD_DIR
LOCAL_DOWNLOAD_DIR=${LOCAL_DOWNLOAD_DIR:-"/mnt/transmission-downloads"}
fi
# Create mapping JSON
TRANSMISSION_DIR_MAPPING=$(cat <<EOF
{
"$REMOTE_DOWNLOAD_DIR": "$LOCAL_DOWNLOAD_DIR"
}
EOF
)
# Create the local directory
mkdir -p "$LOCAL_DOWNLOAD_DIR"
chown -R $USER:$USER "$LOCAL_DOWNLOAD_DIR"
# Update the config file with the new remote settings
log "INFO" "Updating configuration file with remote Transmission settings..."
# Backup the original config file
cp "$CONFIG_DIR/config.json" "$CONFIG_DIR/config.json.bak.$(date +%Y%m%d%H%M%S)"
# Update the isRemote setting
sed -i 's/"isRemote": false/"isRemote": true/' "$CONFIG_DIR/config.json"
# Update the host setting
sed -i "s/\"host\": \"[^\"]*\"/\"host\": \"$TRANSMISSION_HOST\"/" "$CONFIG_DIR/config.json"
# Update the port setting
sed -i "s/\"port\": [0-9]*/\"port\": $TRANSMISSION_PORT/" "$CONFIG_DIR/config.json"
# Update the username setting
sed -i "s/\"username\": \"[^\"]*\"/\"username\": \"$TRANSMISSION_USER\"/" "$CONFIG_DIR/config.json"
# Update the password setting
sed -i "s/\"password\": \"[^\"]*\"/\"password\": \"$TRANSMISSION_PASS\"/" "$CONFIG_DIR/config.json"
# Update the RPC path setting
sed -i "s|\"path\": \"[^\"]*\"|\"path\": \"$TRANSMISSION_RPC_PATH\"|" "$CONFIG_DIR/config.json"
# Update the directory mapping
# Use a more complex approach since it's a JSON object
# This is a simplification and might need improvement for complex JSON handling
sed -i "/\"directoryMapping\":/c\\ \"directoryMapping\": $TRANSMISSION_DIR_MAPPING" "$CONFIG_DIR/config.json"
log "INFO" "Configuration updated for remote Transmission."
fi
else
export TRANSMISSION_REMOTE=false
log "INFO" "Local Transmission selected."
# Update the config file directly to set local mode
if [ -f "$CONFIG_DIR/config.json" ]; then
log "INFO" "Updating configuration file for local Transmission..."
# Backup the original config file
cp "$CONFIG_DIR/config.json" "$CONFIG_DIR/config.json.bak.$(date +%Y%m%d%H%M%S)"
# Update the isRemote setting
sed -i 's/"isRemote": true/"isRemote": false/' "$CONFIG_DIR/config.json"
# Update the host setting
sed -i 's/"host": "[^"]*"/"host": "localhost"/' "$CONFIG_DIR/config.json"
log "INFO" "Configuration updated for local Transmission."
fi
fi
# Step 1: Check dependencies (but don't reconfigure)
log "INFO" "Checking dependencies..."
@@ -128,26 +338,54 @@ if [ "$IS_UPDATE" = true ]; then
exit 1
}
# Install npm dependencies
log "INFO" "Updating npm dependencies..."
cd "$SCRIPT_DIR"
npm install || {
# Install npm dependencies using our common function
ensure_npm_packages "$INSTALL_DIR" || {
log "ERROR" "NPM installation failed"
exit 1
}
# Copy JavaScript module files during update as well
log "INFO" "Copying JavaScript module files..."
copy_module_files || {
log "ERROR" "Failed to copy JavaScript module files"
exit 1
}
else
# This is a fresh installation - run all steps
# Step 1: First, let's directly ask about Transmission
# Step 1: First, let's check if we already have this value from the environment
# This allows for non-interactive usage in scripts
if [ -n "$TRANSMISSION_REMOTE" ]; then
is_remote=$([ "$TRANSMISSION_REMOTE" = true ] && echo "Remote" || echo "Local")
log "INFO" "Using Transmission mode from environment: $is_remote"
# Set the input_remote variable based on the environment variable
# This ensures consistent behavior with the rest of the script
if [ "$TRANSMISSION_REMOTE" = true ]; then
input_remote="y"
else
input_remote="n"
fi
else
# Directly ask about Transmission
# This is a direct approach that bypasses any potential sourcing issues
log "INFO" "Configuring Transmission connection..."
echo -e "${BOLD}Transmission Configuration:${NC}"
echo -e "Configure connection to your Transmission client:"
echo
# If stdin is not a terminal (pipe or redirect), assume default
if [ ! -t 0 ]; then
input_remote="n" # Default to no
log "INFO" "Non-interactive mode detected, using default: local Transmission"
else
read -p "Is Transmission running on a remote server? (y/n) [n]: " input_remote
if [[ $input_remote =~ ^[Yy]$ ]]; then
fi
log "INFO" "DEBUG: Input received for remote: '$input_remote'"
fi
# More explicit check for "y" or "Y" input
if [ "$input_remote" = "y" ] || [ "$input_remote" = "Y" ]; then
export TRANSMISSION_REMOTE=true
log "INFO" "Remote Transmission selected."
else
@@ -176,6 +414,7 @@ else
log "INFO" "Creating directories..."
# Make sure CONFIG_DIR is set and exported
export CONFIG_DIR=${CONFIG_DIR:-"/etc/transmission-rss-manager"}
# Call our new create_directories function
create_directories || {
log "ERROR" "Directory creation failed"
exit 1
@@ -195,10 +434,8 @@ else
exit 1
}
# Step 6: Install npm dependencies
log "INFO" "Installing npm dependencies..."
cd "$SCRIPT_DIR"
npm install || {
# Step 6: Install npm dependencies using our common function
ensure_npm_packages "$INSTALL_DIR" || {
log "ERROR" "NPM installation failed"
exit 1
}
@@ -207,8 +444,14 @@ fi
# Step 7: Set up update script
log "INFO" "Setting up update script..."
mkdir -p "${SCRIPT_DIR}/scripts"
cp "${SCRIPT_DIR}/scripts/update.sh" "${SCRIPT_DIR}/scripts/update.sh" 2>/dev/null || {
# If copy fails, it probably doesn't exist, so we'll create it
# Check if update script exists - don't copy it to itself
if [ ! -f "${SCRIPT_DIR}/scripts/update.sh" ]; then
# First, check if we have an update script to copy
if [ -f "${SCRIPT_DIR}/update.sh" ]; then
cp "${SCRIPT_DIR}/update.sh" "${SCRIPT_DIR}/scripts/update.sh"
log "INFO" "Copied update script from root to scripts directory"
else
# Create the update script since it doesn't exist
cat > "${SCRIPT_DIR}/scripts/update.sh" << 'EOL'
#!/bin/bash
@@ -297,7 +540,9 @@ echo -e "Changes will take effect immediately."
EOL
chmod +x "${SCRIPT_DIR}/scripts/update.sh"
}
log "INFO" "Created update script: ${SCRIPT_DIR}/scripts/update.sh"
fi
fi
# Step 8: Final setup and permissions
log "INFO" "Finalizing setup..."

View File

@@ -2,7 +2,7 @@
# Configuration module for Transmission RSS Manager Installation
# Configuration variables with defaults
INSTALL_DIR="/opt/transmission-rss-manager"
INSTALL_DIR=${INSTALL_DIR:-"/opt/trans-install"}
CONFIG_DIR="/etc/transmission-rss-manager"
SERVICE_NAME="transmission-rss-manager"
PORT=3000

22
modules/dependencies-module.sh Normal file → Executable file
View File

@@ -19,6 +19,24 @@ function install_dependencies() {
log "INFO" "Loaded transmission settings from absolute path: TRANSMISSION_REMOTE=$TRANSMISSION_REMOTE"
fi
# If we're in update mode, try to load the remote status from existing config
if [ "$IS_UPDATE" = "true" ] && [ -n "$EXISTING_CONFIG_PATH" ]; then
log "INFO" "Update mode detected with config at $EXISTING_CONFIG_PATH, checking Transmission remote setting"
if [ -f "$EXISTING_CONFIG_PATH" ]; then
# Try to extract the isRemote setting from the config file
if command -v grep &> /dev/null; then
IS_REMOTE=$(grep -o '"isRemote":[^,}]*' "$EXISTING_CONFIG_PATH" | grep -o 'true\|false')
if [ "$IS_REMOTE" = "true" ]; then
export TRANSMISSION_REMOTE=true
log "INFO" "Detected remote Transmission configuration from existing config"
elif [ "$IS_REMOTE" = "false" ]; then
export TRANSMISSION_REMOTE=false
log "INFO" "Detected local Transmission configuration from existing config"
fi
fi
fi
fi
# Always prompt if we didn't get TRANSMISSION_REMOTE from environment or previous steps
if [ -z "$TRANSMISSION_REMOTE" ]; then
log "WARN" "TRANSMISSION_REMOTE variable was not set, asking now..."
@@ -77,8 +95,8 @@ function install_dependencies() {
log "INFO" "Node.js is already installed."
fi
# Check if we need to install Transmission (only if local transmission was selected)
if [ "$TRANSMISSION_REMOTE" = false ]; then
# Check if we need to install Transmission (only if local transmission was selected and not in update mode)
if [ "$TRANSMISSION_REMOTE" = false ] && [ "$IS_UPDATE" != "true" ]; then
if ! command_exists transmission-daemon; then
log "INFO" "Local Transmission installation selected, but transmission-daemon is not installed."
log "INFO" "You selected to use a local Transmission installation during configuration."

189
modules/file-creator-module.sh Normal file → Executable file
View File

@@ -1,15 +1,87 @@
#!/bin/bash
# File creator module for Transmission RSS Manager Installation
function create_directories() {
echo -e "${YELLOW}Creating directories...${NC}"
# Create main installation directory
mkdir -p "$INSTALL_DIR"
log "INFO" "Created installation directory: $INSTALL_DIR"
# Create modules directory
mkdir -p "$INSTALL_DIR/modules"
log "INFO" "Created modules directory: $INSTALL_DIR/modules"
# Create data directory
mkdir -p "$INSTALL_DIR/data"
log "INFO" "Created data directory: $INSTALL_DIR/data"
# Create public directory structure
mkdir -p "$INSTALL_DIR/public/css"
mkdir -p "$INSTALL_DIR/public/js"
log "INFO" "Created public directories"
# Create config directory
mkdir -p "$CONFIG_DIR"
log "INFO" "Created config directory: $CONFIG_DIR"
# Create logs directory
mkdir -p "$INSTALL_DIR/logs"
log "INFO" "Created logs directory: $INSTALL_DIR/logs"
# Set permissions
chown -R "$USER:$USER" "$INSTALL_DIR"
chmod -R 755 "$INSTALL_DIR"
log "INFO" "Set permissions for installation directories"
}
function create_config_files() {
echo -e "${YELLOW}Creating configuration files...${NC}"
# Create initial config.json
echo "Creating config.json..."
# Check if we're updating an existing installation
if [ "$IS_UPDATE" = "true" ] && [ -n "$CONFIG_FILE" ] && [ -f "$CONFIG_FILE" ]; then
log "INFO" "Preserving existing configuration file: $CONFIG_FILE"
# Get version from package.json dynamically
VERSION=$(grep -oP '"version": "\K[^"]+' "${SCRIPT_DIR}/package.json" 2>/dev/null || echo "2.0.9")
# Update only the version number in the config file
if [ -f "$CONFIG_FILE" ]; then
# Save a backup of the config file
BACKUP_FILE="${CONFIG_FILE}.bak.$(date +%Y%m%d%H%M%S)"
cp "$CONFIG_FILE" "$BACKUP_FILE"
log "INFO" "Backed up config to: $BACKUP_FILE"
# Update version field only
if command -v jq &> /dev/null; then
# If jq is available, use it to safely update the version
jq ".version = \"$VERSION\"" "$CONFIG_FILE" > "${CONFIG_FILE}.new"
if [ $? -eq 0 ]; then
mv "${CONFIG_FILE}.new" "$CONFIG_FILE"
log "INFO" "Updated config version to: $VERSION"
else
log "WARN" "Failed to update version with jq, keeping original config unchanged"
rm -f "${CONFIG_FILE}.new"
fi
else
log "INFO" "jq not available, keeping original config file"
fi
fi
# Return early - no need to create a new config
return 0
fi
# For fresh installations, create initial config.json
echo "Creating new config.json..."
mkdir -p "$CONFIG_DIR"
# Get version from package.json dynamically
VERSION=$(grep -oP '"version": "\K[^"]+' "${SCRIPT_DIR}/package.json" 2>/dev/null || echo "2.0.9")
cat > $CONFIG_DIR/config.json << EOF
{
"version": "2.0.3",
"version": "$VERSION",
"installPath": "$INSTALL_DIR",
"transmissionConfig": {
"host": "$TRANSMISSION_HOST",
@@ -102,8 +174,9 @@ const cors = require('cors');
const Transmission = require('transmission');
// Import custom modules
const PostProcessor = require('./modules/postProcessor');
const RssFeedManager = require('./modules/rssFeedManager');
const PostProcessor = require('./modules/post-processor.js');
const RssFeedManager = require('./modules/rss-feed-manager.js');
const TransmissionClient = require('./modules/transmission-client.js');
// Initialize Express app
const app = express();
@@ -265,11 +338,21 @@ app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' }));
// API routes
//==============================
// Get the version from package.json
let appVersion;
try {
const packageJson = require('./package.json');
appVersion = packageJson.version;
} catch (err) {
console.warn('Could not read version from package.json, using default');
appVersion = '2.0.9'; // Default fallback version aligned with package.json
}
// Server status API
app.get('/api/status', (req, res) => {
res.json({
status: 'running',
version: '1.2.0',
version: appVersion,
transmissionConnected: !!transmissionClient,
postProcessorActive: postProcessor && postProcessor.processingIntervalId !== null,
rssFeedManagerActive: rssFeedManager && rssFeedManager.updateIntervalId !== null,
@@ -1762,4 +1845,98 @@ EOF
}
echo "Configuration files created."
# Copy all JavaScript modules to the installation directory
echo "Copying JavaScript module files..."
copy_module_files
}
# Function to copy all JavaScript module files to the installation directory
function copy_module_files() {
# Create the modules directory if it doesn't exist
mkdir -p "$INSTALL_DIR/modules"
# Copy all JavaScript module files from the source directory
for js_file in "${SCRIPT_DIR}/modules/"*.js; do
if [ -f "$js_file" ]; then
module_name=$(basename "$js_file")
echo "Copying module: $module_name"
cp "$js_file" "$INSTALL_DIR/modules/$module_name"
# Set permissions
chown "$USER:$USER" "$INSTALL_DIR/modules/$module_name"
chmod 644 "$INSTALL_DIR/modules/$module_name"
fi
done
# Copy main server files
echo "Copying main server files..."
# server.js
if [ -f "${SCRIPT_DIR}/server.js" ]; then
cp "${SCRIPT_DIR}/server.js" "$INSTALL_DIR/"
log "INFO" "Copied main server file: server.js"
chown "$USER:$USER" "$INSTALL_DIR/server.js"
chmod 644 "$INSTALL_DIR/server.js"
else
log "ERROR" "Main server file server.js not found in source directory"
return 1
fi
# server-endpoints.js (if it exists)
if [ -f "${SCRIPT_DIR}/server-endpoints.js" ]; then
cp "${SCRIPT_DIR}/server-endpoints.js" "$INSTALL_DIR/"
log "INFO" "Copied API endpoints file: server-endpoints.js"
chown "$USER:$USER" "$INSTALL_DIR/server-endpoints.js"
chmod 644 "$INSTALL_DIR/server-endpoints.js"
fi
# Function to create bidirectional symlinks for module compatibility
create_bidirectional_links() {
local hyphenated="$1"
local camelCase="$2"
# Check if hyphenated version exists
if [ -f "$INSTALL_DIR/modules/$hyphenated.js" ]; then
# Create camelCase symlink
ln -sf "$hyphenated.js" "$INSTALL_DIR/modules/$camelCase.js"
log "INFO" "Created symlink: $camelCase.js -> $hyphenated.js"
# Create symlinks without extension
ln -sf "$hyphenated.js" "$INSTALL_DIR/modules/$hyphenated"
ln -sf "$hyphenated.js" "$INSTALL_DIR/modules/$camelCase"
log "INFO" "Created extension-less symlinks: $hyphenated, $camelCase -> $hyphenated.js"
# Check if camelCase version exists
elif [ -f "$INSTALL_DIR/modules/$camelCase.js" ]; then
# Create hyphenated symlink
ln -sf "$camelCase.js" "$INSTALL_DIR/modules/$hyphenated.js"
log "INFO" "Created symlink: $hyphenated.js -> $camelCase.js"
# Create symlinks without extension
ln -sf "$camelCase.js" "$INSTALL_DIR/modules/$hyphenated"
ln -sf "$camelCase.js" "$INSTALL_DIR/modules/$camelCase"
log "INFO" "Created extension-less symlinks: $hyphenated, $camelCase -> $camelCase.js"
else
log "WARN" "Neither $hyphenated.js nor $camelCase.js exists in $INSTALL_DIR/modules"
fi
# Set permissions for all symlinks
chmod 644 "$INSTALL_DIR/modules/$hyphenated.js" 2>/dev/null || true
chmod 644 "$INSTALL_DIR/modules/$camelCase.js" 2>/dev/null || true
chmod 644 "$INSTALL_DIR/modules/$hyphenated" 2>/dev/null || true
chmod 644 "$INSTALL_DIR/modules/$camelCase" 2>/dev/null || true
# Set ownership for all symlinks
chown "$USER:$USER" "$INSTALL_DIR/modules/$hyphenated.js" 2>/dev/null || true
chown "$USER:$USER" "$INSTALL_DIR/modules/$camelCase.js" 2>/dev/null || true
chown "$USER:$USER" "$INSTALL_DIR/modules/$hyphenated" 2>/dev/null || true
chown "$USER:$USER" "$INSTALL_DIR/modules/$camelCase" 2>/dev/null || true
}
# Create bidirectional symlinks for all modules
create_bidirectional_links "rss-feed-manager" "rssFeedManager"
create_bidirectional_links "transmission-client" "transmissionClient"
create_bidirectional_links "post-processor" "postProcessor"
log "INFO" "Copied JavaScript modules and created compatibility symlinks in $INSTALL_DIR/modules/"
}

1
modules/post-processor Symbolic link
View File

@@ -0,0 +1 @@
post-processor.js

1
modules/postProcessor Symbolic link
View File

@@ -0,0 +1 @@
post-processor.js

1
modules/postProcessor.js Symbolic link
View File

@@ -0,0 +1 @@
post-processor.js

1
modules/rss-feed-manager Symbolic link
View File

@@ -0,0 +1 @@
rss-feed-manager.js

View File

@@ -18,9 +18,30 @@ class RssFeedManager {
this.updateIntervalMinutes = config.updateIntervalMinutes || 60;
this.parser = new xml2js.Parser({ explicitArray: false });
// Ensure dataPath is properly defined
// 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;
}
@@ -31,6 +52,10 @@ class RssFeedManager {
}
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();
@@ -131,10 +156,13 @@ class RssFeedManager {
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/1.2.0'
'User-Agent': `Transmission-RSS-Manager/${version}`
}
});
@@ -480,12 +508,30 @@ class RssFeedManager {
}
}
/**
* 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) {
console.error('Error creating data directory:', error);
throw 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}`);
}
}
}

1
modules/rssFeedManager Symbolic link
View File

@@ -0,0 +1 @@
rss-feed-manager.js

1
modules/rssFeedManager.js Symbolic link
View File

@@ -0,0 +1 @@
rss-feed-manager.js

View File

@@ -0,0 +1,324 @@
#!/bin/bash
# Service setup module for Transmission RSS Manager Installation
# Setup systemd service
function setup_service() {
log "INFO" "Setting up systemd service..."
# Ensure required variables are set
if [ -z "$SERVICE_NAME" ]; then
log "ERROR" "SERVICE_NAME variable is not set"
exit 1
fi
if [ -z "$USER" ]; then
log "ERROR" "USER variable is not set"
exit 1
fi
if [ -z "$INSTALL_DIR" ]; then
log "ERROR" "INSTALL_DIR variable is not set"
exit 1
fi
if [ -z "$CONFIG_DIR" ]; then
log "ERROR" "CONFIG_DIR variable is not set"
exit 1
fi
if [ -z "$PORT" ]; then
log "ERROR" "PORT variable is not set"
exit 1
fi
# Check if systemd is available
if ! command -v systemctl &> /dev/null; then
log "ERROR" "systemd is not available on this system"
log "INFO" "Please set up the service manually using your system's service manager"
return 1
fi
# Ensure the test-and-start script exists and is executable
TEST_START_SCRIPT="$INSTALL_DIR/scripts/test-and-start.sh"
mkdir -p "$(dirname "$TEST_START_SCRIPT")"
cat > "$TEST_START_SCRIPT" << 'EOF'
#!/bin/bash
# Script to ensure data directory exists and start the application
# Define paths
APP_DIR="$(dirname "$(dirname "$(readlink -f "$0")")")"
DATA_DIR="$APP_DIR/data"
echo "Starting Transmission RSS Manager..."
echo "Application directory: $APP_DIR"
echo "Data directory: $DATA_DIR"
# Ensure the data directory exists
if [ ! -d "$DATA_DIR" ]; then
echo "Creating data directory: $DATA_DIR"
mkdir -p "$DATA_DIR"
if [ $? -ne 0 ]; then
echo "Failed to create data directory. Trying alternative method..."
# Try alternative method if standard mkdir fails
cd "$APP_DIR" && mkdir -p data
if [ $? -ne 0 ]; then
echo "ERROR: Both methods to create data directory failed. Please check permissions."
exit 1
fi
fi
fi
# Set permissions
chmod -R 755 "$DATA_DIR"
# Check for RSS files
if [ ! -f "$DATA_DIR/rss-feeds.json" ]; then
echo "Creating initial empty rss-feeds.json file"
echo "[]" > "$DATA_DIR/rss-feeds.json"
fi
if [ ! -f "$DATA_DIR/rss-items.json" ]; then
echo "Creating initial empty rss-items.json file"
echo "[]" > "$DATA_DIR/rss-items.json"
fi
# Find the node executable path
NODE_PATH=$(which node 2>/dev/null)
if [ -z "$NODE_PATH" ]; then
# If node is not in PATH, try common locations
for path in /usr/bin/node /usr/local/bin/node /opt/node/bin/node /usr/lib/node; do
if [ -x "$path" ]; then
NODE_PATH="$path"
break
fi
done
# If we still can't find node, use the default path
if [ -z "$NODE_PATH" ]; then
NODE_PATH="/usr/bin/node"
echo "Warning: Node.js not found in PATH, using default path: $NODE_PATH"
fi
fi
# Start the application
cd "$APP_DIR" || { echo "Failed to change to application directory"; exit 1; }
echo "Starting node.js application with: $NODE_PATH $APP_DIR/server.js"
exec "$NODE_PATH" "$APP_DIR/server.js"
EOF
chmod +x "$TEST_START_SCRIPT"
log "INFO" "Created test-and-start script at $TEST_START_SCRIPT"
# Check if service file already exists
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
if [ -f "$SERVICE_FILE" ] && [ "$IS_UPDATE" = true ]; then
log "INFO" "Service file already exists. Preserving existing service configuration."
# Extract existing JWT_SECRET if present to maintain session consistency
EXISTING_JWT_SECRET=$(grep "Environment=JWT_SECRET=" "$SERVICE_FILE" | cut -d'=' -f3)
# Extract existing PORT if it differs from the configured one
EXISTING_PORT=$(grep "Environment=PORT=" "$SERVICE_FILE" | cut -d'=' -f3)
if [ -n "$EXISTING_PORT" ] && [ "$EXISTING_PORT" != "$PORT" ]; then
log "INFO" "Using existing port configuration: $EXISTING_PORT"
PORT=$EXISTING_PORT
fi
# Create backup of existing service file
backup_file "$SERVICE_FILE"
# Update the service file while preserving key settings
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=Transmission RSS Manager
After=network.target transmission-daemon.service
Wants=network-online.target
[Service]
Type=simple
User=$USER
WorkingDirectory=$INSTALL_DIR
ExecStart=$TEST_START_SCRIPT
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
Environment=PORT=$PORT
Environment=NODE_ENV=production
Environment=DEBUG_ENABLED=false
Environment=LOG_FILE=$INSTALL_DIR/logs/transmission-rss-manager.log
Environment=CONFIG_DIR=$CONFIG_DIR
EOF
# Preserve the existing JWT_SECRET if available
if [ -n "$EXISTING_JWT_SECRET" ]; then
echo "Environment=JWT_SECRET=$EXISTING_JWT_SECRET" >> "$SERVICE_FILE"
else
echo "# Generate a random JWT secret for security" >> "$SERVICE_FILE"
echo "Environment=JWT_SECRET=$(openssl rand -hex 32)" >> "$SERVICE_FILE"
fi
# Close the service file definition
cat >> "$SERVICE_FILE" << EOF
[Install]
WantedBy=multi-user.target
EOF
else
# For fresh installations, create a new service file
log "INFO" "Creating new service file"
# Create backup of existing service file if it exists
if [ -f "$SERVICE_FILE" ]; then
backup_file "$SERVICE_FILE"
fi
# Create systemd service file
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=Transmission RSS Manager
After=network.target transmission-daemon.service
Wants=network-online.target
[Service]
Type=simple
User=$USER
WorkingDirectory=$INSTALL_DIR
ExecStart=$TEST_START_SCRIPT
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
Environment=PORT=$PORT
Environment=NODE_ENV=production
Environment=DEBUG_ENABLED=false
Environment=LOG_FILE=$INSTALL_DIR/logs/transmission-rss-manager.log
Environment=CONFIG_DIR=$CONFIG_DIR
# Generate a random JWT secret for security
Environment=JWT_SECRET=$(openssl rand -hex 32)
[Install]
WantedBy=multi-user.target
EOF
fi
# Create logs directory
mkdir -p "$INSTALL_DIR/logs"
chown -R $USER:$USER "$INSTALL_DIR/logs"
# Check if file was created successfully
if [ ! -f "$SERVICE_FILE" ]; then
log "ERROR" "Failed to create systemd service file"
return 1
fi
log "INFO" "Setting up Nginx reverse proxy..."
# Check if nginx is installed
if ! command -v nginx &> /dev/null; then
log "ERROR" "Nginx is not installed"
log "INFO" "Skipping Nginx configuration. Please configure your web server manually."
# Reload systemd and enable service
systemctl daemon-reload
systemctl enable "$SERVICE_NAME"
log "INFO" "Systemd service has been created and enabled."
log "INFO" "The service will start automatically after installation."
return 0
fi
# Detect nginx configuration directory
NGINX_AVAILABLE_DIR=""
NGINX_ENABLED_DIR=""
if [ -d "/etc/nginx/sites-available" ] && [ -d "/etc/nginx/sites-enabled" ]; then
# Debian/Ubuntu style
NGINX_AVAILABLE_DIR="/etc/nginx/sites-available"
NGINX_ENABLED_DIR="/etc/nginx/sites-enabled"
elif [ -d "/etc/nginx/conf.d" ]; then
# CentOS/RHEL style
NGINX_AVAILABLE_DIR="/etc/nginx/conf.d"
NGINX_ENABLED_DIR="/etc/nginx/conf.d"
else
log "WARN" "Unable to determine Nginx configuration directory"
log "INFO" "Please configure Nginx manually"
# Reload systemd and enable service
systemctl daemon-reload
systemctl enable "$SERVICE_NAME"
log "INFO" "Systemd service has been created and enabled."
log "INFO" "The service will start automatically after installation."
return 0
fi
# Check if default nginx file exists, back it up if it does
if [ -f "$NGINX_ENABLED_DIR/default" ]; then
backup_file "$NGINX_ENABLED_DIR/default"
if [ -f "$NGINX_ENABLED_DIR/default.bak" ]; then
log "INFO" "Backed up default nginx configuration."
fi
fi
# Create nginx configuration file
NGINX_CONFIG_FILE="$NGINX_AVAILABLE_DIR/$SERVICE_NAME.conf"
cat > "$NGINX_CONFIG_FILE" << EOF
server {
listen 80;
server_name _;
location / {
proxy_pass http://127.0.0.1:$PORT;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host \$host;
proxy_cache_bypass \$http_upgrade;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
}
EOF
log "INFO" "Nginx configured to proxy connections from port 80 to port $PORT"
log "INFO" "You can access Transmission RSS Manager at http://your-server-ip/ (port 80) via Nginx"
# Check if Debian/Ubuntu style (need symlink between available and enabled)
if [ "$NGINX_AVAILABLE_DIR" != "$NGINX_ENABLED_DIR" ]; then
# Create symbolic link to enable the site (if it doesn't already exist)
if [ ! -h "$NGINX_ENABLED_DIR/$SERVICE_NAME.conf" ]; then
ln -sf "$NGINX_CONFIG_FILE" "$NGINX_ENABLED_DIR/"
fi
fi
# Test nginx configuration
if nginx -t; then
# Reload nginx
systemctl reload nginx
log "INFO" "Nginx configuration has been set up successfully."
else
log "ERROR" "Nginx configuration test failed. Please check the configuration manually."
log "WARN" "You may need to correct the configuration before the web interface will be accessible."
fi
# Check for port conflicts
if ss -lnt | grep ":$PORT " &> /dev/null; then
log "WARN" "Port $PORT is already in use. This may cause conflicts with the service."
log "WARN" "The service will fail to start. Please stop any service using port $PORT and try again."
else
log "INFO" "You can access the web interface at: http://localhost:$PORT or http://your-server-ip:$PORT"
log "INFO" "You may need to configure your firewall to allow access to port $PORT"
fi
# Reload systemd
systemctl daemon-reload
# Enable the service to start on boot
systemctl enable "$SERVICE_NAME"
log "INFO" "Systemd service has been created and enabled."
log "INFO" "The service will start automatically after installation."
}

1
modules/transmission-client Symbolic link
View File

@@ -0,0 +1 @@
transmission-client.js

View File

@@ -28,8 +28,14 @@ class TransmissionClient {
this.dirMappings = config.remoteConfig.directoryMapping;
}
// Initialize the connection
// Initialize the connection - but don't throw if it fails initially
// This allows the object to be created even if the connection fails
try {
this.initializeConnection();
} catch (error) {
console.error("Failed to initialize Transmission connection:", error.message);
// Don't throw - allow methods to handle connection retry logic
}
}
/**
@@ -39,8 +45,11 @@ class TransmissionClient {
const { host, port, username, password, path: rpcPath } = this.config.transmissionConfig;
try {
// Only default to localhost if host is empty/null/undefined
const connectionHost = (host === undefined || host === null || host === '') ? 'localhost' : host;
this.client = new Transmission({
host: host || 'localhost',
host: connectionHost,
port: port || 9091,
username: username || '',
password: password || '',
@@ -48,7 +57,7 @@ class TransmissionClient {
timeout: 30000 // 30 seconds
});
console.log(`Initialized Transmission client connection to ${host}:${port}${rpcPath}`);
console.log(`Initialized Transmission client connection to ${connectionHost}:${port}${rpcPath}`);
} catch (error) {
console.error('Failed to initialize Transmission client:', error);
throw error;
@@ -61,13 +70,17 @@ class TransmissionClient {
*/
async getStatus() {
try {
// Use the session-stats method for basic connectivity check
const sessionInfo = await this.client.sessionStats();
const version = await this.client.sessionGet();
// Use the session-get method to get version info
// Note: In transmission-promise, this is 'session' not 'sessionGet'
const session = await this.client.session();
return {
connected: true,
version: version.version,
rpcVersion: version['rpc-version'],
version: session.version || "Unknown",
rpcVersion: session['rpc-version'] || "Unknown",
downloadSpeed: sessionInfo.downloadSpeed,
uploadSpeed: sessionInfo.uploadSpeed,
torrentCount: sessionInfo.torrentCount,
@@ -112,6 +125,15 @@ class TransmissionClient {
*/
async addTorrent(url, options = {}) {
try {
// Verify client is initialized
if (!this.client) {
await this.initializeConnection();
if (!this.client) {
throw new Error("Failed to initialize Transmission client");
}
}
const downloadDir = options.downloadDir || null;
const result = await this.client.addUrl(url, {
"download-dir": downloadDir,
@@ -443,7 +465,8 @@ class TransmissionClient {
*/
async setSessionParams(params) {
try {
await this.client.sessionSet(params);
// In transmission-promise, the method is sessionUpdate not sessionSet
await this.client.sessionUpdate(params);
return {
success: true,
message: 'Session parameters updated successfully'

1
modules/transmissionClient Symbolic link
View File

@@ -0,0 +1 @@
transmission-client.js

View File

@@ -0,0 +1 @@
transmission-client.js

View File

@@ -97,6 +97,76 @@ function create_dir_if_not_exists() {
}
# Function to finalize the setup (permissions, etc.)
# Function to ensure NPM packages are properly installed
function ensure_npm_packages() {
local install_dir=$1
# First ensure the installation directory exists
if [ ! -d "$install_dir" ]; then
log "INFO" "Creating installation directory: $install_dir"
mkdir -p "$install_dir"
if [ $? -ne 0 ]; then
log "ERROR" "Failed to create installation directory: $install_dir"
return 1
fi
fi
# Ensure data directory exists
if [ ! -d "$install_dir/data" ]; then
log "INFO" "Creating data directory: $install_dir/data"
mkdir -p "$install_dir/data"
if [ $? -ne 0 ]; then
log "ERROR" "Failed to create data directory: $install_dir/data"
return 1
fi
# Initialize empty data files
echo "[]" > "$install_dir/data/rss-feeds.json"
echo "[]" > "$install_dir/data/rss-items.json"
log "INFO" "Initialized empty data files"
fi
# Ensure package.json exists in the installation directory
if [ ! -f "$install_dir/package.json" ]; then
log "INFO" "Copying package.json to installation directory..."
cp "$SCRIPT_DIR/package.json" "$install_dir/package.json"
if [ $? -ne 0 ]; then
log "ERROR" "Failed to copy package.json to installation directory"
return 1
fi
fi
# Install NPM packages if not already installed or if it's an update
if [ ! -d "$install_dir/node_modules" ] || [ "$IS_UPDATE" = "true" ]; then
log "INFO" "Installing NPM packages in $install_dir..."
# Save current directory
local current_dir=$(pwd)
# Change to install directory and install packages
cd "$install_dir"
if [ $? -ne 0 ]; then
log "ERROR" "Failed to change to installation directory: $install_dir"
return 1
fi
npm install
if [ $? -ne 0 ]; then
log "ERROR" "NPM installation failed in $install_dir"
cd "$current_dir" # Return to original directory
return 1
fi
# Return to original directory
cd "$current_dir"
log "INFO" "NPM packages successfully installed in $install_dir"
else
log "INFO" "NPM packages appear to be already installed in $install_dir, skipping"
fi
return 0
}
function finalize_setup() {
log "INFO" "Setting up final permissions and configurations..."
@@ -139,9 +209,10 @@ function finalize_setup() {
create_dir_if_not_exists "$MEDIA_DIR/magazines" "$USER:$USER"
fi
# Install NPM packages
log "INFO" "Installing NPM packages..."
cd $INSTALL_DIR && npm install
# Install npm packages
ensure_npm_packages "$INSTALL_DIR" || {
log "ERROR" "Failed to install NPM packages"
}
# Handle configuration file
if ! update_config_file "$CONFIG_DIR/config.json" "$IS_UPDATE"; then
@@ -156,9 +227,12 @@ function finalize_setup() {
# Make sure CONFIG_DIR exists
mkdir -p "$CONFIG_DIR"
# Get version from package.json dynamically
VERSION=$(grep -oP '"version": "\K[^"]+' "${SCRIPT_DIR}/package.json" 2>/dev/null || echo "2.0.9")
cat > $CONFIG_DIR/config.json << EOF
{
"version": "1.2.0",
"version": "$VERSION",
"transmissionConfig": {
"host": "${TRANSMISSION_HOST}",
"port": ${TRANSMISSION_PORT},

View File

@@ -1,6 +1,6 @@
{
"name": "transmission-rss-manager",
"version": "2.0.5",
"version": "2.0.12",
"description": "A comprehensive web-based tool to automate and manage your Transmission torrent downloads with RSS feed integration and intelligent media organization",
"main": "server.js",
"scripts": {

View File

@@ -137,8 +137,8 @@
<div class="mt-2 testing-controls">
<small><a href="#" id="toggle-test-update-button">Toggle Test Update</a></small>
</div>
<div id="update-available" class="mt-3 d-none">
<div class="alert alert-info">
<div id="update-available" class="mt-3">
<div class="alert alert-info update-alert" style="display: none;">
<i class="fas fa-arrow-circle-up"></i>
<span>A new version is available!</span>
<button id="btn-update-now" class="btn btn-sm btn-primary ml-2">
@@ -545,7 +545,7 @@
<div class="container">
<div class="row">
<div class="col-md-6">
<p>Transmission RSS Manager v2.0.0</p>
<p>Transmission RSS Manager <span id="footer-version">v2.0.10</span></p>
</div>
<div class="col-md-6 text-right">
<p><a href="https://git.powerdata.dk/masterdraco/transmission-rss-manager" target="_blank" rel="noopener noreferrer">GitHub</a> | <a href="#" id="show-about-modal">About</a></p>
@@ -592,6 +592,49 @@
<h4>Version History</h4>
<div class="version-history">
<div class="version">
<h5>v2.0.11 - March 2025</h5>
<ul>
<li><strong>Fixed</strong>: Update button persistence with floating notification</li>
<li><strong>Fixed</strong>: Version display issues with direct package.json reading</li>
<li><strong>Fixed</strong>: Update process for better version reporting</li>
<li><strong>Fixed</strong>: Conflict between test mode and actual update status</li>
<li><strong>Added</strong>: Refresh button on update notification</li>
<li><strong>Added</strong>: Clear test mode indicators to prevent confusion</li>
<li><strong>Improved</strong>: Enhanced cache busting for version checking</li>
<li><strong>Improved</strong>: Better user feedback during update process</li>
</ul>
</div>
<div class="version">
<h5>v2.0.10 - March 2025</h5>
<ul>
<li><strong>Fixed</strong>: fs.existsSync error in update check</li>
<li><strong>Fixed</strong>: Update button now stays visible when update is available</li>
<li><strong>Fixed</strong>: Footer version now shows correct running version</li>
<li><strong>Improved</strong>: Better error handling for git repository checks</li>
<li><strong>Improved</strong>: More robust file system operations for update detection</li>
</ul>
</div>
<div class="version">
<h5>v2.0.9 - March 2025</h5>
<ul>
<li><strong>Fixed</strong>: Update button now appears properly on dashboard</li>
<li><strong>Fixed</strong>: Remote Transmission connection issues resolved</li>
<li><strong>Fixed</strong>: Improved connection test with better error handling</li>
<li><strong>Added</strong>: System status and update endpoints for version checking</li>
<li><strong>Improved</strong>: Update detection and notification on dashboard</li>
</ul>
</div>
<div class="version">
<h5>v2.0.6 - March 2025</h5>
<ul>
<li><strong>Fixed</strong>: Remote Transmission configuration storage and application</li>
<li><strong>Fixed</strong>: Config directory issues in /etc/transmission-rss-manager</li>
<li><strong>Fixed</strong>: Input handling for automated/piped installations</li>
<li><strong>Improved</strong>: Non-interactive mode for scripted installations</li>
<li><strong>Added</strong>: Enhanced debug logging for installation process</li>
</ul>
</div>
<div class="version">
<h5>v2.0.0 - March 2025</h5>
<ul>
@@ -628,7 +671,7 @@
</div>
<div class="text-center mt-4">
<p><strong>Transmission RSS Manager v2.0.0</strong></p>
<p><strong id="about-version">Transmission RSS Manager v2.0.11</strong></p>
<p>© 2025 PowerData.dk - All Rights Reserved</p>
<p><a href="https://powerdata.dk" target="_blank">Visit PowerData.dk</a></p>
</div>
@@ -639,6 +682,60 @@
</div>
</div>
<!-- Update Alert Custom Styles -->
<style>
/* Custom styles for update alert to ensure it's visible */
.update-alert {
display: none;
margin-top: 10px !important;
border: 2px solid #007bff !important;
background-color: #cce5ff !important;
color: #004085 !important;
font-weight: bold !important;
box-shadow: 0 2px 5px rgba(0,0,0,0.2) !important;
position: relative !important;
z-index: 100 !important;
}
.update-alert span {
color: #004085 !important;
font-weight: bold !important;
}
/* Floating update notification that's impossible to miss */
#floating-update-notification {
display: none;
position: fixed;
top: 20px;
right: 20px;
width: 300px;
padding: 15px;
background-color: #ff5555;
color: white;
border: 3px solid #cc0000;
border-radius: 5px;
box-shadow: 0 0 20px rgba(0,0,0,0.5);
z-index: 10000;
font-weight: bold;
text-align: center;
}
#floating-update-notification button {
margin-top: 10px;
padding: 5px 10px;
background-color: white;
color: #cc0000;
border: none;
border-radius: 3px;
font-weight: bold;
cursor: pointer;
}
#floating-update-notification button:hover {
background-color: #eeeeee;
}
</style>
<!-- Removed floating notification completely -->
<!-- The floating notification was removed to fix persistent display issues -->
<!-- JavaScript Files -->
<script src="/js/system-status.js"></script>
<script src="/js/app.js"></script>

View File

@@ -16,15 +16,30 @@ function initSystemStatus() {
// Load system status
function loadSystemStatus() {
fetch('/api/system/status', {
// Add cache-busting parameter
const cacheBuster = `?_=${new Date().getTime()}`;
fetch('/api/system/status' + cacheBuster, {
headers: authHeaders()
})
.then(handleResponse)
.then(data => {
if (data.status === 'success') {
// Update version display
versionElement.textContent = data.data.version;
uptimeElement.textContent = data.data.uptime;
// Also update footer version
const footerVersion = document.getElementById('footer-version');
if (footerVersion) {
footerVersion.textContent = 'v' + data.data.version;
}
// Update version in about modal too if it exists
const aboutVersionElement = document.getElementById('about-version');
if (aboutVersionElement) {
aboutVersionElement.textContent = 'Transmission RSS Manager v' + data.data.version;
}
// Update transmission status with icon
if (data.data.transmissionStatus === 'Connected') {
transmissionStatusElement.innerHTML = '<i class="fas fa-check-circle text-success"></i> Connected';
@@ -41,38 +56,203 @@ function initSystemStatus() {
});
}
// More robust update check status tracking
const UPDATE_KEY = 'trm_update_available';
const CURRENT_VERSION_KEY = 'trm_current_version';
const REMOTE_VERSION_KEY = 'trm_remote_version';
// Force clear any existing update notification state
localStorage.removeItem(UPDATE_KEY);
localStorage.removeItem(CURRENT_VERSION_KEY);
localStorage.removeItem(REMOTE_VERSION_KEY);
let updateCheckInProgress = false;
// Function to show update alert
function showUpdateAlert(currentVersion, remoteVersion) {
// Set status text in the system status panel
updateStatusElement.innerHTML = '<i class="fas fa-exclamation-circle text-warning"></i> Update available';
// Show only the original alert box in the dashboard
try {
const alertBox = updateAvailableDiv.querySelector('.alert');
if (alertBox) {
alertBox.style.display = 'block';
const spanElement = alertBox.querySelector('span');
if (spanElement) {
spanElement.textContent = `A new version is available: ${currentVersion}${remoteVersion}`;
}
}
} catch (e) {
console.error('Error showing original alert box:', e);
}
// We've removed the floating notification entirely, so this part is skipped
console.log('Update alert shown in dashboard:', currentVersion, '->', remoteVersion);
// Store in localStorage
localStorage.setItem(UPDATE_KEY, 'true');
localStorage.setItem(CURRENT_VERSION_KEY, currentVersion);
localStorage.setItem(REMOTE_VERSION_KEY, remoteVersion);
}
// Function to hide update alert
function hideUpdateAlert() {
// Hide original alert
try {
const alertBox = updateAvailableDiv.querySelector('.alert');
if (alertBox) {
alertBox.style.display = 'none';
}
} catch (e) {
console.error('Error hiding original alert:', e);
}
// Clear localStorage
localStorage.removeItem(UPDATE_KEY);
localStorage.removeItem(CURRENT_VERSION_KEY);
localStorage.removeItem(REMOTE_VERSION_KEY);
console.log('Update alert hidden');
}
// Check localStorage on init and set up MutationObserver to prevent hiding
(function checkStoredUpdateStatus() {
const isUpdateAvailable = localStorage.getItem(UPDATE_KEY) === 'true';
if (isUpdateAvailable) {
const currentVersion = localStorage.getItem(CURRENT_VERSION_KEY);
const remoteVersion = localStorage.getItem(REMOTE_VERSION_KEY);
if (currentVersion && remoteVersion) {
showUpdateAlert(currentVersion, remoteVersion);
// Set up mutation observer to detect and revert any attempts to hide the update alert
const alertBox = updateAvailableDiv.querySelector('.alert');
if (alertBox) {
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' &&
(mutation.attributeName === 'style' ||
mutation.attributeName === 'class')) {
// If display is being changed to hide the element, force it back to visible
if (alertBox.style.display !== 'block' ||
alertBox.classList.contains('d-none') ||
alertBox.style.visibility === 'hidden' ||
alertBox.style.opacity === '0') {
console.log('Detected attempt to hide update button, forcing display');
showUpdateAlert(currentVersion, remoteVersion);
}
}
});
});
// Observe style and class attribute changes
observer.observe(alertBox, {
attributes: true,
attributeFilter: ['style', 'class']
});
// Store observer in window object to prevent garbage collection
window._updateButtonObserver = observer;
}
}
}
})();
// Check for updates
function checkForUpdates() {
updateStatusElement.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Checking...';
updateAvailableDiv.classList.add('d-none');
updateCheckInProgress = true;
// Add test=true parameter to force update availability for testing
const testMode = localStorage.getItem('showUpdateButton') === 'true';
const url = testMode ? '/api/system/check-updates?test=true' : '/api/system/check-updates';
const cacheBuster = `_=${new Date().getTime()}`;
const url = testMode
? `/api/system/check-updates?test=true&${cacheBuster}`
: `/api/system/check-updates?${cacheBuster}`;
// Set a timeout to detect network issues
const timeoutId = setTimeout(() => {
updateStatusElement.innerHTML = '<i class="fas fa-times-circle text-danger"></i> Check timed out';
updateCheckInProgress = false;
showNotification('Update check timed out. Please try again later.', 'warning');
}, 10000); // 10 second timeout
// Create a timeout controller
const controller = new AbortController();
const timeoutId2 = setTimeout(() => controller.abort(), 15000);
fetch(url, {
headers: authHeaders()
headers: authHeaders(),
// Add a fetch timeout using abort controller
signal: controller.signal // 15 second timeout
})
.then(response => {
clearTimeout(timeoutId2);
clearTimeout(timeoutId);
return response;
})
.catch(error => {
clearTimeout(timeoutId2);
clearTimeout(timeoutId);
throw error;
})
.then(response => {
// Better error checking
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.message || `Server error: ${response.status}`);
}).catch(e => {
if (e instanceof SyntaxError) {
throw new Error(`Server error: ${response.status}`);
}
throw e;
});
}
return response.json();
})
.then(handleResponse)
.then(data => {
updateCheckInProgress = false;
if (data.status === 'success') {
if (data.data.updateAvailable) {
updateStatusElement.innerHTML = '<i class="fas fa-exclamation-circle text-warning"></i> Update available';
updateAvailableDiv.classList.remove('d-none');
updateAvailableDiv.querySelector('span').textContent =
`A new version is available: ${data.data.currentVersion}${data.data.remoteVersion}`;
if (data.data && data.data.updateAvailable) {
// Show update alert with version info
showUpdateAlert(data.data.currentVersion, data.data.remoteVersion);
// Log to console for debugging
console.log('Update available detected:', data.data.currentVersion, '->', data.data.remoteVersion);
} else {
// No update available
updateStatusElement.innerHTML = '<i class="fas fa-check-circle text-success"></i> Up to date';
hideUpdateAlert();
// Force reload system status to ensure version is current
setTimeout(() => loadSystemStatus(), 1000);
}
} else {
// Error status but with a response
updateStatusElement.innerHTML = '<i class="fas fa-times-circle text-danger"></i> Check failed';
showNotification(data.message || 'Failed to check for updates', 'danger');
// Don't clear update status on error - keep any previous update notification
}
})
.catch(error => {
updateCheckInProgress = false;
clearTimeout(timeoutId);
console.error('Error checking for updates:', error);
updateStatusElement.innerHTML = '<i class="fas fa-times-circle text-danger"></i> Check failed';
showNotification('Failed to connect to server', 'danger');
// More specific error message based on the error type
if (error.name === 'AbortError') {
showNotification('Update check timed out. Please try again later.', 'warning');
} else if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
showNotification('Network error. Please check your connection and try again.', 'danger');
} else {
showNotification(error.message || 'Failed to connect to server', 'danger');
}
// Don't clear update status on error - keep any previous update notification
});
}
@@ -83,37 +263,227 @@ function initSystemStatus() {
return;
}
// Show loading state
// Disable test mode whenever we try to apply an update
localStorage.setItem('showUpdateButton', 'false');
// Update toggle button text if it exists
const testToggle = document.getElementById('toggle-test-update-button');
if (testToggle) {
testToggle.innerText = 'Enable Test Update';
}
// Show loading state on both update buttons
// Original button
if (updateButton) {
updateButton.disabled = true;
updateButton.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Updating...';
}
// Floating notification button
const floatingButton = document.getElementById('floating-update-button');
if (floatingButton) {
floatingButton.disabled = true;
floatingButton.textContent = 'Updating...';
}
showNotification('Applying update. Please wait...', 'info');
// Set a timeout for the update process
const updateTimeoutId = setTimeout(() => {
// Re-enable original button
if (updateButton) {
updateButton.disabled = false;
updateButton.innerHTML = '<i class="fas fa-download"></i> Update Now';
}
// Re-enable floating button
if (floatingButton) {
floatingButton.disabled = false;
floatingButton.textContent = 'Update Now';
}
showNotification('Update process timed out. Please try again or check server logs.', 'warning');
}, 60000); // 60 second timeout for the entire update process
// Create a timeout controller
const updateController = new AbortController();
const updateTimeoutId2 = setTimeout(() => updateController.abort(), 45000);
fetch('/api/system/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...authHeaders()
}
},
signal: updateController.signal // 45 second timeout
})
.then(response => {
clearTimeout(updateTimeoutId2);
return response;
})
.catch(error => {
clearTimeout(updateTimeoutId2);
throw error;
})
.then(response => {
// Better error checking
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.message || `Server error: ${response.status}`);
}).catch(e => {
if (e instanceof SyntaxError) {
throw new Error(`Server error: ${response.status}`);
}
throw e;
});
}
return response.json();
})
.then(handleResponse)
.then(data => {
clearTimeout(updateTimeoutId);
if (data.status === 'success') {
// Check if there's an update message to determine if an update was actually applied
const updateApplied = data.message && data.message.includes('Update applied successfully');
const noNewUpdate = data.data && data.data.output && data.data.output.includes('already have the latest version');
// Hide update notification
hideUpdateAlert();
if (noNewUpdate) {
// If no update was needed, show a different message
showNotification('You already have the latest version. No update was needed.', 'info');
// Re-enable both buttons
if (updateButton) {
updateButton.disabled = false;
updateButton.innerHTML = '<i class="fas fa-download"></i> Check Again';
}
const floatingButton = document.getElementById('floating-update-button');
if (floatingButton) {
floatingButton.disabled = false;
floatingButton.textContent = 'Check Again';
}
// Update page to show current version without reloading
loadSystemStatus();
// Double-check system status again after a delay to ensure version is updated
setTimeout(() => {
loadSystemStatus();
checkForUpdates(); // Run check again to update status text
}, 2000);
return;
}
// Show success notification
showNotification('Update applied successfully. The page will reload in 30 seconds.', 'success');
// Update both buttons with countdown
let secondsLeft = 30;
// Function to update the countdown text
function updateCountdown() {
// Update original button if it exists
if (updateButton) {
updateButton.innerHTML = `<i class="fas fa-sync"></i> Reloading in ${secondsLeft}s...`;
}
// Update floating button if it exists
const floatingButton = document.getElementById('floating-update-button');
if (floatingButton) {
floatingButton.textContent = `Reloading in ${secondsLeft}s...`;
}
}
// Initial text update
updateCountdown();
// Start countdown
const countdownInterval = setInterval(() => {
secondsLeft--;
updateCountdown();
if (secondsLeft <= 0) {
clearInterval(countdownInterval);
// Clear localStorage to ensure a clean reload
localStorage.removeItem(UPDATE_KEY);
localStorage.removeItem(CURRENT_VERSION_KEY);
localStorage.removeItem(REMOTE_VERSION_KEY);
// Also ensure floating notification is completely removed
const floatingNotification = document.getElementById('floating-update-notification');
if (floatingNotification) {
floatingNotification.style.display = 'none';
floatingNotification.removeAttribute('style');
}
// Force a clean reload
window.location.href = window.location.href.split('#')[0] + '?t=' + new Date().getTime();
}
}, 1000);
// Set a timer to reload the page after the service has time to restart
setTimeout(() => {
window.location.reload();
clearInterval(countdownInterval);
// Clear localStorage to ensure a clean reload
localStorage.removeItem(UPDATE_KEY);
localStorage.removeItem(CURRENT_VERSION_KEY);
localStorage.removeItem(REMOTE_VERSION_KEY);
// Also ensure floating notification is completely removed
const floatingNotification = document.getElementById('floating-update-notification');
if (floatingNotification) {
floatingNotification.style.display = 'none';
floatingNotification.removeAttribute('style');
}
// Force a clean reload with cache-busting parameter
window.location.href = window.location.href.split('#')[0] + '?t=' + new Date().getTime();
}, 30000);
} else {
// Enable both buttons on failure
if (updateButton) {
updateButton.disabled = false;
updateButton.innerHTML = '<i class="fas fa-download"></i> Update Now';
}
const floatingButton = document.getElementById('floating-update-button');
if (floatingButton) {
floatingButton.disabled = false;
floatingButton.textContent = 'Update Now';
}
showNotification(data.message || 'Failed to apply update', 'danger');
}
})
.catch(error => {
clearTimeout(updateTimeoutId);
console.error('Error applying update:', error);
// Re-enable both buttons on error
if (updateButton) {
updateButton.disabled = false;
updateButton.innerHTML = '<i class="fas fa-download"></i> Update Now';
showNotification('Failed to connect to server', 'danger');
}
const floatingButton = document.getElementById('floating-update-button');
if (floatingButton) {
floatingButton.disabled = false;
floatingButton.textContent = 'Update Now';
}
// More specific error message based on the error type
if (error.name === 'AbortError') {
showNotification('Update request timed out. The server might still be processing the update.', 'warning');
} else if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
showNotification('Network error. Please check your connection and try again.', 'danger');
} else {
showNotification(error.message || 'Failed to connect to server', 'danger');
}
});
}
@@ -129,12 +499,73 @@ function initSystemStatus() {
updateButton.addEventListener('click', applyUpdate);
}
// Add handler for floating refresh button
const floatingRefreshButton = document.getElementById('floating-refresh-button');
if (floatingRefreshButton) {
floatingRefreshButton.addEventListener('click', () => {
// Force a hard refresh of everything
floatingRefreshButton.textContent = 'Refreshing...';
floatingRefreshButton.disabled = true;
// Force reload system status
loadSystemStatus();
// Force a check without the test parameter to get real status
const realCheckUrl = `/api/system/check-updates?_=${new Date().getTime()}`;
fetch(realCheckUrl, { headers: authHeaders() })
.then(response => response.json())
.then(data => {
console.log('Manual refresh result:', data);
if (data.status === 'success') {
// Check if we're in test mode
const isTestMode = localStorage.getItem('showUpdateButton') === 'true';
// If test mode is enabled but update says no update available, disable test mode
if (isTestMode && data.data && !data.data.updateAvailable) {
localStorage.setItem('showUpdateButton', 'false');
testToggle.innerText = 'Enable Test Update';
showNotification('Test mode has been disabled - no real update is available', 'info');
hideUpdateAlert();
showNotification(`Current version: ${data.data.currentVersion}. You are up to date.`, 'success');
}
// Regular update handling
else if (data.data && data.data.updateAvailable) {
showUpdateAlert(data.data.currentVersion, data.data.remoteVersion);
showNotification(`Update is available: ${data.data.currentVersion}${data.data.remoteVersion}`, 'info');
} else {
hideUpdateAlert();
showNotification(`Current version: ${data.data.currentVersion}. You are up to date.`, 'success');
}
}
// Re-enable button
floatingRefreshButton.textContent = 'Refresh Status';
floatingRefreshButton.disabled = false;
})
.catch(error => {
console.error('Error during manual refresh:', error);
floatingRefreshButton.textContent = 'Refresh Status';
floatingRefreshButton.disabled = false;
showNotification('Error checking update status', 'danger');
});
});
}
// Test mode toggle (for developers)
const testToggle = document.getElementById('toggle-test-update-button');
if (testToggle) {
// Initialize based on current localStorage setting
const isTestMode = localStorage.getItem('showUpdateButton') === 'true';
// If test mode is enabled but we have a version mismatch, update the stored version
if (isTestMode && versionElement && versionElement.textContent) {
const currentVersion = versionElement.textContent.trim();
if (localStorage.getItem(CURRENT_VERSION_KEY) !== currentVersion) {
localStorage.setItem(CURRENT_VERSION_KEY, currentVersion);
}
}
// Update toggle text
testToggle.innerText = isTestMode ? 'Disable Test Update' : 'Enable Test Update';
@@ -147,16 +578,206 @@ function initSystemStatus() {
localStorage.setItem('showUpdateButton', newSetting);
testToggle.innerText = newSetting ? 'Disable Test Update' : 'Enable Test Update';
// Re-check for updates with new setting
checkForUpdates();
if (newSetting) {
// Get the current version from the version element
let currentVersion = '2.0.11'; // Default fallback
if (versionElement && versionElement.textContent) {
currentVersion = versionElement.textContent.trim();
}
showNotification(`Test update button ${newSetting ? 'enabled' : 'disabled'}`, 'info');
// If enabling test mode, force show update button
showUpdateAlert(currentVersion, '2.1.0-test');
updateStatusElement.innerHTML = '<i class="fas fa-exclamation-circle text-warning"></i> Update available (TEST MODE)';
// Add test mode indicator to floating notification
const floatingNotification = document.getElementById('floating-update-notification');
if (floatingNotification) {
const testBadge = document.createElement('div');
testBadge.style.backgroundColor = 'orange';
testBadge.style.color = 'black';
testBadge.style.padding = '3px 8px';
testBadge.style.borderRadius = '4px';
testBadge.style.fontWeight = 'bold';
testBadge.style.marginBottom = '5px';
testBadge.style.fontSize = '12px';
testBadge.textContent = 'TEST MODE - NOT A REAL UPDATE';
// Insert at the top of the notification
floatingNotification.insertBefore(testBadge, floatingNotification.firstChild);
}
showNotification('TEST MODE ENABLED - This is not a real update', 'warning');
} else {
// If disabling test mode, check for real updates
hideUpdateAlert();
// Force a check without the test parameter to get real status
const realCheckUrl = '/api/system/check-updates';
fetch(realCheckUrl, { headers: authHeaders() })
.then(response => response.json())
.then(data => {
console.log('Real update check result:', data);
if (data.status === 'success' && data.data && !data.data.updateAvailable) {
showNotification('No actual updates are available.', 'info');
} else if (data.status === 'success' && data.data && data.data.updateAvailable) {
showUpdateAlert(data.data.currentVersion, data.data.remoteVersion);
showNotification(`A real update is available: ${data.data.currentVersion}${data.data.remoteVersion}`, 'info');
}
})
.catch(error => console.error('Error checking for real updates:', error));
showNotification('Test update button disabled', 'info');
}
});
}
// Persistent update button - force display every second if update is available
function forceShowUpdateButton() {
const isUpdateAvailable = localStorage.getItem(UPDATE_KEY) === 'true';
if (isUpdateAvailable) {
// Get the most current version
let currentVersion = localStorage.getItem(CURRENT_VERSION_KEY);
const remoteVersion = localStorage.getItem(REMOTE_VERSION_KEY);
// If we have the version element on screen, use that as the source of truth
if (versionElement && versionElement.textContent) {
const displayedVersion = versionElement.textContent.trim();
// Update stored version if different
if (displayedVersion !== currentVersion) {
localStorage.setItem(CURRENT_VERSION_KEY, displayedVersion);
currentVersion = displayedVersion;
}
}
if (currentVersion && remoteVersion) {
// Check floating notification
const floatingNotification = document.getElementById('floating-update-notification');
if (floatingNotification && floatingNotification.style.display !== 'block') {
console.log('Forcing floating update notification display');
// Set the version text
const versionElement = document.getElementById('floating-update-version');
if (versionElement) {
versionElement.textContent = `Version ${currentVersion}${remoteVersion}`;
}
// Apply strong styling - make sure to completely override any previous styles
floatingNotification.setAttribute('style', ''); // Clear any previous styles first
floatingNotification.setAttribute('style', ''); // Clear any previous styles first
floatingNotification.setAttribute('style',
'display: block !important; ' +
'visibility: visible !important; ' +
'opacity: 1 !important; ' +
'position: fixed !important; ' +
'top: 20px !important; ' +
'right: 20px !important; ' +
'width: 300px !important; ' +
'padding: 15px !important; ' +
'background-color: #ff5555 !important; ' +
'color: white !important; ' +
'border: 3px solid #cc0000 !important; ' +
'border-radius: 5px !important; ' +
'box-shadow: 0 0 20px rgba(0,0,0,0.5) !important; ' +
'z-index: 10000 !important; ' +
'font-weight: bold !important; ' +
'text-align: center !important;'
);
// Ensure button has correct event handler
const updateButton = document.getElementById('floating-update-button');
if (updateButton) {
// Remove any existing listeners
updateButton.removeEventListener('click', applyUpdate);
// Add new listener
updateButton.addEventListener('click', applyUpdate);
}
}
// Still try the original alert as a fallback
try {
const alertBox = updateAvailableDiv.querySelector('.alert');
if (alertBox && alertBox.style.display !== 'block') {
alertBox.style.display = 'block';
updateAvailableDiv.style.display = 'block';
// Update message
const spanElement = alertBox.querySelector('span');
if (spanElement) {
spanElement.textContent = `A new version is available: ${currentVersion}${remoteVersion}`;
}
}
} catch (e) {
console.error('Error forcing original update button:', e);
}
}
}
}
// Initialize
loadSystemStatus();
checkForUpdates();
// SUPER EMERGENCY FIX: Force hide and remove all update notifications
const emergencyFix = () => {
// Hard reset all update states
localStorage.clear(); // Clear ALL localStorage to be absolutely safe
// Find and destroy any floating notification elements
document.querySelectorAll('[id*="notification"], [id*="update"], [class*="notification"], [class*="update"]').forEach(el => {
try {
if (el.id !== 'update-status' && !el.id.includes('refresh')) {
el.style.cssText = 'display: none !important; visibility: hidden !important; opacity: 0 !important';
el.removeAttribute('style');
if (el.parentNode) {
el.parentNode.removeChild(el);
console.log('Element removed from DOM:', el.id || el.className || 'unnamed element');
}
}
} catch (e) {
console.error('Error removing element:', e);
}
});
// Add a MutationObserver to keep killing any notification elements that might reappear
if (!window._notificationKiller) {
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) { // Element node
if ((node.id && (node.id.includes('notification') || node.id.includes('update'))) ||
(node.className && (node.className.includes('notification') || node.className.includes('update')))) {
if (node.id !== 'update-status' && !node.id.includes('refresh')) {
node.style.cssText = 'display: none !important';
if (node.parentNode) {
node.parentNode.removeChild(node);
console.log('Dynamically added notification killed');
}
}
}
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
window._notificationKiller = observer;
}
console.log('Super emergency notification cleanup complete');
};
// Run immediately
emergencyFix();
// Run again after delays to ensure it works
setTimeout(emergencyFix, 100);
setTimeout(emergencyFix, 500);
setTimeout(emergencyFix, 1000);
// Set interval to refresh uptime every minute
setInterval(loadSystemStatus, 60000);

212
scripts/create-module-links.sh Executable file
View File

@@ -0,0 +1,212 @@
#!/bin/bash
# Script to create symlinks for all modules in different naming styles
# This ensures compatibility with different module import styles
APP_DIR="$(dirname "$(dirname "$(readlink -f "$0")")")"
MODULE_DIR="$APP_DIR/modules"
echo "Creating module symlinks for compatibility..."
echo "Module directory: $MODULE_DIR"
# Create a function to make bidirectional symlinks
create_module_symlinks() {
if [ ! -d "$MODULE_DIR" ]; then
echo "Error: Module directory not found at $MODULE_DIR"
mkdir -p "$MODULE_DIR"
echo "Created module directory: $MODULE_DIR"
fi
# Check if any .js files exist in the module directory
js_file_count=$(find "$MODULE_DIR" -maxdepth 1 -name "*.js" -type f | wc -l)
if [ "$js_file_count" -eq 0 ]; then
echo "Warning: No JavaScript module files found in $MODULE_DIR"
echo "Skipping symlink creation as there are no modules to link"
return 0
fi
# Create symlinks for hyphenated modules
for module in "$MODULE_DIR"/*-*.js; do
if [ -f "$module" ]; then
# Convert hyphenated to camelCase
BASE_NAME=$(basename "$module")
CAMEL_NAME=$(echo "$BASE_NAME" | sed -E 's/-([a-z])/\U\1/g')
# Create camelCase symlink if needed
if [ ! -f "$MODULE_DIR/$CAMEL_NAME" ] && [ ! -L "$MODULE_DIR/$CAMEL_NAME" ]; then
if ln -sf "$BASE_NAME" "$MODULE_DIR/$CAMEL_NAME"; then
echo "Created symlink: $CAMEL_NAME -> $BASE_NAME"
else
echo "Error: Failed to create symlink $CAMEL_NAME"
fi
fi
# Create extension-less symlink for both versions
NO_EXT_BASE="${BASE_NAME%.js}"
if [ ! -f "$MODULE_DIR/$NO_EXT_BASE" ] && [ ! -L "$MODULE_DIR/$NO_EXT_BASE" ]; then
if ln -sf "$BASE_NAME" "$MODULE_DIR/$NO_EXT_BASE"; then
echo "Created symlink: $NO_EXT_BASE -> $BASE_NAME"
else
echo "Error: Failed to create symlink $NO_EXT_BASE"
fi
fi
NO_EXT_CAMEL="${CAMEL_NAME%.js}"
if [ ! -f "$MODULE_DIR/$NO_EXT_CAMEL" ] && [ ! -L "$MODULE_DIR/$NO_EXT_CAMEL" ]; then
if ln -sf "$BASE_NAME" "$MODULE_DIR/$NO_EXT_CAMEL"; then
echo "Created symlink: $NO_EXT_CAMEL -> $BASE_NAME"
else
echo "Error: Failed to create symlink $NO_EXT_CAMEL"
fi
fi
fi
done
# Create symlinks for camelCase modules (only non-symlinked files)
for module in "$MODULE_DIR"/[a-z]*[A-Z]*.js; do
if [ -f "$module" ] && [ ! -L "$module" ]; then
# Convert camelCase to hyphenated
BASE_NAME=$(basename "$module")
HYPHEN_NAME=$(echo "$BASE_NAME" | sed -E 's/([a-z])([A-Z])/\1-\L\2/g')
# Create hyphenated symlink if needed
if [ ! -f "$MODULE_DIR/$HYPHEN_NAME" ] && [ ! -L "$MODULE_DIR/$HYPHEN_NAME" ]; then
if ln -sf "$BASE_NAME" "$MODULE_DIR/$HYPHEN_NAME"; then
echo "Created symlink: $HYPHEN_NAME -> $BASE_NAME"
else
echo "Error: Failed to create symlink $HYPHEN_NAME"
fi
fi
# Create extension-less symlink for both versions
NO_EXT_BASE="${BASE_NAME%.js}"
if [ ! -f "$MODULE_DIR/$NO_EXT_BASE" ] && [ ! -L "$MODULE_DIR/$NO_EXT_BASE" ]; then
if ln -sf "$BASE_NAME" "$MODULE_DIR/$NO_EXT_BASE"; then
echo "Created symlink: $NO_EXT_BASE -> $BASE_NAME"
else
echo "Error: Failed to create symlink $NO_EXT_BASE"
fi
fi
NO_EXT_HYPHEN="${HYPHEN_NAME%.js}"
if [ ! -f "$MODULE_DIR/$NO_EXT_HYPHEN" ] && [ ! -L "$MODULE_DIR/$NO_EXT_HYPHEN" ]; then
if ln -sf "$BASE_NAME" "$MODULE_DIR/$NO_EXT_HYPHEN"; then
echo "Created symlink: $NO_EXT_HYPHEN -> $BASE_NAME"
else
echo "Error: Failed to create symlink $NO_EXT_HYPHEN"
fi
fi
fi
done
echo "Module symlinks created successfully"
}
# Setup production directory if needed
setup_production_dir() {
# Check if this is running in development environment
DEV_DIR="/opt/develop/transmission-rss-manager"
# Check systemd service file to determine the correct production directory
PROD_DIR="/opt/transmission-rss-manager"
SERVICE_FILE="/etc/systemd/system/transmission-rss-manager.service"
if [ -f "$SERVICE_FILE" ]; then
# Extract the WorkingDirectory from the service file
WORKING_DIR=$(grep "WorkingDirectory=" "$SERVICE_FILE" | cut -d'=' -f2)
if [ -n "$WORKING_DIR" ]; then
PROD_DIR="$WORKING_DIR"
echo "Found production directory from service file: $PROD_DIR"
fi
fi
if [ "$APP_DIR" == "$DEV_DIR" ] && [ -d "$DEV_DIR" ]; then
echo "Setting up production directory symlinks at $PROD_DIR..."
# Create the production directory if it doesn't exist
if [ ! -d "$PROD_DIR" ]; then
if mkdir -p "$PROD_DIR"; then
echo "Created production directory: $PROD_DIR"
else
echo "Error: Failed to create production directory $PROD_DIR"
return 1
fi
fi
# Create the modules directory in production if it doesn't exist
if [ ! -d "$PROD_DIR/modules" ]; then
if mkdir -p "$PROD_DIR/modules"; then
echo "Created production modules directory: $PROD_DIR/modules"
else
echo "Error: Failed to create production modules directory"
return 1
fi
fi
# Check for JavaScript modules in dev directory
js_file_count=$(find "$MODULE_DIR" -maxdepth 1 -name "*.js" -type f | wc -l)
if [ "$js_file_count" -eq 0 ]; then
echo "Warning: No JavaScript module files found in $MODULE_DIR"
echo "Skipping production symlink creation"
else
# Create symlinks from development modules to production modules
for module in "$MODULE_DIR"/*.js; do
if [ -f "$module" ] && [ ! -L "$module" ]; then
MODULE_NAME=$(basename "$module")
# Create symlink in production directory
if ln -sf "$module" "$PROD_DIR/modules/$MODULE_NAME"; then
echo "Created production symlink: $PROD_DIR/modules/$MODULE_NAME -> $module"
else
echo "Error: Failed to create production symlink for $MODULE_NAME"
fi
fi
done
fi
# Copy server.js to production if it doesn't exist or needs updating
if [ -f "$DEV_DIR/server.js" ]; then
if [ ! -f "$PROD_DIR/server.js" ] || [ "$DEV_DIR/server.js" -nt "$PROD_DIR/server.js" ]; then
if cp "$DEV_DIR/server.js" "$PROD_DIR/server.js"; then
echo "Copied server.js to production directory"
else
echo "Error: Failed to copy server.js to production"
fi
fi
else
echo "Warning: server.js not found in development directory"
fi
# Create data directory in production if it doesn't exist
if mkdir -p "$PROD_DIR/data"; then
echo "Ensured data directory exists in production"
else
echo "Error: Failed to create production data directory"
fi
# Make sure scripts directory exists in production
if mkdir -p "$PROD_DIR/scripts"; then
echo "Ensured scripts directory exists in production"
else
echo "Error: Failed to create production scripts directory"
fi
# Copy test-and-start.sh to production
if [ -f "$DEV_DIR/scripts/test-and-start.sh" ]; then
if cp "$DEV_DIR/scripts/test-and-start.sh" "$PROD_DIR/scripts/test-and-start.sh"; then
chmod +x "$PROD_DIR/scripts/test-and-start.sh"
echo "Copied test-and-start.sh script to production"
else
echo "Error: Failed to copy test-and-start.sh to production"
fi
else
echo "Warning: test-and-start.sh not found in development scripts directory"
fi
echo "Production directory setup complete"
fi
}
# Execute the symlink creation function
create_module_symlinks
# Setup production directory if needed
setup_production_dir

View File

@@ -1,165 +1,140 @@
#!/bin/bash
# Test and start script for Transmission RSS Manager
# This script checks the installation, dependencies, and starts the application
# Script to ensure data directory exists and start the application
# Text formatting
BOLD='\033[1m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Define paths
APP_DIR="$(dirname "$(dirname "$(readlink -f "$0")")")"
DATA_DIR="$APP_DIR/data"
# Get directory of this script
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
APP_DIR="$(dirname "$SCRIPT_DIR")"
# Function to check if a command exists
command_exists() {
command -v "$1" &> /dev/null
}
# Check Node.js and npm
check_node() {
echo -e "${BOLD}Checking Node.js and npm...${NC}"
if command_exists node; then
NODE_VERSION=$(node -v)
echo -e "${GREEN}Node.js is installed: $NODE_VERSION${NC}"
else
echo -e "${RED}Node.js is not installed. Please install Node.js 14 or later.${NC}"
exit 1
fi
if command_exists npm; then
NPM_VERSION=$(npm -v)
echo -e "${GREEN}npm is installed: $NPM_VERSION${NC}"
else
echo -e "${RED}npm is not installed. Please install npm.${NC}"
exit 1
fi
}
# Check if Transmission is running
check_transmission() {
echo -e "${BOLD}Checking Transmission...${NC}"
# Try to get the status of the transmission-daemon service
if command_exists systemctl; then
if systemctl is-active --quiet transmission-daemon; then
echo -e "${GREEN}Transmission daemon is running${NC}"
else
echo -e "${YELLOW}Warning: Transmission daemon does not appear to be running${NC}"
echo -e "${YELLOW}You may need to start it with: sudo systemctl start transmission-daemon${NC}"
fi
else
# Try a different method if systemctl is not available
if pgrep -x "transmission-daemon" > /dev/null; then
echo -e "${GREEN}Transmission daemon is running${NC}"
else
echo -e "${YELLOW}Warning: Transmission daemon does not appear to be running${NC}"
echo -e "${YELLOW}Please start Transmission daemon before using this application${NC}"
fi
fi
}
# Check dependencies in package.json
check_dependencies() {
echo -e "${BOLD}Checking dependencies...${NC}"
# Check if node_modules exists
if [ ! -d "$APP_DIR/node_modules" ]; then
echo -e "${YELLOW}Node modules not found. Installing dependencies...${NC}"
cd "$APP_DIR" && npm install
echo "Starting Transmission RSS Manager..."
echo "Application directory: $APP_DIR"
echo "Data directory: $DATA_DIR"
# Ensure the data directory exists
if [ ! -d "$DATA_DIR" ]; then
echo "Creating data directory: $DATA_DIR"
mkdir -p "$DATA_DIR"
if [ $? -ne 0 ]; then
echo -e "${RED}Failed to install dependencies.${NC}"
echo "Failed to create data directory. Trying alternative method..."
# Try alternative method if standard mkdir fails
cd "$APP_DIR" && mkdir -p data
if [ $? -ne 0 ]; then
echo "ERROR: Both methods to create data directory failed. Please check permissions."
exit 1
else
echo -e "${GREEN}Dependencies installed successfully${NC}"
fi
else
echo -e "${GREEN}Dependencies are already installed${NC}"
fi
fi
# Set permissions
chmod -R 755 "$DATA_DIR" || {
echo "Warning: Failed to set permissions on data directory"
}
# Check if config.json exists
check_config() {
echo -e "${BOLD}Checking configuration...${NC}"
if [ ! -f "$APP_DIR/config.json" ]; then
echo -e "${RED}Configuration file not found: $APP_DIR/config.json${NC}"
echo -e "${YELLOW}Please run the installer or create a config.json file${NC}"
# Check for RSS files
if [ ! -f "$DATA_DIR/rss-feeds.json" ]; then
echo "Creating initial empty rss-feeds.json file"
echo "[]" > "$DATA_DIR/rss-feeds.json" || {
echo "ERROR: Failed to create rss-feeds.json file"
exit 1
else
echo -e "${GREEN}Configuration file found${NC}"
fi
}
# Start the application
start_app() {
echo -e "${BOLD}Starting Transmission RSS Manager...${NC}"
# Check if running as a service
if command_exists systemctl; then
if systemctl is-active --quiet transmission-rss-manager; then
echo -e "${YELLOW}Transmission RSS Manager is already running as a service${NC}"
echo -e "${YELLOW}To restart it, use: sudo systemctl restart transmission-rss-manager${NC}"
exit 0
fi
fi
# Start the application
cd "$APP_DIR"
if [ ! -f "$DATA_DIR/rss-items.json" ]; then
echo "Creating initial empty rss-items.json file"
echo "[]" > "$DATA_DIR/rss-items.json" || {
echo "ERROR: Failed to create rss-items.json file"
exit 1
}
fi
# Parse arguments
FOREGROUND=false
DEBUG=false
while [[ "$#" -gt 0 ]]; do
case $1 in
--foreground|-f) FOREGROUND=true ;;
--debug|-d) DEBUG=true ;;
*) echo "Unknown parameter: $1"; exit 1 ;;
esac
shift
# Find the node executable path
NODE_PATH=$(which node 2>/dev/null)
if [ -z "$NODE_PATH" ]; then
# If node is not in PATH, try common locations
for path in /usr/bin/node /usr/local/bin/node /opt/node/bin/node /usr/lib/node; do
if [ -x "$path" ]; then
NODE_PATH="$path"
break
fi
done
if [ "$FOREGROUND" = true ]; then
echo -e "${GREEN}Starting in foreground mode...${NC}"
if [ "$DEBUG" = true ]; then
echo -e "${YELLOW}Debug mode enabled${NC}"
DEBUG_ENABLED=true node server.js
else
node server.js
# If we still can't find node, use the default path
if [ -z "$NODE_PATH" ]; then
NODE_PATH="/usr/bin/node"
echo "Warning: Node.js not found in PATH, using default path: $NODE_PATH"
fi
else
echo -e "${GREEN}Starting in background mode...${NC}"
if [ "$DEBUG" = true ]; then
echo -e "${YELLOW}Debug mode enabled${NC}"
DEBUG_ENABLED=true nohup node server.js > logs/output.log 2>&1 &
else
nohup node server.js > logs/output.log 2>&1 &
fi
echo $! > "$APP_DIR/transmission-rss-manager.pid"
echo -e "${GREEN}Application started with PID: $!${NC}"
echo -e "${GREEN}Logs available at: $APP_DIR/logs/output.log${NC}"
# Create module symlinks to ensure compatibility
echo "Creating module symlinks for compatibility..."
MODULE_DIR="$APP_DIR/modules"
# Create a function to make bidirectional symlinks
create_module_symlinks() {
if [ -d "$MODULE_DIR" ]; then
# Create symlinks for hyphenated modules
for module in "$MODULE_DIR"/*-*.js; do
if [ -f "$module" ]; then
# Convert hyphenated to camelCase
BASE_NAME=$(basename "$module")
CAMEL_NAME=$(echo "$BASE_NAME" | sed -E 's/-([a-z])/\U\1/g')
# Create camelCase symlink if needed
if [ ! -f "$MODULE_DIR/$CAMEL_NAME" ]; then
ln -sf "$BASE_NAME" "$MODULE_DIR/$CAMEL_NAME"
echo "Created symlink: $CAMEL_NAME -> $BASE_NAME"
fi
# Create extension-less symlink for both versions
NO_EXT_BASE="${BASE_NAME%.js}"
if [ ! -f "$MODULE_DIR/$NO_EXT_BASE" ]; then
ln -sf "$BASE_NAME" "$MODULE_DIR/$NO_EXT_BASE"
echo "Created symlink: $NO_EXT_BASE -> $BASE_NAME"
fi
NO_EXT_CAMEL="${CAMEL_NAME%.js}"
if [ ! -f "$MODULE_DIR/$NO_EXT_CAMEL" ]; then
ln -sf "$BASE_NAME" "$MODULE_DIR/$NO_EXT_CAMEL"
echo "Created symlink: $NO_EXT_CAMEL -> $BASE_NAME"
fi
fi
done
# Create symlinks for camelCase modules
for module in "$MODULE_DIR"/[a-z]*[A-Z]*.js; do
if [ -f "$module" ]; then
# Convert camelCase to hyphenated
BASE_NAME=$(basename "$module")
HYPHEN_NAME=$(echo "$BASE_NAME" | sed -E 's/([a-z])([A-Z])/\1-\L\2/g')
# Create hyphenated symlink if needed
if [ ! -f "$MODULE_DIR/$HYPHEN_NAME" ]; then
ln -sf "$BASE_NAME" "$MODULE_DIR/$HYPHEN_NAME"
echo "Created symlink: $HYPHEN_NAME -> $BASE_NAME"
fi
# Create extension-less symlink for both versions
NO_EXT_BASE="${BASE_NAME%.js}"
if [ ! -f "$MODULE_DIR/$NO_EXT_BASE" ]; then
ln -sf "$BASE_NAME" "$MODULE_DIR/$NO_EXT_BASE"
echo "Created symlink: $NO_EXT_BASE -> $BASE_NAME"
fi
NO_EXT_HYPHEN="${HYPHEN_NAME%.js}"
if [ ! -f "$MODULE_DIR/$NO_EXT_HYPHEN" ]; then
ln -sf "$BASE_NAME" "$MODULE_DIR/$NO_EXT_HYPHEN"
echo "Created symlink: $NO_EXT_HYPHEN -> $BASE_NAME"
fi
fi
done
else
echo "Warning: Module directory not found at $MODULE_DIR"
fi
}
# Main script
echo -e "${BOLD}==================================================${NC}"
echo -e "${BOLD} Transmission RSS Manager - Test & Start ${NC}"
echo -e "${BOLD}==================================================${NC}"
echo
# Run checks
check_node
check_transmission
check_dependencies
check_config
# Execute the symlink creation function
create_module_symlinks
# Start the application
start_app "$@"
cd "$APP_DIR" || { echo "Failed to change to application directory"; exit 1; }
echo "Starting node.js application with: $NODE_PATH $APP_DIR/server.js"
exec "$NODE_PATH" "$APP_DIR/server.js"

View File

@@ -56,6 +56,11 @@ fi
# Install any new npm dependencies
echo -e "${YELLOW}Installing dependencies...${NC}"
npm install
if [ $? -ne 0 ]; then
echo -e "${RED}Failed to install npm dependencies. Update aborted.${NC}"
echo -e "Please check the error messages above and try again."
exit 1
fi
# Apply any local configuration changes
if git stash list | grep -q "stash@{0}"; then

434
server.js
View File

@@ -5,7 +5,8 @@
const express = require('express');
const path = require('path');
const fs = require('fs').promises;
const fsPromises = require('fs').promises;
const fs = require('fs'); // Regular fs module for synchronous operations
const bodyParser = require('body-parser');
const cors = require('cors');
const morgan = require('morgan');
@@ -13,11 +14,73 @@ const http = require('http');
const https = require('https');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const { exec } = require('child_process');
const util = require('util');
const execAsync = util.promisify(exec);
const semver = require('semver'); // For semantic version comparison
// Import custom modules
const RssFeedManager = require('./modules/rss-feed-manager.js');
const TransmissionClient = require('./modules/transmission-client.js');
const PostProcessor = require('./modules/post-processor.js');
let RssFeedManager, TransmissionClient, PostProcessor;
/**
* Helper function to try multiple module paths
* This function tries to require a module using different naming conventions
* to work around issues with module resolution in different Node.js environments
*
* @param {string} baseName - The base module name without extension or path
* @returns {Object} The loaded module
* @throws {Error} If module cannot be loaded from any path
*/
function loadModule(baseName) {
// Generate all possible module paths
const paths = [
`./modules/${baseName}.js`, // With extension
`./modules/${baseName}`, // Without extension
// Convert hyphenated to camelCase
`./modules/${baseName.replace(/-([a-z])/g, (_, c) => c.toUpperCase())}.js`,
`./modules/${baseName.replace(/-([a-z])/g, (_, c) => c.toUpperCase())}`,
// Convert camelCase to hyphenated
`./modules/${baseName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}.js`,
`./modules/${baseName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}`
];
console.log(`Attempting to load module: ${baseName}`);
let lastError = null;
for (const modulePath of paths) {
try {
return require(modulePath);
} catch (err) {
if (err.code !== 'MODULE_NOT_FOUND' || !err.message.includes(modulePath)) {
// This is a real error in the module, not just not finding it
throw err;
}
lastError = err;
}
}
// If we get here, we couldn't load the module from any path
const error = new Error(`Could not load module '${baseName}' from any path. Original error: ${lastError.message}`);
error.original = lastError;
throw error;
}
// Try loading modules with improved error reporting
try {
RssFeedManager = loadModule('rss-feed-manager');
console.log('Successfully loaded RssFeedManager module');
TransmissionClient = loadModule('transmission-client');
console.log('Successfully loaded TransmissionClient module');
PostProcessor = loadModule('post-processor');
console.log('Successfully loaded PostProcessor module');
} catch (err) {
console.error('Fatal error loading required module:', err.message);
console.error('Please make sure all module files exist and are valid JavaScript');
process.exit(1);
}
// Constants and configuration
const DEFAULT_CONFIG_PATH = '/etc/transmission-rss-manager/config.json';
@@ -27,8 +90,20 @@ const JWT_SECRET = process.env.JWT_SECRET || 'transmission-rss-manager-secret';
const JWT_EXPIRY = '24h';
// Get the version from package.json (single source of truth)
// Re-read the package.json file each time to ensure we get the latest version
const APP_VERSION = (() => {
try {
// Use synchronous file read to ensure we have the version before continuing
const packageJsonPath = path.join(__dirname, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
return packageJson.version;
} catch (err) {
console.error('Error reading package.json version:', err);
// Fallback to requiring package.json if file read fails
const PACKAGE_JSON = require('./package.json');
const APP_VERSION = PACKAGE_JSON.version;
return PACKAGE_JSON.version;
}
})();
// Create Express app
const app = express();
@@ -153,7 +228,7 @@ async function loadConfig() {
try {
// Try to read existing config from primary location
console.log(`Trying to load config from: ${DEFAULT_CONFIG_PATH}`);
const configData = await fs.readFile(DEFAULT_CONFIG_PATH, 'utf8');
const configData = await fsPromises.readFile(DEFAULT_CONFIG_PATH, 'utf8');
const loadedConfig = JSON.parse(configData);
// Use recursive merge function to merge configs
@@ -174,7 +249,7 @@ async function loadConfig() {
console.log(`Config not found at ${DEFAULT_CONFIG_PATH}, trying fallback location...`);
try {
const fallbackData = await fs.readFile(FALLBACK_CONFIG_PATH, 'utf8');
const fallbackData = await fsPromises.readFile(FALLBACK_CONFIG_PATH, 'utf8');
const fallbackConfig = JSON.parse(fallbackData);
// Merge configs
@@ -189,8 +264,8 @@ async function loadConfig() {
// Try to save to primary location, but don't fail if we can't
try {
// Create directory if it doesn't exist
await fs.mkdir(path.dirname(DEFAULT_CONFIG_PATH), { recursive: true });
await fs.writeFile(DEFAULT_CONFIG_PATH, JSON.stringify(mergedConfig, null, 2), 'utf8');
await fsPromises.mkdir(path.dirname(DEFAULT_CONFIG_PATH), { recursive: true });
await fsPromises.writeFile(DEFAULT_CONFIG_PATH, JSON.stringify(mergedConfig, null, 2), 'utf8');
console.log(`Migrated config from ${FALLBACK_CONFIG_PATH} to ${DEFAULT_CONFIG_PATH}`);
} catch (saveError) {
console.warn(`Could not save config to ${DEFAULT_CONFIG_PATH}: ${saveError.message}`);
@@ -206,15 +281,15 @@ async function loadConfig() {
// Try to save to primary location first
try {
await fs.mkdir(path.dirname(DEFAULT_CONFIG_PATH), { recursive: true });
await fs.writeFile(DEFAULT_CONFIG_PATH, JSON.stringify(defaultConfig, null, 2), 'utf8');
await fsPromises.mkdir(path.dirname(DEFAULT_CONFIG_PATH), { recursive: true });
await fsPromises.writeFile(DEFAULT_CONFIG_PATH, JSON.stringify(defaultConfig, null, 2), 'utf8');
console.log(`Created default config at ${DEFAULT_CONFIG_PATH}`);
} catch (saveError) {
console.warn(`Could not save config to ${DEFAULT_CONFIG_PATH}: ${saveError.message}`);
console.warn('Saving to fallback location instead');
// Save to fallback location instead
await fs.writeFile(FALLBACK_CONFIG_PATH, JSON.stringify(defaultConfig, null, 2), 'utf8');
await fsPromises.writeFile(FALLBACK_CONFIG_PATH, JSON.stringify(defaultConfig, null, 2), 'utf8');
console.log(`Created default config at ${FALLBACK_CONFIG_PATH}`);
}
@@ -289,8 +364,8 @@ async function saveConfig(config) {
// Always try to save to the primary config location first
try {
// Make sure directory exists
await fs.mkdir(path.dirname(DEFAULT_CONFIG_PATH), { recursive: true });
await fs.writeFile(DEFAULT_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
await fsPromises.mkdir(path.dirname(DEFAULT_CONFIG_PATH), { recursive: true });
await fsPromises.writeFile(DEFAULT_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
console.log(`Configuration saved to ${DEFAULT_CONFIG_PATH}`);
return;
} catch (primaryError) {
@@ -298,7 +373,7 @@ async function saveConfig(config) {
console.warn('Trying fallback location...');
// If we couldn't save to the primary location, try the fallback
await fs.writeFile(FALLBACK_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
await fsPromises.writeFile(FALLBACK_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
console.log(`Configuration saved to fallback location: ${FALLBACK_CONFIG_PATH}`);
}
} catch (error) {
@@ -324,8 +399,8 @@ async function startServer() {
config.securitySettings?.sslKeyPath) {
try {
const sslOptions = {
key: await fs.readFile(config.securitySettings.sslKeyPath),
cert: await fs.readFile(config.securitySettings.sslCertPath)
key: await fsPromises.readFile(config.securitySettings.sslKeyPath),
cert: await fsPromises.readFile(config.securitySettings.sslCertPath)
};
server = https.createServer(sslOptions, app);
@@ -397,7 +472,7 @@ app.get('/api/status', authenticateJWT, async (req, res) => {
res.json({
success: true,
status: 'running',
version: '1.2.0',
version: APP_VERSION, // Use the dynamic APP_VERSION from package.json
transmissionConnected: transmissionStatus.connected,
transmissionVersion: transmissionStatus.version,
transmissionStats: {
@@ -447,9 +522,45 @@ app.post('/api/config', authenticateJWT, async (req, res) => {
// 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;
// Preserve existing Transmission configuration values when not explicitly provided
if (newConfig.transmissionConfig && config.transmissionConfig) {
// First create a copy of the existing configuration
const preservedTransConfig = { ...config.transmissionConfig };
// Only update values that are explicitly provided and not empty
if (!req.body.transmissionConfig?.host) {
newConfig.transmissionConfig.host = preservedTransConfig.host;
}
if (!req.body.transmissionConfig?.port) {
newConfig.transmissionConfig.port = preservedTransConfig.port;
}
if (!req.body.transmissionConfig?.path) {
newConfig.transmissionConfig.path = preservedTransConfig.path;
}
if (!req.body.transmissionConfig?.username) {
newConfig.transmissionConfig.username = preservedTransConfig.username;
}
// Always preserve password if not provided
if (!newConfig.transmissionConfig.password) {
newConfig.transmissionConfig.password = preservedTransConfig.password;
}
}
// Preserve remote configuration settings if not explicitly provided
if (newConfig.remoteConfig && config.remoteConfig) {
// Make sure isRemote setting is preserved if not explicitly set
if (req.body.remoteConfig?.isRemote === undefined) {
newConfig.remoteConfig.isRemote = config.remoteConfig.isRemote;
}
// Preserve directory mappings if not provided
if (!req.body.remoteConfig?.directoryMapping && config.remoteConfig.directoryMapping) {
newConfig.remoteConfig.directoryMapping = { ...config.remoteConfig.directoryMapping };
}
}
// Keep user passwords
@@ -828,7 +939,15 @@ app.post('/api/transmission/remove', authenticateJWT, async (req, res) => {
app.post('/api/transmission/test', authenticateJWT, async (req, res) => {
try {
const { host, port, username, password } = req.body;
const { host, port, username, password, path: rpcPath } = req.body;
// Debug info
console.log('Testing Transmission connection with params:', {
host: host || 'from config',
port: port || 'from config',
username: username ? 'provided' : 'from config',
path: rpcPath || 'from config',
});
// Create a temporary client for testing
const testConfig = {
@@ -837,10 +956,25 @@ app.post('/api/transmission/test', authenticateJWT, async (req, res) => {
port: port || config.transmissionConfig.port,
username: username || config.transmissionConfig.username,
password: password || config.transmissionConfig.password,
path: config.transmissionConfig.path
path: rpcPath || config.transmissionConfig.path
},
// Also include remoteConfig to ensure proper remote handling
remoteConfig: {
// If host is provided and different from localhost, set isRemote to true
isRemote: host && host !== 'localhost' ? true : config.remoteConfig?.isRemote || false,
directoryMapping: config.remoteConfig?.directoryMapping || {}
}
};
// Log the actual test config (without password)
console.log('Test configuration:', {
host: testConfig.transmissionConfig.host,
port: testConfig.transmissionConfig.port,
path: testConfig.transmissionConfig.path,
isRemote: testConfig.remoteConfig.isRemote
});
try {
const testClient = new TransmissionClient(testConfig);
const status = await testClient.getStatus();
@@ -859,6 +993,13 @@ app.post('/api/transmission/test', authenticateJWT, async (req, res) => {
message: `Failed to connect: ${status.error}`
});
}
} catch (error) {
console.error('Error testing Transmission connection:', error);
res.status(500).json({
success: false,
message: `Error testing connection: ${error.message}`
});
}
} catch (error) {
res.status(500).json({
success: false,
@@ -958,17 +1099,17 @@ async function getMediaLibrary(searchQuery) {
try {
// Check if directory exists
await fs.access(destinationPath);
await fsPromises.access(destinationPath);
// Get directory listing
const files = await fs.readdir(destinationPath, { withFileTypes: true });
const files = await fsPromises.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);
const stats = await fsPromises.stat(fullPath);
// Create an item object
const item = {
@@ -1109,6 +1250,247 @@ app.get('/api/auth/validate', authenticateJWT, (req, res) => {
});
});
// System endpoints - consolidated from server-endpoints.js
// Ensure TransmissionClient has the necessary methods
if (!TransmissionClient.prototype.sessionGet && TransmissionClient.prototype.getStatus) {
// Add compatibility method if missing
TransmissionClient.prototype.sessionGet = async function() {
return this.getStatus();
};
}
// System status endpoint
app.get('/api/system/status', authenticateJWT, async (req, res) => {
try {
// Get system uptime
const uptimeSeconds = Math.floor(process.uptime());
const hours = Math.floor(uptimeSeconds / 3600);
const minutes = Math.floor((uptimeSeconds % 3600) / 60);
const seconds = uptimeSeconds % 60;
const uptime = `${hours}h ${minutes}m ${seconds}s`;
// Check transmission connection
let transmissionStatus = 'Connected';
try {
const status = await transmissionClient.sessionGet();
if (!status || (status.connected === false)) {
transmissionStatus = 'Disconnected';
}
} catch (err) {
console.error('Transmission connection error:', err);
transmissionStatus = 'Disconnected';
}
// Read version directly from package.json to ensure it's always current
let currentVersion = APP_VERSION;
try {
const packageJsonPath = path.join(__dirname, 'package.json');
const packageJsonContent = await fsPromises.readFile(packageJsonPath, 'utf8');
const packageData = JSON.parse(packageJsonContent);
currentVersion = packageData.version;
// Log the version for debugging
console.log(`System status endpoint returning version: ${currentVersion}`);
} catch (err) {
console.error('Error reading package.json in status endpoint:', err);
// Fall back to the cached APP_VERSION
}
res.json({
status: 'success',
data: {
version: currentVersion,
uptime,
transmissionStatus
}
});
} catch (error) {
console.error('Error getting system status:', error);
res.status(500).json({
status: 'error',
message: 'Failed to get system status'
});
}
});
// Check for updates
app.get('/api/system/check-updates', authenticateJWT, async (req, res) => {
try {
// Check if git is available and if this is a git repository
let isGitRepo = false;
let isGitAvailable = false;
try {
// First check if git command is available
await execAsync('which git');
isGitAvailable = true;
// Then check if directory is a git repository
isGitRepo = await fsPromises.access(path.join(__dirname, '.git'))
.then(() => true)
.catch(() => false);
} catch (error) {
console.error('Error checking git availability:', error);
isGitAvailable = false;
}
if (!isGitAvailable) {
return res.json({
status: 'error',
message: 'Git is not installed or not available. Please install Git to enable updates.'
});
}
if (!isGitRepo) {
return res.json({
status: 'error',
message: 'This installation is not set up as a Git repository. Please use the bootstrap installer.'
});
}
// Get current version
const currentVersion = APP_VERSION;
// Check for test mode flag which forces update availability for testing
const testMode = req.query.test === 'true';
if (testMode) {
// In test mode, always return that an update is available
return res.json({
status: 'success',
data: {
updateAvailable: true,
currentVersion,
remoteVersion: '2.1.0-test',
commitsBehind: 1,
testMode: true
}
});
}
try {
// Normal mode - fetch latest updates without applying them
await execAsync('git fetch');
// Check if we're behind the remote repository
const { stdout } = await execAsync('git rev-list HEAD..origin/main --count');
const behindCount = parseInt(stdout.trim());
if (behindCount > 0) {
// Get the new version from the remote package.json
const { stdout: remotePackageJson } = await execAsync('git show origin/main:package.json');
const remotePackage = JSON.parse(remotePackageJson);
const remoteVersion = remotePackage.version;
// Compare versions semantically - only consider it an update if remote version is higher
const isNewerVersion = semver.gt(remoteVersion, currentVersion);
return res.json({
status: 'success',
data: {
updateAvailable: isNewerVersion,
currentVersion,
remoteVersion,
commitsBehind: behindCount,
newerVersion: isNewerVersion
}
});
} else {
return res.json({
status: 'success',
data: {
updateAvailable: false,
currentVersion
}
});
}
} catch (gitError) {
console.error('Git error checking for updates:', gitError);
// Even if git commands fail, return a valid response with error information
return res.json({
status: 'error',
message: 'Error checking git repository: ' + gitError.message,
data: {
updateAvailable: false,
currentVersion,
error: true,
errorDetails: gitError.message
}
});
}
} catch (error) {
console.error('Error checking for updates:', error);
res.status(500).json({
status: 'error',
message: 'Failed to check for updates: ' + error.message
});
}
});
// Apply updates
app.post('/api/system/update', authenticateJWT, async (req, res) => {
try {
// Check if git is available and if this is a git repository
let isGitRepo = false;
let isGitAvailable = false;
try {
// First check if git command is available
await execAsync('which git');
isGitAvailable = true;
// Then check if directory is a git repository
isGitRepo = await fsPromises.access(path.join(__dirname, '.git'))
.then(() => true)
.catch(() => false);
} catch (error) {
console.error('Error checking git availability:', error);
isGitAvailable = false;
}
if (!isGitAvailable) {
return res.status(400).json({
status: 'error',
message: 'Git is not installed or not available. Please install Git to enable updates.'
});
}
if (!isGitRepo) {
return res.status(400).json({
status: 'error',
message: 'This installation is not set up as a Git repository. Please use the bootstrap installer.'
});
}
// Run the update script
const updateScriptPath = path.join(__dirname, 'scripts', 'update.sh');
// Make sure the update script is executable
await execAsync(`chmod +x ${updateScriptPath}`);
// Execute the update script
const { stdout, stderr } = await execAsync(updateScriptPath);
// If we get here, the update was successful
// The service will be restarted by the update script
res.json({
status: 'success',
message: 'Update applied successfully. The service will restart.',
data: {
output: stdout,
errors: stderr
}
});
} catch (error) {
console.error('Error applying update:', error);
res.status(500).json({
status: 'error',
message: 'Failed to apply update: ' + error.message
});
}
});
// Catch-all route for SPA
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));