63 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
1c16243a2d Bump version to 2.0.5 and update changelog
- Update version number in package.json to 2.0.5
- Add changelog entry for version 2.0.5 with recent fixes:
  - Config file properly stored in /etc/transmission-rss-manager
  - Fixed remote Transmission detection in install-script.sh
  - Enhanced symlink handling between installation and config directories
  - Better environment variable passing between install scripts
2025-03-05 09:49:30 +00:00
306049abdd Fix config directory handling in install script
- Ensure CONFIG_DIR exists during installation
- Create proper symlink from INSTALL_DIR/config.json to CONFIG_DIR/config.json
- Use CONFIG_DIR instead of INSTALL_DIR for configuration files
- Add code to move existing config file to CONFIG_DIR if needed

This fixes the issue where the config.json file was not being created in /etc/transmission-rss-manager, even though the directory was created.
2025-03-05 09:47:29 +00:00
1d99962024 Remove debug code and fix duplicate lines 2025-03-05 09:40:10 +00:00
95a086dff7 Fix input recognition in remote selection and add debug information 2025-03-05 09:36:48 +00:00
455006992d Fix Transmission remote configuration handling
This commit addresses an issue where the Transmission remote setting wasn't properly preserved between install scripts.
Major changes:

- Fixed environment variable sharing between install-script.sh and main-installer.sh
- Added multiple methods to locate and load the .env.install file
- Removed duplicate log message in dependencies-module.sh
- Improved permission handling for the environment file
- Directly export TRANSMISSION_REMOTE variable before launching main-installer.sh

This ensures the user's choice of remote or local Transmission is correctly honored throughout the installation process.

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-05 09:35:10 +00:00
b54086cfd0 Add fallback prompt in dependencies module
- Add direct prompt for remote Transmission as fallback in dependencies module
- Save choice to environment file for other scripts
- Add additional logging to show which path was taken
- Ensures remote transmission is always asked about, regardless of execution path

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-05 08:59:06 +00:00
aa3978903b Add robust environment sharing for install script
- Create .env.install file to store configuration choices
- Modify main-installer.sh to load environment variables
- Use direct sourcing to ensure environment variables are properly passed
- Add explicit logging of environment variable values
- Implement safer method of handling environment across script execution

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-05 08:58:50 +00:00
a1773d0bae Fix install-script.sh handling of remote Transmission
- Add direct remote Transmission prompt in install-script.sh
- Pass TRANSMISSION_REMOTE environment variable to main-installer.sh
- Add clarifying message when asking about installing Transmission daemon
- This fixes execution path when using install-script.sh instead of bootstrap-installer.sh

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-05 08:55:11 +00:00
1795373b42 Direct prompt for remote Transmission in main installer
- Move the remote Transmission prompt to main installer before config module is called
- Modify config module to use the already-set TRANSMISSION_REMOTE variable
- Remove existing logic in config module that was causing issues
- Clean up environment variables when invoking installer from bootstrap
- Remove emergency fallback since we now handle it in the right order

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-05 08:52:46 +00:00
d2babeba27 Fix configuration gathering and order of operations
- Add emergency fallback to ask about remote Transmission if config step was skipped
- Add CONFIG_GATHERED flag to track if config step was run
- Improve bootstrap installer with clear explanation of what to expect
- Explicitly check for gather_configuration success and output debug info
- Add additional variable initialization to ensure consistent behavior

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-05 08:46:06 +00:00
e05c8da811 Fix installation order and properly initialize TRANSMISSION_REMOTE
- Move SCRIPT_DIR initialization before its usage
- Ensure TRANSMISSION_REMOTE is initialized with default value
- Add logging to show whether remote or local Transmission is being used
- Initialize TRANSMISSION_REMOTE at the start of gather_configuration

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-05 08:42:55 +00:00
4c68a1ac07 Fix remote Transmission detection and config directory creation
- Fix bug where installer doesn't ask if Transmission is remote
- Fix missing configuration directory in /etc/transmission-rss-manager
- Create symlink between config locations to ensure app always finds config
- Ensure CONFIG_DIR is properly exported in the environment
- Update version to 2.0.4

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-05 08:39:21 +00:00
c495bce21f Move config file to /etc/transmission-rss-manager
- Changed config location to /etc/transmission-rss-manager/config.json
- Added fallback to maintain backward compatibility
- Updated installers to create and use the new location
- Added installPath property to configuration for updates
- Enhanced documentation with new config location
- Bumped version to 2.0.3

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-05 00:48:57 +00:00
3cfc5f3489 Update README.md with v2.0.2 changes and fix documentation 2025-03-05 00:36:29 +00:00
f5404c241d Fix remote transmission detection in installer
- Fix bug in dependencies-module.sh that would prompt to install transmission-daemon for remote installations
- Add checks for TRANSMISSION_REMOTE flag to correctly handle remote vs local installations
- Update version to 2.0.2

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-05 00:35:42 +00:00
39 changed files with 4518 additions and 304 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"

132
README.md Executable file → Normal file
View File

@@ -1,9 +1,88 @@
# Transmission RSS Manager v2.0.1 # 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! 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 ## 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
- **Improved**: Enhanced symlink handling between installation dir and config dir
- **Improved**: Better environment variable passing between install scripts
### v2.0.4 (2025-03-05)
- **Fixed**: Remote transmission detection in installer
- **Fixed**: Configuration directory creation and permissions
- **Fixed**: Config file symlink now ensures application finds correct config location
### v2.0.3 (2025-03-05)
- **New**: Configuration file now stored in `/etc/transmission-rss-manager/`
- **Improved**: Installer creates config directory with proper permissions
- **Improved**: Application now stores path to installation directory for easier updates
### v2.0.2 (2025-03-05)
- **Fixed**: Bug in installer that would prompt to install Transmission for remote configurations
- **Improved**: Better detection of remote vs local Transmission installations
- **Improved**: Installation process now checks the TRANSMISSION_REMOTE flag correctly
### v2.0.1 (2025-03-05) ### v2.0.1 (2025-03-05)
- **New**: Automatic detection of existing installations for seamless updates - **New**: Automatic detection of existing installations for seamless updates
- **Improved**: Enhanced update process that preserves existing configurations - **Improved**: Enhanced update process that preserves existing configurations
@@ -48,11 +127,21 @@ A comprehensive web-based tool to automate and manage your Transmission torrent
## Installation ## 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 ### Prerequisites
- Ubuntu/Debian-based system (may work on other Linux distributions) - 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) - 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 ### System Requirements
@@ -61,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 - Disk: At least 200MB for the application, plus storage space for your media
- Network: Internet connection for RSS feed fetching and torrent downloading - 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 ### Automatic Installation
The easiest way to install Transmission RSS Manager is with the bootstrap installer: The easiest way to install Transmission RSS Manager is with the bootstrap installer:
@@ -127,10 +220,11 @@ If you prefer to install manually:
### Main Configuration Options ### Main Configuration Options
The system can be configured through the web interface or by editing the `config.json` file: The system can be configured through the web interface or by editing the configuration file, which is located at `/etc/transmission-rss-manager/config.json` (or at the application's installation directory as a fallback):
```json ```json
{ {
"installPath": "/opt/transmission-rss-manager",
"transmissionConfig": { "transmissionConfig": {
"host": "localhost", "host": "localhost",
"port": 9091, "port": 9091,
@@ -280,6 +374,38 @@ The system will:
- Restore your configuration - Restore your configuration
- Restart the service automatically - 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 ### Using the Installer
You can also update by running the installer again: You can also update by running the installer again:

View File

@@ -11,9 +11,14 @@ NC='\033[0m' # No Color
BOLD='\033[1m' BOLD='\033[1m'
# Installation directory # Installation directory
INSTALL_DIR="/opt/trans-install" DEFAULT_INSTALL_DIR="/opt/trans-install"
REPO_URL="https://git.powerdata.dk/masterdraco/transmission-rss-manager.git" 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 # Check if running as root
if [ "$EUID" -ne 0 ]; then if [ "$EUID" -ne 0 ]; then
echo -e "${RED}This script must be run as root or with sudo privileges.${NC}" echo -e "${RED}This script must be run as root or with sudo privileges.${NC}"
@@ -23,6 +28,7 @@ fi
# Display welcome message # Display welcome message
echo -e "${GREEN}${BOLD}Transmission RSS Manager - Bootstrap Installer${NC}" 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 "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 echo
# Check for git installation # Check for git installation
@@ -62,7 +68,21 @@ fi
echo -e "${YELLOW}Running the main installer...${NC}" echo -e "${YELLOW}Running the main installer...${NC}"
cd "$INSTALL_DIR" cd "$INSTALL_DIR"
chmod +x main-installer.sh chmod +x main-installer.sh
./main-installer.sh
# Let the user know what's about to happen
echo -e "${YELLOW}You will now be asked for configuration options including:${NC}"
echo -e "- Installation directory"
echo -e "- User to run the service as"
echo -e "- Whether Transmission is running on a remote server"
echo -e "- Transmission connection details"
echo -e "- Media organization preferences"
echo
# Add small delay to ensure user sees this message
sleep 2
# Force the installer to run with the right environment
env -i PATH="$PATH" TERM="$TERM" USER="$USER" HOME="$HOME" SUDO_USER="$SUDO_USER" ./main-installer.sh
# Installation complete # Installation complete
echo -e "${GREEN}${BOLD}Bootstrap installation complete!${NC}" echo -e "${GREEN}${BOLD}Bootstrap installation complete!${NC}"

View File

@@ -28,14 +28,44 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# Create modules directory if it doesn't exist # Create modules directory if it doesn't exist
mkdir -p "${SCRIPT_DIR}/modules" mkdir -p "${SCRIPT_DIR}/modules"
# Check for installation type # Check for installation type in multiple locations
IS_UPDATE=false IS_UPDATE=false
if [ -f "${SCRIPT_DIR}/config.json" ]; then POSSIBLE_CONFIG_LOCATIONS=(
IS_UPDATE=true "${SCRIPT_DIR}/config.json"
echo -e "${YELLOW}Existing installation detected. Running in update mode.${NC}" "/opt/transmission-rss-manager/config.json"
echo -e "${GREEN}Your existing configuration will be preserved.${NC}" "/etc/transmission-rss-manager/config.json"
else )
echo -e "${GREEN}Fresh installation. Will create new configuration.${NC}"
# 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 at: $CONFIG_PATH${NC}"
echo -e "${YELLOW}Running in update mode.${NC}"
echo -e "${GREEN}Your existing configuration will be preserved.${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 fi
# Check if modules exist, if not, extract them # Check if modules exist, if not, extract them
@@ -1166,4 +1196,164 @@ fi
# Launch the main installer # Launch the main installer
echo -e "${GREEN}Launching main installer...${NC}" echo -e "${GREEN}Launching main installer...${NC}"
exec "${SCRIPT_DIR}/main-installer.sh"
# 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
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
# 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
cp "${SCRIPT_DIR}/main-installer.sh" "${SCRIPT_DIR}/main-installer.sh.bak"
# Insert the source command after the shebang line
awk 'NR==1{print; print "# Load installation environment variables"; print "if [ -f \"$(dirname \"$0\")/.env.install\" ]; then"; print " source \"$(dirname \"$0\")/.env.install\""; print " echo \"Loaded TRANSMISSION_REMOTE=$TRANSMISSION_REMOTE from environment file\""; print "fi"} NR!=1{print}' "${SCRIPT_DIR}/main-installer.sh.bak" > "${SCRIPT_DIR}/main-installer.sh"
chmod +x "${SCRIPT_DIR}/main-installer.sh"
fi
# Now execute the main installer with the environment variables set
echo "Running main installer with TRANSMISSION_REMOTE=$TRANSMISSION_REMOTE"
export TRANSMISSION_REMOTE
"${SCRIPT_DIR}/main-installer.sh"

View File

@@ -5,6 +5,12 @@
# Set script to exit on error # Set script to exit on error
set -e set -e
# Load installation environment variables if they exist
if [ -f "$(dirname "$0")/.env.install" ]; then
source "$(dirname "$0")/.env.install"
echo "Loaded TRANSMISSION_REMOTE=$TRANSMISSION_REMOTE from environment file"
fi
# Text formatting # Text formatting
BOLD='\033[1m' BOLD='\033[1m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
@@ -12,10 +18,20 @@ YELLOW='\033[0;33m'
RED='\033[0;31m' RED='\033[0;31m'
NC='\033[0m' # No Color 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 # Print header
echo -e "${BOLD}==================================================${NC}" echo -e "${BOLD}==================================================${NC}"
echo -e "${BOLD} Transmission RSS Manager Installer ${NC}" echo -e "${BOLD} Transmission RSS Manager Installer ${NC}"
VERSION=$(grep -oP '"version": "\K[^"]+' "${SCRIPT_DIR}/package.json" 2>/dev/null || echo "Unknown") 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} Version ${VERSION} - Git Edition ${NC}"
echo -e "${BOLD}==================================================${NC}" echo -e "${BOLD}==================================================${NC}"
echo echo
@@ -26,34 +42,78 @@ if [ "$EUID" -ne 0 ]; then
exit 1 exit 1
fi fi
# Get current directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# Check for installation type # Check for installation type
IS_UPDATE=false IS_UPDATE=false
INSTALLATION_DETECTED=false INSTALLATION_DETECTED=false
# Check for config.json file (primary indicator) # Check if we have existing config info from install-script.sh
if [ -f "${SCRIPT_DIR}/config.json" ]; then if [ -n "$EXISTING_INSTALL_DIR" ] && [ -n "$EXISTING_CONFIG_PATH" ]; then
INSTALLATION_DETECTED=true INSTALLATION_DETECTED=true
fi
# Check for service file (secondary indicator)
if [ -f "/etc/systemd/system/transmission-rss-manager.service" ]; then
INSTALLATION_DETECTED=true
fi
# Check for data directory (tertiary indicator)
if [ -d "${SCRIPT_DIR}/data" ] && [ "$(ls -A "${SCRIPT_DIR}/data" 2>/dev/null)" ]; then
INSTALLATION_DETECTED=true
fi
if [ "$INSTALLATION_DETECTED" = true ]; then
IS_UPDATE=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)
POSSIBLE_CONFIG_LOCATIONS=(
"${SCRIPT_DIR}/config.json"
"/opt/transmission-rss-manager/config.json"
"/etc/transmission-rss-manager/config.json"
)
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 [ "$INSTALLATION_DETECTED" = "false" ] && [ -d "${SCRIPT_DIR}/data" ] && [ "$(ls -A "${SCRIPT_DIR}/data" 2>/dev/null)" ]; then
INSTALLATION_DETECTED=true
fi
fi
# 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 "${YELLOW}Existing installation detected. Running in update mode.${NC}"
echo -e "${GREEN}Your existing configuration will be preserved.${NC}" echo -e "${GREEN}Your existing configuration will be preserved.${NC}"
echo -e "${GREEN}Only application files will be updated.${NC}" echo -e "${GREEN}Only application files will be updated.${NC}"
else else
log "INFO" "Running in FRESH INSTALL mode"
echo -e "${GREEN}Fresh installation. Will create new configuration.${NC}" echo -e "${GREEN}Fresh installation. Will create new configuration.${NC}"
fi fi
export IS_UPDATE export IS_UPDATE
@@ -75,11 +135,17 @@ for module in "${REQUIRED_MODULES[@]}"; do
fi fi
done done
# Source the module files # Source the remaining module files
source "${SCRIPT_DIR}/modules/utils-module.sh" # Load utilities first for logging
source "${SCRIPT_DIR}/modules/config-module.sh" source "${SCRIPT_DIR}/modules/config-module.sh"
source "${SCRIPT_DIR}/modules/dependencies-module.sh" source "${SCRIPT_DIR}/modules/dependencies-module.sh"
source "${SCRIPT_DIR}/modules/service-setup-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" source "${SCRIPT_DIR}/modules/file-creator-module.sh"
# Function to handle cleanup on error # Function to handle cleanup on error
@@ -99,11 +165,164 @@ trap 'cleanup_on_error "$BASH_COMMAND"' ERR
# Execute the installation steps in sequence # Execute the installation steps in sequence
log "INFO" "Starting installation process..." log "INFO" "Starting installation process..."
# 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 if [ "$IS_UPDATE" = true ]; then
log "INFO" "Running in update mode - preserving existing configuration..." log "INFO" "Running in update mode - preserving existing configuration..."
# When updating, we only need to update core files and dependencies # First, let's check if we already have this value from the environment
# Configuration should be preserved # 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) # Step 1: Check dependencies (but don't reconfigure)
log "INFO" "Checking dependencies..." log "INFO" "Checking dependencies..."
@@ -119,24 +338,71 @@ if [ "$IS_UPDATE" = true ]; then
exit 1 exit 1
} }
# Install npm dependencies # Install npm dependencies using our common function
log "INFO" "Updating npm dependencies..." ensure_npm_packages "$INSTALL_DIR" || {
cd "$SCRIPT_DIR"
npm install || {
log "ERROR" "NPM installation failed" log "ERROR" "NPM installation failed"
exit 1 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 else
# This is a fresh installation - run all steps # This is a fresh installation - run all steps
# Step 1: Gather configuration from user # Step 1: First, let's check if we already have this value from the environment
log "INFO" "Gathering configuration..." # 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."
else
export TRANSMISSION_REMOTE=false
log "INFO" "Local Transmission selected."
fi
# Now gather the rest of the configuration
log "INFO" "Gathering remaining configuration..."
gather_configuration || { gather_configuration || {
log "ERROR" "Configuration gathering failed" log "ERROR" "Configuration gathering failed"
exit 1 exit 1
} }
# Debug: Verify TRANSMISSION_REMOTE is set
log "INFO" "After configuration gathering, TRANSMISSION_REMOTE=$TRANSMISSION_REMOTE"
# Step 2: Install dependencies # Step 2: Install dependencies
log "INFO" "Installing dependencies..." log "INFO" "Installing dependencies..."
install_dependencies || { install_dependencies || {
@@ -146,6 +412,9 @@ else
# Step 3: Create installation directories # Step 3: Create installation directories
log "INFO" "Creating directories..." 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 || { create_directories || {
log "ERROR" "Directory creation failed" log "ERROR" "Directory creation failed"
exit 1 exit 1
@@ -165,10 +434,8 @@ else
exit 1 exit 1
} }
# Step 6: Install npm dependencies # Step 6: Install npm dependencies using our common function
log "INFO" "Installing npm dependencies..." ensure_npm_packages "$INSTALL_DIR" || {
cd "$SCRIPT_DIR"
npm install || {
log "ERROR" "NPM installation failed" log "ERROR" "NPM installation failed"
exit 1 exit 1
} }
@@ -177,9 +444,15 @@ fi
# Step 7: Set up update script # Step 7: Set up update script
log "INFO" "Setting up update script..." log "INFO" "Setting up update script..."
mkdir -p "${SCRIPT_DIR}/scripts" mkdir -p "${SCRIPT_DIR}/scripts"
cp "${SCRIPT_DIR}/scripts/update.sh" "${SCRIPT_DIR}/scripts/update.sh" 2>/dev/null || { # Check if update script exists - don't copy it to itself
# If copy fails, it probably doesn't exist, so we'll create it if [ ! -f "${SCRIPT_DIR}/scripts/update.sh" ]; then
cat > "${SCRIPT_DIR}/scripts/update.sh" << 'EOL' # 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 #!/bin/bash
# Transmission RSS Manager - Update Script # Transmission RSS Manager - Update Script
@@ -266,8 +539,10 @@ echo -e "Updated from version $CURRENT_VERSION to $NEW_VERSION"
echo -e "Changes will take effect immediately." echo -e "Changes will take effect immediately."
EOL EOL
chmod +x "${SCRIPT_DIR}/scripts/update.sh" 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 # Step 8: Final setup and permissions
log "INFO" "Finalizing setup..." log "INFO" "Finalizing setup..."

View File

@@ -2,7 +2,8 @@
# Configuration module for Transmission RSS Manager Installation # Configuration module for Transmission RSS Manager Installation
# Configuration variables with defaults # 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" SERVICE_NAME="transmission-rss-manager"
PORT=3000 PORT=3000
@@ -74,6 +75,12 @@ validate_hostname() {
function gather_configuration() { function gather_configuration() {
log "INFO" "Starting configuration gathering" log "INFO" "Starting configuration gathering"
# Initialize default values for Transmission mode
export TRANSMISSION_REMOTE=false
# Set flag to indicate that configuration was gathered
export CONFIG_GATHERED=true
echo -e "${BOLD}Installation Configuration:${NC}" echo -e "${BOLD}Installation Configuration:${NC}"
echo -e "Please provide the following configuration parameters:" echo -e "Please provide the following configuration parameters:"
echo echo
@@ -109,11 +116,11 @@ function gather_configuration() {
echo -e "Configure connection to your Transmission client:" echo -e "Configure connection to your Transmission client:"
echo echo
# Ask if Transmission is remote # Don't ask about remote again - this is now handled in the main installer
read -p "Is Transmission running on a remote server? (y/n) [n]: " input_remote # Just log the current setting for clarity
if [[ $input_remote =~ ^[Yy]$ ]]; then log "INFO" "Using previously selected Transmission mode: $([ "$TRANSMISSION_REMOTE" = true ] && echo "Remote" || echo "Local")"
TRANSMISSION_REMOTE=true
if [ "$TRANSMISSION_REMOTE" = true ]; then
# Get and validate hostname # Get and validate hostname
while true; do while true; do
read -p "Remote Transmission host [localhost]: " input_trans_host read -p "Remote Transmission host [localhost]: " input_trans_host
@@ -225,7 +232,8 @@ function gather_configuration() {
# Set Transmission download dir for configuration # Set Transmission download dir for configuration
TRANSMISSION_DOWNLOAD_DIR=$REMOTE_DOWNLOAD_DIR TRANSMISSION_DOWNLOAD_DIR=$REMOTE_DOWNLOAD_DIR
else else
# Local Transmission selected # Local Transmission selected - this part was already set in main-installer.sh
echo -e "${YELLOW}You've selected to use a local Transmission installation.${NC}" echo -e "${YELLOW}You've selected to use a local Transmission installation.${NC}"
# Check if Transmission is already installed # Check if Transmission is already installed

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

@@ -4,6 +4,66 @@
function install_dependencies() { function install_dependencies() {
log "INFO" "Installing dependencies..." log "INFO" "Installing dependencies..."
# Make sure TRANSMISSION_REMOTE variable is set and honored
# First check for environment variable that might have been directly set
# Then check the .env.install file in various locations
# Try relative path first
ENV_FILE="$(dirname "$(dirname "$0")")/.env.install"
if [ -f "$ENV_FILE" ]; then
source "$ENV_FILE"
log "INFO" "Loaded transmission settings from environment file: TRANSMISSION_REMOTE=$TRANSMISSION_REMOTE"
# Try absolute path as fallback
elif [ -f "/opt/develop/transmission-rss-manager/.env.install" ]; then
source "/opt/develop/transmission-rss-manager/.env.install"
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..."
# Directly ask about remote Transmission
echo -e "${BOLD}Transmission Configuration:${NC}"
echo -e "Configure connection to your Transmission client:"
echo
read -p "Is Transmission running on a remote server? (y/n) [n]: " input_remote
if [[ $input_remote =~ ^[Yy]$ ]]; then
export TRANSMISSION_REMOTE=true
log "INFO" "Remote Transmission selected."
else
export TRANSMISSION_REMOTE=false
log "INFO" "Local Transmission selected."
fi
# Save this choice to environment file for other scripts
echo "export TRANSMISSION_REMOTE=$TRANSMISSION_REMOTE" > "$(dirname "$(dirname "$0")")/.env.install"
chmod +x "$(dirname "$(dirname "$0")")/.env.install"
else
log "INFO" "Using previously set TRANSMISSION_REMOTE=$TRANSMISSION_REMOTE"
fi
log "INFO" "Proceeding with Transmission mode: $([ "$TRANSMISSION_REMOTE" = true ] && echo "Remote" || echo "Local")"
# Check for package manager # Check for package manager
if command -v apt-get &> /dev/null; then if command -v apt-get &> /dev/null; then
# Update package index # Update package index
@@ -35,10 +95,11 @@ function install_dependencies() {
log "INFO" "Node.js is already installed." log "INFO" "Node.js is already installed."
fi fi
# Check if we need to install Transmission (only if local transmission was selected) # Check if we need to install Transmission (only if local transmission was selected and not in update mode)
if [ "$TRANSMISSION_HOST" = "localhost" ] || [ "$TRANSMISSION_HOST" = "127.0.0.1" ]; then if [ "$TRANSMISSION_REMOTE" = false ] && [ "$IS_UPDATE" != "true" ]; then
if ! command_exists transmission-daemon; then if ! command_exists transmission-daemon; then
log "INFO" "Local Transmission installation selected, but transmission-daemon is not installed." log "INFO" "Local Transmission installation selected, but transmission-daemon is not installed."
log "INFO" "You selected to use a local Transmission installation during configuration."
read -p "Would you like to install Transmission now? (y/n): " install_transmission read -p "Would you like to install Transmission now? (y/n): " install_transmission
if [[ "$install_transmission" =~ ^[Yy]$ ]]; then if [[ "$install_transmission" =~ ^[Yy]$ ]]; then
@@ -110,7 +171,7 @@ function install_dependencies() {
local missing_deps=() local missing_deps=()
# Add transmission to dependencies check if local installation was selected # Add transmission to dependencies check if local installation was selected
if [ "$TRANSMISSION_HOST" = "localhost" ] || [ "$TRANSMISSION_HOST" = "127.0.0.1" ]; then if [ "$TRANSMISSION_REMOTE" = false ]; then
dependencies+=("transmission-daemon") dependencies+=("transmission-daemon")
fi fi
@@ -163,6 +224,12 @@ function create_directories() {
exit 1 exit 1
fi fi
# Check if CONFIG_DIR is defined
if [ -z "$CONFIG_DIR" ]; then
log "ERROR" "CONFIG_DIR is not defined"
exit 1
fi
# Create directories and check for errors # Create directories and check for errors
DIRECTORIES=( DIRECTORIES=(
"$INSTALL_DIR" "$INSTALL_DIR"
@@ -171,6 +238,7 @@ function create_directories() {
"$INSTALL_DIR/public/css" "$INSTALL_DIR/public/css"
"$INSTALL_DIR/modules" "$INSTALL_DIR/modules"
"$INSTALL_DIR/data" "$INSTALL_DIR/data"
"$CONFIG_DIR"
) )
for dir in "${DIRECTORIES[@]}"; do for dir in "${DIRECTORIES[@]}"; do
@@ -180,5 +248,13 @@ function create_directories() {
fi fi
done done
# Set permissions for configuration directory
chown -R "$USER:$USER" "$CONFIG_DIR"
chmod 755 "$CONFIG_DIR"
# Create a symlink from the installation directory to the config directory
# This ensures the application can find the config regardless of where it looks
ln -sf "$CONFIG_DIR/config.json" "$INSTALL_DIR/config.json"
log "INFO" "Directories created successfully." log "INFO" "Directories created successfully."
} }

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

@@ -1,9 +1,139 @@
#!/bin/bash #!/bin/bash
# File creator module for Transmission RSS Manager Installation # 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() { function create_config_files() {
echo -e "${YELLOW}Creating configuration files...${NC}" echo -e "${YELLOW}Creating configuration files...${NC}"
# 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": "$VERSION",
"installPath": "$INSTALL_DIR",
"transmissionConfig": {
"host": "$TRANSMISSION_HOST",
"port": $TRANSMISSION_PORT,
"username": "$TRANSMISSION_USER",
"password": "$TRANSMISSION_PASS",
"path": "$TRANSMISSION_RPC_PATH"
},
"remoteConfig": {
"isRemote": $TRANSMISSION_REMOTE,
"directoryMapping": $TRANSMISSION_DIR_MAPPING
},
"destinationPaths": {
"movies": "$MEDIA_DIR/movies",
"tvShows": "$MEDIA_DIR/tvshows",
"music": "$MEDIA_DIR/music",
"books": "$MEDIA_DIR/books",
"magazines": "$MEDIA_DIR/magazines",
"software": "$MEDIA_DIR/software"
},
"seedingRequirements": {
"minRatio": 1.0,
"minTimeMinutes": 60,
"checkIntervalSeconds": 300
},
"processingOptions": {
"enableBookSorting": $ENABLE_BOOK_SORTING,
"extractArchives": true,
"deleteArchives": true,
"createCategoryFolders": true,
"ignoreSample": true,
"ignoreExtras": true,
"renameFiles": true,
"autoReplaceUpgrades": true,
"removeDuplicates": true,
"keepOnlyBestVersion": true
},
"rssFeeds": [],
"rssUpdateIntervalMinutes": 60,
"autoProcessing": false,
"securitySettings": {
"authEnabled": false,
"httpsEnabled": false,
"sslCertPath": "",
"sslKeyPath": "",
"users": []
},
"port": $PORT,
"logLevel": "info"
}
EOF
# Create package.json # Create package.json
echo "Creating package.json..." echo "Creating package.json..."
cat > $INSTALL_DIR/package.json << EOF cat > $INSTALL_DIR/package.json << EOF
@@ -44,8 +174,9 @@ const cors = require('cors');
const Transmission = require('transmission'); const Transmission = require('transmission');
// Import custom modules // Import custom modules
const PostProcessor = require('./modules/postProcessor'); const PostProcessor = require('./modules/post-processor.js');
const RssFeedManager = require('./modules/rssFeedManager'); const RssFeedManager = require('./modules/rss-feed-manager.js');
const TransmissionClient = require('./modules/transmission-client.js');
// Initialize Express app // Initialize Express app
const app = express(); const app = express();
@@ -207,11 +338,21 @@ app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' }));
// API routes // 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 // Server status API
app.get('/api/status', (req, res) => { app.get('/api/status', (req, res) => {
res.json({ res.json({
status: 'running', status: 'running',
version: '1.2.0', version: appVersion,
transmissionConnected: !!transmissionClient, transmissionConnected: !!transmissionClient,
postProcessorActive: postProcessor && postProcessor.processingIntervalId !== null, postProcessorActive: postProcessor && postProcessor.processingIntervalId !== null,
rssFeedManagerActive: rssFeedManager && rssFeedManager.updateIntervalId !== null, rssFeedManagerActive: rssFeedManager && rssFeedManager.updateIntervalId !== null,
@@ -1704,4 +1845,98 @@ EOF
} }
echo "Configuration files created." 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,8 +18,29 @@ class RssFeedManager {
this.updateIntervalMinutes = config.updateIntervalMinutes || 60; this.updateIntervalMinutes = config.updateIntervalMinutes || 60;
this.parser = new xml2js.Parser({ explicitArray: false }); 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
this.dataPath = path.join(__dirname, '..', 'data'); 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 // Maximum items to keep in memory to prevent memory leaks
this.maxItemsInMemory = config.maxItemsInMemory || 5000; this.maxItemsInMemory = config.maxItemsInMemory || 5000;
@@ -31,6 +52,10 @@ class RssFeedManager {
} }
try { try {
// Make sure the data directory exists first
await this.ensureDataDirectory();
console.log(`Using data directory: ${this.dataPath}`);
// Load existing feeds and items // Load existing feeds and items
await this.loadFeeds(); await this.loadFeeds();
await this.loadItems(); await this.loadItems();
@@ -131,10 +156,13 @@ class RssFeedManager {
console.log(`Updating feed: ${feed.name || 'Unnamed'} (${feed.url})`); console.log(`Updating feed: ${feed.name || 'Unnamed'} (${feed.url})`);
try { 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, { const response = await fetch(feed.url, {
timeout: 30000, // 30 second timeout timeout: 30000, // 30 second timeout
headers: { 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() { async ensureDataDirectory() {
try { try {
// Create data directory with recursive option (creates all parent directories if they don't exist)
await fs.mkdir(this.dataPath, { recursive: true }); await fs.mkdir(this.dataPath, { recursive: true });
console.log(`Ensured data directory exists at: ${this.dataPath}`);
return true;
} catch (error) { } catch (error) {
console.error('Error creating data directory:', error); // Log the error details for debugging
throw error; 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."
}

View File

@@ -21,6 +21,11 @@ function setup_service() {
exit 1 exit 1
fi fi
if [ -z "$CONFIG_DIR" ]; then
log "ERROR" "CONFIG_DIR variable is not set"
exit 1
fi
if [ -z "$PORT" ]; then if [ -z "$PORT" ]; then
log "ERROR" "PORT variable is not set" log "ERROR" "PORT variable is not set"
exit 1 exit 1
@@ -71,6 +76,7 @@ Environment=PORT=$PORT
Environment=NODE_ENV=production Environment=NODE_ENV=production
Environment=DEBUG_ENABLED=false Environment=DEBUG_ENABLED=false
Environment=LOG_FILE=$INSTALL_DIR/logs/transmission-rss-manager.log Environment=LOG_FILE=$INSTALL_DIR/logs/transmission-rss-manager.log
Environment=CONFIG_DIR=$CONFIG_DIR
EOF EOF
# Preserve the existing JWT_SECRET if available # Preserve the existing JWT_SECRET if available
@@ -117,6 +123,7 @@ Environment=PORT=$PORT
Environment=NODE_ENV=production Environment=NODE_ENV=production
Environment=DEBUG_ENABLED=false Environment=DEBUG_ENABLED=false
Environment=LOG_FILE=$INSTALL_DIR/logs/transmission-rss-manager.log Environment=LOG_FILE=$INSTALL_DIR/logs/transmission-rss-manager.log
Environment=CONFIG_DIR=$CONFIG_DIR
# Generate a random JWT secret for security # Generate a random JWT secret for security
Environment=JWT_SECRET=$(openssl rand -hex 32) Environment=JWT_SECRET=$(openssl rand -hex 32)

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; this.dirMappings = config.remoteConfig.directoryMapping;
} }
// Initialize the connection // Initialize the connection - but don't throw if it fails initially
this.initializeConnection(); // 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; const { host, port, username, password, path: rpcPath } = this.config.transmissionConfig;
try { try {
// Only default to localhost if host is empty/null/undefined
const connectionHost = (host === undefined || host === null || host === '') ? 'localhost' : host;
this.client = new Transmission({ this.client = new Transmission({
host: host || 'localhost', host: connectionHost,
port: port || 9091, port: port || 9091,
username: username || '', username: username || '',
password: password || '', password: password || '',
@@ -48,7 +57,7 @@ class TransmissionClient {
timeout: 30000 // 30 seconds 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) { } catch (error) {
console.error('Failed to initialize Transmission client:', error); console.error('Failed to initialize Transmission client:', error);
throw error; throw error;
@@ -61,13 +70,17 @@ class TransmissionClient {
*/ */
async getStatus() { async getStatus() {
try { try {
// Use the session-stats method for basic connectivity check
const sessionInfo = await this.client.sessionStats(); 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 { return {
connected: true, connected: true,
version: version.version, version: session.version || "Unknown",
rpcVersion: version['rpc-version'], rpcVersion: session['rpc-version'] || "Unknown",
downloadSpeed: sessionInfo.downloadSpeed, downloadSpeed: sessionInfo.downloadSpeed,
uploadSpeed: sessionInfo.uploadSpeed, uploadSpeed: sessionInfo.uploadSpeed,
torrentCount: sessionInfo.torrentCount, torrentCount: sessionInfo.torrentCount,
@@ -112,6 +125,15 @@ class TransmissionClient {
*/ */
async addTorrent(url, options = {}) { async addTorrent(url, options = {}) {
try { 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 downloadDir = options.downloadDir || null;
const result = await this.client.addUrl(url, { const result = await this.client.addUrl(url, {
"download-dir": downloadDir, "download-dir": downloadDir,
@@ -443,7 +465,8 @@ class TransmissionClient {
*/ */
async setSessionParams(params) { async setSessionParams(params) {
try { try {
await this.client.sessionSet(params); // In transmission-promise, the method is sessionUpdate not sessionSet
await this.client.sessionUpdate(params);
return { return {
success: true, success: true,
message: 'Session parameters updated successfully' 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 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() { function finalize_setup() {
log "INFO" "Setting up final permissions and configurations..." log "INFO" "Setting up final permissions and configurations..."
@@ -104,6 +174,26 @@ function finalize_setup() {
mkdir -p "$INSTALL_DIR/logs" mkdir -p "$INSTALL_DIR/logs"
log "INFO" "Created logs directory: $INSTALL_DIR/logs" log "INFO" "Created logs directory: $INSTALL_DIR/logs"
# Ensure CONFIG_DIR exists
if [ ! -d "$CONFIG_DIR" ]; then
mkdir -p "$CONFIG_DIR"
log "INFO" "Created configuration directory: $CONFIG_DIR"
chown -R "$USER:$USER" "$CONFIG_DIR"
fi
# Check if the config symlink exists, create it if not
if [ ! -L "$INSTALL_DIR/config.json" ] || [ ! -e "$INSTALL_DIR/config.json" ]; then
# If there's a real file at INSTALL_DIR/config.json (not a symlink), move it to CONFIG_DIR
if [ -f "$INSTALL_DIR/config.json" ] && [ ! -L "$INSTALL_DIR/config.json" ]; then
log "INFO" "Moving existing config.json to $CONFIG_DIR"
mv "$INSTALL_DIR/config.json" "$CONFIG_DIR/config.json"
fi
# Create the symlink
ln -sf "$CONFIG_DIR/config.json" "$INSTALL_DIR/config.json"
log "INFO" "Created symlink from $INSTALL_DIR/config.json to $CONFIG_DIR/config.json"
fi
# Set proper ownership for the installation directory # Set proper ownership for the installation directory
chown -R $USER:$USER $INSTALL_DIR chown -R $USER:$USER $INSTALL_DIR
@@ -119,12 +209,13 @@ function finalize_setup() {
create_dir_if_not_exists "$MEDIA_DIR/magazines" "$USER:$USER" create_dir_if_not_exists "$MEDIA_DIR/magazines" "$USER:$USER"
fi fi
# Install NPM packages # Install npm packages
log "INFO" "Installing NPM packages..." ensure_npm_packages "$INSTALL_DIR" || {
cd $INSTALL_DIR && npm install log "ERROR" "Failed to install NPM packages"
}
# Handle configuration file # Handle configuration file
if ! update_config_file "$INSTALL_DIR/config.json" "$IS_UPDATE"; then if ! update_config_file "$CONFIG_DIR/config.json" "$IS_UPDATE"; then
log "INFO" "Creating default configuration file..." log "INFO" "Creating default configuration file..."
# Create the users array content for JSON # Create the users array content for JSON
@@ -133,9 +224,15 @@ function finalize_setup() {
USER_JSON="{ \"username\": \"${ADMIN_USERNAME}\", \"password\": \"${ADMIN_PASSWORD}\", \"role\": \"admin\" }" USER_JSON="{ \"username\": \"${ADMIN_USERNAME}\", \"password\": \"${ADMIN_PASSWORD}\", \"role\": \"admin\" }"
fi fi
cat > $INSTALL_DIR/config.json << EOF # 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": { "transmissionConfig": {
"host": "${TRANSMISSION_HOST}", "host": "${TRANSMISSION_HOST}",
"port": ${TRANSMISSION_PORT}, "port": ${TRANSMISSION_PORT},
@@ -188,7 +285,12 @@ function finalize_setup() {
"logLevel": "info" "logLevel": "info"
} }
EOF EOF
chown $USER:$USER $INSTALL_DIR/config.json # Set ownership for the config file
chown $USER:$USER $CONFIG_DIR/config.json
# Ensure symlink exists from INSTALL_DIR to CONFIG_DIR
ln -sf "$CONFIG_DIR/config.json" "$INSTALL_DIR/config.json"
log "INFO" "Created symlink from $INSTALL_DIR/config.json to $CONFIG_DIR/config.json"
log "INFO" "Default configuration created successfully" log "INFO" "Default configuration created successfully"
fi fi

View File

@@ -1,6 +1,6 @@
{ {
"name": "transmission-rss-manager", "name": "transmission-rss-manager",
"version": "2.0.1", "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", "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", "main": "server.js",
"scripts": { "scripts": {

View File

@@ -137,8 +137,8 @@
<div class="mt-2 testing-controls"> <div class="mt-2 testing-controls">
<small><a href="#" id="toggle-test-update-button">Toggle Test Update</a></small> <small><a href="#" id="toggle-test-update-button">Toggle Test Update</a></small>
</div> </div>
<div id="update-available" class="mt-3 d-none"> <div id="update-available" class="mt-3">
<div class="alert alert-info"> <div class="alert alert-info update-alert" style="display: none;">
<i class="fas fa-arrow-circle-up"></i> <i class="fas fa-arrow-circle-up"></i>
<span>A new version is available!</span> <span>A new version is available!</span>
<button id="btn-update-now" class="btn btn-sm btn-primary ml-2"> <button id="btn-update-now" class="btn btn-sm btn-primary ml-2">
@@ -545,7 +545,7 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-6"> <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>
<div class="col-md-6 text-right"> <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> <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> <h4>Version History</h4>
<div class="version-history"> <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"> <div class="version">
<h5>v2.0.0 - March 2025</h5> <h5>v2.0.0 - March 2025</h5>
<ul> <ul>
@@ -628,7 +671,7 @@
</div> </div>
<div class="text-center mt-4"> <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>© 2025 PowerData.dk - All Rights Reserved</p>
<p><a href="https://powerdata.dk" target="_blank">Visit PowerData.dk</a></p> <p><a href="https://powerdata.dk" target="_blank">Visit PowerData.dk</a></p>
</div> </div>
@@ -639,6 +682,60 @@
</div> </div>
</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 --> <!-- JavaScript Files -->
<script src="/js/system-status.js"></script> <script src="/js/system-status.js"></script>
<script src="/js/app.js"></script> <script src="/js/app.js"></script>

View File

@@ -16,15 +16,30 @@ function initSystemStatus() {
// Load system status // Load system status
function loadSystemStatus() { function loadSystemStatus() {
fetch('/api/system/status', { // Add cache-busting parameter
const cacheBuster = `?_=${new Date().getTime()}`;
fetch('/api/system/status' + cacheBuster, {
headers: authHeaders() headers: authHeaders()
}) })
.then(handleResponse) .then(handleResponse)
.then(data => { .then(data => {
if (data.status === 'success') { if (data.status === 'success') {
// Update version display
versionElement.textContent = data.data.version; versionElement.textContent = data.data.version;
uptimeElement.textContent = data.data.uptime; 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 // Update transmission status with icon
if (data.data.transmissionStatus === 'Connected') { if (data.data.transmissionStatus === 'Connected') {
transmissionStatusElement.innerHTML = '<i class="fas fa-check-circle text-success"></i> 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 // Check for updates
function checkForUpdates() { function checkForUpdates() {
updateStatusElement.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Checking...'; 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 // Add test=true parameter to force update availability for testing
const testMode = localStorage.getItem('showUpdateButton') === 'true'; 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, { fetch(url, {
headers: authHeaders() headers: authHeaders(),
// Add a fetch timeout using abort controller
signal: controller.signal // 15 second timeout
}) })
.then(handleResponse) .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(data => { .then(data => {
updateCheckInProgress = false;
if (data.status === 'success') { if (data.status === 'success') {
if (data.data.updateAvailable) { if (data.data && data.data.updateAvailable) {
updateStatusElement.innerHTML = '<i class="fas fa-exclamation-circle text-warning"></i> Update available'; // Show update alert with version info
updateAvailableDiv.classList.remove('d-none'); showUpdateAlert(data.data.currentVersion, data.data.remoteVersion);
updateAvailableDiv.querySelector('span').textContent =
`A new version is available: ${data.data.currentVersion}${data.data.remoteVersion}`; // Log to console for debugging
console.log('Update available detected:', data.data.currentVersion, '->', data.data.remoteVersion);
} else { } else {
// No update available
updateStatusElement.innerHTML = '<i class="fas fa-check-circle text-success"></i> Up to date'; 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 { } else {
// Error status but with a response
updateStatusElement.innerHTML = '<i class="fas fa-times-circle text-danger"></i> Check failed'; updateStatusElement.innerHTML = '<i class="fas fa-times-circle text-danger"></i> Check failed';
showNotification(data.message || 'Failed to check for updates', 'danger'); showNotification(data.message || 'Failed to check for updates', 'danger');
// Don't clear update status on error - keep any previous update notification
} }
}) })
.catch(error => { .catch(error => {
updateCheckInProgress = false;
clearTimeout(timeoutId);
console.error('Error checking for updates:', error); console.error('Error checking for updates:', error);
updateStatusElement.innerHTML = '<i class="fas fa-times-circle text-danger"></i> Check failed'; 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; return;
} }
// Show loading state // Disable test mode whenever we try to apply an update
updateButton.disabled = true; localStorage.setItem('showUpdateButton', 'false');
updateButton.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Updating...';
// 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'); 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', { fetch('/api/system/update', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...authHeaders() ...authHeaders()
} },
signal: updateController.signal // 45 second timeout
}) })
.then(handleResponse) .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(data => { .then(data => {
clearTimeout(updateTimeoutId);
if (data.status === 'success') { 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'); 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 // Set a timer to reload the page after the service has time to restart
setTimeout(() => { 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); }, 30000);
} else { } else {
updateButton.disabled = false; // Enable both buttons on failure
updateButton.innerHTML = '<i class="fas fa-download"></i> Update Now'; 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'); showNotification(data.message || 'Failed to apply update', 'danger');
} }
}) })
.catch(error => { .catch(error => {
clearTimeout(updateTimeoutId);
console.error('Error applying update:', error); console.error('Error applying update:', error);
updateButton.disabled = false;
updateButton.innerHTML = '<i class="fas fa-download"></i> Update Now'; // Re-enable both buttons on error
showNotification('Failed to connect to server', 'danger'); 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';
}
// 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); 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) // Test mode toggle (for developers)
const testToggle = document.getElementById('toggle-test-update-button'); const testToggle = document.getElementById('toggle-test-update-button');
if (testToggle) { if (testToggle) {
// Initialize based on current localStorage setting // Initialize based on current localStorage setting
const isTestMode = localStorage.getItem('showUpdateButton') === 'true'; 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 // Update toggle text
testToggle.innerText = isTestMode ? 'Disable Test Update' : 'Enable Test Update'; testToggle.innerText = isTestMode ? 'Disable Test Update' : 'Enable Test Update';
@@ -147,16 +578,206 @@ function initSystemStatus() {
localStorage.setItem('showUpdateButton', newSetting); localStorage.setItem('showUpdateButton', newSetting);
testToggle.innerText = newSetting ? 'Disable Test Update' : 'Enable Test Update'; testToggle.innerText = newSetting ? 'Disable Test Update' : 'Enable Test Update';
// Re-check for updates with new setting if (newSetting) {
checkForUpdates(); // 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 // Initialize
loadSystemStatus(); 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 // Set interval to refresh uptime every minute
setInterval(loadSystemStatus, 60000); 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 #!/bin/bash
# Test and start script for Transmission RSS Manager # Script to ensure data directory exists and start the application
# This script checks the installation, dependencies, and starts the application
# Text formatting # Define paths
BOLD='\033[1m' APP_DIR="$(dirname "$(dirname "$(readlink -f "$0")")")"
GREEN='\033[0;32m' DATA_DIR="$APP_DIR/data"
YELLOW='\033[0;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Get directory of this script echo "Starting Transmission RSS Manager..."
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" echo "Application directory: $APP_DIR"
APP_DIR="$(dirname "$SCRIPT_DIR")" echo "Data directory: $DATA_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
# 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 if [ $? -ne 0 ]; then
echo -e "${RED}Failed to install dependencies.${NC}" echo "ERROR: Both methods to create data directory failed. Please check permissions."
exit 1 exit 1
else
echo -e "${GREEN}Dependencies installed successfully${NC}"
fi fi
else
echo -e "${GREEN}Dependencies are already installed${NC}"
fi fi
fi
# Set permissions
chmod -R 755 "$DATA_DIR" || {
echo "Warning: Failed to set permissions on data directory"
} }
# Check if config.json exists # Check for RSS files
check_config() { if [ ! -f "$DATA_DIR/rss-feeds.json" ]; then
echo -e "${BOLD}Checking configuration...${NC}" echo "Creating initial empty rss-feeds.json file"
echo "[]" > "$DATA_DIR/rss-feeds.json" || {
if [ ! -f "$APP_DIR/config.json" ]; then echo "ERROR: Failed to create rss-feeds.json file"
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}"
exit 1 exit 1
else }
echo -e "${GREEN}Configuration file found${NC}" fi
fi
}
# Start the application if [ ! -f "$DATA_DIR/rss-items.json" ]; then
start_app() { echo "Creating initial empty rss-items.json file"
echo -e "${BOLD}Starting Transmission RSS Manager...${NC}" echo "[]" > "$DATA_DIR/rss-items.json" || {
echo "ERROR: Failed to create rss-items.json file"
exit 1
}
fi
# Check if running as a service # Find the node executable path
if command_exists systemctl; then NODE_PATH=$(which node 2>/dev/null)
if systemctl is-active --quiet transmission-rss-manager; then if [ -z "$NODE_PATH" ]; then
echo -e "${YELLOW}Transmission RSS Manager is already running as a service${NC}" # If node is not in PATH, try common locations
echo -e "${YELLOW}To restart it, use: sudo systemctl restart transmission-rss-manager${NC}" for path in /usr/bin/node /usr/local/bin/node /opt/node/bin/node /usr/lib/node; do
exit 0 if [ -x "$path" ]; then
NODE_PATH="$path"
break
fi fi
fi
# Start the application
cd "$APP_DIR"
# 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
done done
if [ "$FOREGROUND" = true ]; then # If we still can't find node, use the default path
echo -e "${GREEN}Starting in foreground mode...${NC}" 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
if [ "$DEBUG" = true ]; then # Create module symlinks to ensure compatibility
echo -e "${YELLOW}Debug mode enabled${NC}" echo "Creating module symlinks for compatibility..."
DEBUG_ENABLED=true node server.js MODULE_DIR="$APP_DIR/modules"
else
node server.js # Create a function to make bidirectional symlinks
fi 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 else
echo -e "${GREEN}Starting in background mode...${NC}" echo "Warning: Module directory not found at $MODULE_DIR"
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}"
fi fi
} }
# Main script # Execute the symlink creation function
echo -e "${BOLD}==================================================${NC}" create_module_symlinks
echo -e "${BOLD} Transmission RSS Manager - Test & Start ${NC}"
echo -e "${BOLD}==================================================${NC}"
echo
# Run checks
check_node
check_transmission
check_dependencies
check_config
# Start the application # 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 # Install any new npm dependencies
echo -e "${YELLOW}Installing dependencies...${NC}" echo -e "${YELLOW}Installing dependencies...${NC}"
npm install 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 # Apply any local configuration changes
if git stash list | grep -q "stash@{0}"; then if git stash list | grep -q "stash@{0}"; then

536
server.js
View File

@@ -5,7 +5,8 @@
const express = require('express'); const express = require('express');
const path = require('path'); 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 bodyParser = require('body-parser');
const cors = require('cors'); const cors = require('cors');
const morgan = require('morgan'); const morgan = require('morgan');
@@ -13,21 +14,96 @@ const http = require('http');
const https = require('https'); const https = require('https');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt'); 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 // Import custom modules
const RssFeedManager = require('./modules/rss-feed-manager.js'); let RssFeedManager, TransmissionClient, PostProcessor;
const TransmissionClient = require('./modules/transmission-client.js');
const PostProcessor = require('./modules/post-processor.js'); /**
* 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 // Constants and configuration
const DEFAULT_CONFIG_PATH = path.join(__dirname, 'config.json'); const DEFAULT_CONFIG_PATH = '/etc/transmission-rss-manager/config.json';
const FALLBACK_CONFIG_PATH = path.join(__dirname, 'config.json');
const DEFAULT_PORT = 3000; const DEFAULT_PORT = 3000;
const JWT_SECRET = process.env.JWT_SECRET || 'transmission-rss-manager-secret'; const JWT_SECRET = process.env.JWT_SECRET || 'transmission-rss-manager-secret';
const JWT_EXPIRY = '24h'; const JWT_EXPIRY = '24h';
// Get the version from package.json (single source of truth) // Get the version from package.json (single source of truth)
const PACKAGE_JSON = require('./package.json'); // Re-read the package.json file each time to ensure we get the latest version
const APP_VERSION = PACKAGE_JSON.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');
return PACKAGE_JSON.version;
}
})();
// Create Express app // Create Express app
const app = express(); const app = express();
@@ -97,6 +173,7 @@ async function loadConfig() {
// Define default configuration // Define default configuration
const defaultConfig = { const defaultConfig = {
version: APP_VERSION, // Use the version from package.json version: APP_VERSION, // Use the version from package.json
installPath: __dirname, // Store the path to the installation directory
transmissionConfig: { transmissionConfig: {
host: 'localhost', host: 'localhost',
port: 9091, port: 9091,
@@ -147,9 +224,11 @@ async function loadConfig() {
logLevel: "info" logLevel: "info"
}; };
// Try primary config location first
try { try {
// Try to read existing config // Try to read existing config from primary location
const configData = await fs.readFile(DEFAULT_CONFIG_PATH, 'utf8'); console.log(`Trying to load config from: ${DEFAULT_CONFIG_PATH}`);
const configData = await fsPromises.readFile(DEFAULT_CONFIG_PATH, 'utf8');
const loadedConfig = JSON.parse(configData); const loadedConfig = JSON.parse(configData);
// Use recursive merge function to merge configs // Use recursive merge function to merge configs
@@ -162,17 +241,68 @@ async function loadConfig() {
await saveConfig(mergedConfig); await saveConfig(mergedConfig);
} }
console.log(`Config loaded from ${DEFAULT_CONFIG_PATH}`);
return mergedConfig; return mergedConfig;
} catch (readError) { } catch (primaryError) {
// If file not found, create default config // If file not found in primary location, try fallback location
if (readError.code === 'ENOENT') { if (primaryError.code === 'ENOENT') {
console.log('Config file not found, creating default configuration'); console.log(`Config not found at ${DEFAULT_CONFIG_PATH}, trying fallback location...`);
await saveConfig(defaultConfig);
return defaultConfig; try {
const fallbackData = await fsPromises.readFile(FALLBACK_CONFIG_PATH, 'utf8');
const fallbackConfig = JSON.parse(fallbackData);
// Merge configs
const mergedConfig = mergeConfigs(defaultConfig, fallbackConfig);
// If version is different, save updated config
if (fallbackConfig.version !== APP_VERSION) {
console.log(`Updating config from version ${fallbackConfig.version || 'unknown'} to ${APP_VERSION}`);
mergedConfig.version = APP_VERSION;
}
// Try to save to primary location, but don't fail if we can't
try {
// Create directory if it doesn't exist
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}`);
console.warn('Continuing with fallback config location');
}
console.log(`Config loaded from fallback location: ${FALLBACK_CONFIG_PATH}`);
return mergedConfig;
} catch (fallbackError) {
// If file not found in fallback location, create default config
if (fallbackError.code === 'ENOENT') {
console.log('Config file not found in either location, creating default configuration');
// Try to save to primary location first
try {
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 fsPromises.writeFile(FALLBACK_CONFIG_PATH, JSON.stringify(defaultConfig, null, 2), 'utf8');
console.log(`Created default config at ${FALLBACK_CONFIG_PATH}`);
}
return defaultConfig;
}
// For other errors, rethrow
throw fallbackError;
}
} }
// For other errors, rethrow // For other errors, rethrow
throw readError; throw primaryError;
} }
} catch (error) { } catch (error) {
console.error('Error loading configuration:', error); console.error('Error loading configuration:', error);
@@ -231,8 +361,21 @@ function mergeConfigs(defaultConfig, userConfig) {
*/ */
async function saveConfig(config) { async function saveConfig(config) {
try { try {
await fs.writeFile(DEFAULT_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8'); // Always try to save to the primary config location first
console.log('Configuration saved'); try {
// Make sure directory exists
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) {
console.warn(`Could not save config to ${DEFAULT_CONFIG_PATH}: ${primaryError.message}`);
console.warn('Trying fallback location...');
// If we couldn't save to the primary location, try the fallback
await fsPromises.writeFile(FALLBACK_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
console.log(`Configuration saved to fallback location: ${FALLBACK_CONFIG_PATH}`);
}
} catch (error) { } catch (error) {
console.error('Error saving configuration:', error); console.error('Error saving configuration:', error);
throw error; throw error;
@@ -256,8 +399,8 @@ async function startServer() {
config.securitySettings?.sslKeyPath) { config.securitySettings?.sslKeyPath) {
try { try {
const sslOptions = { const sslOptions = {
key: await fs.readFile(config.securitySettings.sslKeyPath), key: await fsPromises.readFile(config.securitySettings.sslKeyPath),
cert: await fs.readFile(config.securitySettings.sslCertPath) cert: await fsPromises.readFile(config.securitySettings.sslCertPath)
}; };
server = https.createServer(sslOptions, app); server = https.createServer(sslOptions, app);
@@ -329,7 +472,7 @@ app.get('/api/status', authenticateJWT, async (req, res) => {
res.json({ res.json({
success: true, success: true,
status: 'running', status: 'running',
version: '1.2.0', version: APP_VERSION, // Use the dynamic APP_VERSION from package.json
transmissionConnected: transmissionStatus.connected, transmissionConnected: transmissionStatus.connected,
transmissionVersion: transmissionStatus.version, transmissionVersion: transmissionStatus.version,
transmissionStats: { transmissionStats: {
@@ -379,9 +522,45 @@ app.post('/api/config', authenticateJWT, async (req, res) => {
// Merge the new config with the existing one // Merge the new config with the existing one
const newConfig = { ...config, ...req.body }; const newConfig = { ...config, ...req.body };
// Keep passwords if they're not provided // Preserve existing Transmission configuration values when not explicitly provided
if (newConfig.transmissionConfig && !newConfig.transmissionConfig.password && config.transmissionConfig) { if (newConfig.transmissionConfig && config.transmissionConfig) {
newConfig.transmissionConfig.password = config.transmissionConfig.password; // 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 // Keep user passwords
@@ -760,7 +939,15 @@ app.post('/api/transmission/remove', authenticateJWT, async (req, res) => {
app.post('/api/transmission/test', authenticateJWT, async (req, res) => { app.post('/api/transmission/test', authenticateJWT, async (req, res) => {
try { 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 // Create a temporary client for testing
const testConfig = { const testConfig = {
@@ -769,26 +956,48 @@ app.post('/api/transmission/test', authenticateJWT, async (req, res) => {
port: port || config.transmissionConfig.port, port: port || config.transmissionConfig.port,
username: username || config.transmissionConfig.username, username: username || config.transmissionConfig.username,
password: password || config.transmissionConfig.password, 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 || {}
} }
}; };
const testClient = new TransmissionClient(testConfig); // Log the actual test config (without password)
const status = await testClient.getStatus(); console.log('Test configuration:', {
host: testConfig.transmissionConfig.host,
port: testConfig.transmissionConfig.port,
path: testConfig.transmissionConfig.path,
isRemote: testConfig.remoteConfig.isRemote
});
if (status.connected) { try {
res.json({ const testClient = new TransmissionClient(testConfig);
success: true, const status = await testClient.getStatus();
message: 'Successfully connected to Transmission server',
data: { if (status.connected) {
version: status.version, res.json({
rpcVersion: status.rpcVersion success: true,
} message: 'Successfully connected to Transmission server',
}); data: {
} else { version: status.version,
res.status(400).json({ rpcVersion: status.rpcVersion
}
});
} else {
res.status(400).json({
success: false,
message: `Failed to connect: ${status.error}`
});
}
} catch (error) {
console.error('Error testing Transmission connection:', error);
res.status(500).json({
success: false, success: false,
message: `Failed to connect: ${status.error}` message: `Error testing connection: ${error.message}`
}); });
} }
} catch (error) { } catch (error) {
@@ -890,17 +1099,17 @@ async function getMediaLibrary(searchQuery) {
try { try {
// Check if directory exists // Check if directory exists
await fs.access(destinationPath); await fsPromises.access(destinationPath);
// Get directory listing // Get directory listing
const files = await fs.readdir(destinationPath, { withFileTypes: true }); const files = await fsPromises.readdir(destinationPath, { withFileTypes: true });
// Process each file/directory // Process each file/directory
for (const file of files) { for (const file of files) {
const fullPath = path.join(destinationPath, file.name); const fullPath = path.join(destinationPath, file.name);
try { try {
const stats = await fs.stat(fullPath); const stats = await fsPromises.stat(fullPath);
// Create an item object // Create an item object
const item = { const item = {
@@ -1041,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 // Catch-all route for SPA
app.get('*', (req, res) => { app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html')); res.sendFile(path.join(__dirname, 'public', 'index.html'));

48
temp_work/SUMMARY.md Normal file
View File

@@ -0,0 +1,48 @@
# Changes for Moving Config File to /etc/transmission-rss-manager
## Overview
This update moves the configuration file from the application directory to `/etc/transmission-rss-manager/config.json` for better system organization, while maintaining backward compatibility by checking the original location as a fallback.
## Changes Made
### 1. Server.js Updates
- Changed default config location to `/etc/transmission-rss-manager/config.json`
- Added fallback location (`path.join(__dirname, 'config.json')`) for backward compatibility
- Enhanced `loadConfig()` function to try primary location first, then fallback location
- Updated `saveConfig()` function to try saving to primary location first, then fallback
- Added `installPath` property to configuration to store the application installation path for easier updates
### 2. Installer Updates
- Added `CONFIG_DIR="/etc/transmission-rss-manager"` variable to config-module.sh
- Updated `create_directories()` in dependencies-module.sh to create the config directory
- Updated permission settings to ensure proper access to config directory
- Added checks for the CONFIG_DIR variable to ensure it's set
- Modified service-setup-module.sh to include CONFIG_DIR in the environment variables for the systemd service
- Updated file-creator-module.sh to write the initial config.json to the new location
### 3. Documentation Updates
- Updated README.md to reflect new config file location
- Added entry to the changelog for version 2.0.3
- Updated code examples in README
- Bumped version from 2.0.2 to 2.0.3 in both README.md and package.json
## Technical Details
### Config File Loading Process
1. Try to load config from primary location (`/etc/transmission-rss-manager/config.json`)
2. If not found, try fallback location (`<install_dir>/config.json`)
3. If neither exists, create a new config file at primary location
4. If primary location is not writable, fall back to application directory
### Configuration Storage Logic
- Primary storage location: `/etc/transmission-rss-manager/config.json`
- Fallback location: `<install_dir>/config.json`
- Automatic migration from fallback to primary when possible
- Always try to write to primary first, then fallback if needed
## Benefits
- More standard Linux configuration location in /etc
- Easier to find and edit configuration
- Clear separation between code and configuration
- Maintains backward compatibility with existing installations
- Simplified update process by storing installation path

131
temp_work/app-update.js Normal file
View File

@@ -0,0 +1,131 @@
// Client-side JavaScript for system status and updates
// Add this to the public/js/app.js file
// System status and updates functionality
function initSystemStatus() {
// Elements
const versionElement = document.getElementById('system-version');
const uptimeElement = document.getElementById('uptime');
const transmissionStatusElement = document.getElementById('transmission-status');
const updateStatusElement = document.getElementById('update-status');
const updateAvailableDiv = document.getElementById('update-available');
const updateButton = document.getElementById('btn-update-now');
const refreshButton = document.getElementById('btn-refresh-status');
// Load system status
function loadSystemStatus() {
fetch('/api/system/status')
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
versionElement.textContent = data.data.version;
uptimeElement.textContent = data.data.uptime;
// Update transmission status with icon
if (data.data.transmissionStatus === 'Connected') {
transmissionStatusElement.innerHTML = '<i class="fas fa-check-circle text-success"></i> Connected';
} else {
transmissionStatusElement.innerHTML = '<i class="fas fa-times-circle text-danger"></i> Disconnected';
}
} else {
showToast('error', 'Failed to load system status');
}
})
.catch(error => {
console.error('Error fetching system status:', error);
showToast('error', 'Failed to connect to server');
});
}
// Check for updates
function checkForUpdates() {
updateStatusElement.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Checking...';
updateAvailableDiv.classList.add('d-none');
fetch('/api/system/check-updates')
.then(response => response.json())
.then(data => {
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}`;
} else {
updateStatusElement.innerHTML = '<i class="fas fa-check-circle text-success"></i> Up to date';
}
} else {
updateStatusElement.innerHTML = '<i class="fas fa-times-circle text-danger"></i> Check failed';
showToast('error', data.message || 'Failed to check for updates');
}
})
.catch(error => {
console.error('Error checking for updates:', error);
updateStatusElement.innerHTML = '<i class="fas fa-times-circle text-danger"></i> Check failed';
showToast('error', 'Failed to connect to server');
});
}
// Apply update
function applyUpdate() {
// Show confirmation dialog
if (!confirm('Are you sure you want to update the application? The service will restart.')) {
return;
}
// Show loading state
updateButton.disabled = true;
updateButton.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Updating...';
showToast('info', 'Applying update. Please wait...');
fetch('/api/system/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
showToast('success', 'Update applied successfully. The page will reload in 30 seconds.');
// Set a timer to reload the page after the service has time to restart
setTimeout(() => {
window.location.reload();
}, 30000);
} else {
updateButton.disabled = false;
updateButton.innerHTML = '<i class="fas fa-download"></i> Update Now';
showToast('error', data.message || 'Failed to apply update');
}
})
.catch(error => {
console.error('Error applying update:', error);
updateButton.disabled = false;
updateButton.innerHTML = '<i class="fas fa-download"></i> Update Now';
showToast('error', 'Failed to connect to server');
});
}
// Event listeners
if (refreshButton) {
refreshButton.addEventListener('click', () => {
loadSystemStatus();
checkForUpdates();
});
}
if (updateButton) {
updateButton.addEventListener('click', applyUpdate);
}
// Initialize
loadSystemStatus();
checkForUpdates();
// Set interval to refresh uptime every minute
setInterval(loadSystemStatus, 60000);
}
// Call this function from the main init function
// Add this line to your document ready or DOMContentLoaded handler:
// initSystemStatus();

View File

@@ -0,0 +1,72 @@
#!/bin/bash
# Transmission RSS Manager - Bootstrap Installer
# This script downloads the latest version from git and runs the setup
# Color and formatting
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
BOLD='\033[1m'
# Installation directory
INSTALL_DIR="/opt/trans-install"
REPO_URL="https://git.powerdata.dk/masterdraco/transmission-rss-manager.git"
# 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}"
exit 1
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
# Check for git installation
echo -e "${YELLOW}Checking dependencies...${NC}"
if ! command -v git &> /dev/null; then
echo -e "Git not found. Installing git..."
apt-get update
apt-get install -y git
fi
# Check if installation directory exists
if [ -d "$INSTALL_DIR" ]; then
echo -e "${YELLOW}Installation directory already exists.${NC}"
read -p "Do you want to remove it and perform a fresh install? (y/n): " choice
if [[ "$choice" =~ ^[Yy]$ ]]; then
echo "Removing existing installation..."
rm -rf "$INSTALL_DIR"
else
echo -e "${RED}Installation aborted.${NC}"
exit 1
fi
fi
# Create installation directory
echo -e "${YELLOW}Creating installation directory...${NC}"
mkdir -p "$INSTALL_DIR"
# Clone the repository
echo -e "${YELLOW}Cloning the latest version from git...${NC}"
git clone "$REPO_URL" "$INSTALL_DIR"
if [ $? -ne 0 ]; then
echo -e "${RED}Failed to clone the repository.${NC}"
exit 1
fi
# Run the main installer
echo -e "${YELLOW}Running the main installer...${NC}"
cd "$INSTALL_DIR"
chmod +x main-installer.sh
./main-installer.sh
# Installation complete
echo -e "${GREEN}${BOLD}Bootstrap installation complete!${NC}"
echo -e "Transmission RSS Manager has been installed in $INSTALL_DIR"
echo -e "You can access the web interface at http://localhost:3000"
echo
echo -e "To update in the future, use the update button in the System Status section of the web interface."

View File

@@ -0,0 +1,374 @@
#!/bin/bash
# Configuration module for Transmission RSS Manager Installation
# Configuration variables with defaults
INSTALL_DIR="/opt/transmission-rss-manager"
SERVICE_NAME="transmission-rss-manager"
PORT=3000
# Get default user safely - avoid using root
get_default_user() {
local default_user
# Try logname first to get the user who invoked sudo
if command -v logname &> /dev/null; then
default_user=$(logname 2>/dev/null)
fi
# If logname failed, try SUDO_USER
if [ -z "$default_user" ] && [ -n "$SUDO_USER" ]; then
default_user="$SUDO_USER"
fi
# Fallback to current user if both methods failed
if [ -z "$default_user" ]; then
default_user="$(whoami)"
fi
# Ensure the user is not root
if [ "$default_user" = "root" ]; then
echo "nobody"
else
echo "$default_user"
fi
}
# Initialize default user
USER=$(get_default_user)
# Transmission configuration variables
TRANSMISSION_REMOTE=false
TRANSMISSION_HOST="localhost"
TRANSMISSION_PORT=9091
TRANSMISSION_USER=""
TRANSMISSION_PASS=""
TRANSMISSION_RPC_PATH="/transmission/rpc"
TRANSMISSION_DOWNLOAD_DIR="/var/lib/transmission-daemon/downloads"
TRANSMISSION_DIR_MAPPING="{}"
# Media path defaults
MEDIA_DIR="/mnt/media"
ENABLE_BOOK_SORTING=true
# Helper function to validate port number
validate_port() {
local port="$1"
if [[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ]; then
return 0
else
return 1
fi
}
# Helper function to validate URL hostname
validate_hostname() {
local hostname="$1"
if [[ "$hostname" =~ ^[a-zA-Z0-9]([a-zA-Z0-9\-\.]+[a-zA-Z0-9])?$ ]]; then
return 0
elif [[ "$hostname" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
return 0
else
return 1
fi
}
function gather_configuration() {
log "INFO" "Starting configuration gathering"
echo -e "${BOLD}Installation Configuration:${NC}"
echo -e "Please provide the following configuration parameters:"
echo
read -p "Installation directory [$INSTALL_DIR]: " input_install_dir
if [ -n "$input_install_dir" ]; then
# Validate installation directory
if [[ ! "$input_install_dir" =~ ^/ ]]; then
log "WARN" "Installation directory must be an absolute path. Using default."
else
INSTALL_DIR="$input_install_dir"
fi
fi
# Using fixed port 3000 to avoid permission issues with ports below 1024
log "INFO" "Using port 3000 for the web interface"
log "INFO" "This is to avoid permission issues with ports below 1024 (like port 80)"
PORT=3000
# Get user
read -p "Run as user [$USER]: " input_user
if [ -n "$input_user" ]; then
# Check if user exists
if id "$input_user" &>/dev/null; then
USER="$input_user"
else
log "WARN" "User $input_user does not exist. Using $USER instead."
fi
fi
echo
echo -e "${BOLD}Transmission Configuration:${NC}"
echo -e "Configure connection to your Transmission client:"
echo
# Ask if Transmission is remote
read -p "Is Transmission running on a remote server? (y/n) [n]: " input_remote
if [[ $input_remote =~ ^[Yy]$ ]]; then
TRANSMISSION_REMOTE=true
# Get and validate hostname
while true; do
read -p "Remote Transmission host [localhost]: " input_trans_host
if [ -z "$input_trans_host" ]; then
break
elif validate_hostname "$input_trans_host"; then
TRANSMISSION_HOST="$input_trans_host"
break
else
log "WARN" "Invalid hostname format."
fi
done
# Get and validate port
while true; do
read -p "Remote Transmission port [9091]: " input_trans_port
if [ -z "$input_trans_port" ]; then
break
elif validate_port "$input_trans_port"; then
TRANSMISSION_PORT="$input_trans_port"
break
else
log "WARN" "Invalid port number. Port must be between 1 and 65535."
fi
done
# Get credentials
read -p "Remote Transmission username []: " input_trans_user
TRANSMISSION_USER=${input_trans_user:-$TRANSMISSION_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
if [ -n "$input_trans_pass" ]; then
# TODO: In a production environment, consider encrypting this password
TRANSMISSION_PASS="$input_trans_pass"
fi
read -p "Remote Transmission RPC path [/transmission/rpc]: " input_trans_path
if [ -n "$input_trans_path" ]; then
# Ensure path starts with / for consistency
if [[ ! "$input_trans_path" =~ ^/ ]]; then
input_trans_path="/$input_trans_path"
fi
TRANSMISSION_RPC_PATH="$input_trans_path"
fi
# 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: " 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: " LOCAL_DOWNLOAD_DIR
LOCAL_DOWNLOAD_DIR=${LOCAL_DOWNLOAD_DIR:-"/mnt/transmission-downloads"}
# Create mapping JSON - use proper JSON escaping for directory paths
REMOTE_DOWNLOAD_DIR_ESCAPED=$(echo "$REMOTE_DOWNLOAD_DIR" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g')
LOCAL_DOWNLOAD_DIR_ESCAPED=$(echo "$LOCAL_DOWNLOAD_DIR" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g')
TRANSMISSION_DIR_MAPPING="{\"$REMOTE_DOWNLOAD_DIR_ESCAPED\": \"$LOCAL_DOWNLOAD_DIR_ESCAPED\"}"
# Create the local directory
if ! mkdir -p "$LOCAL_DOWNLOAD_DIR"; then
log "ERROR" "Failed to create local download directory: $LOCAL_DOWNLOAD_DIR"
else
if ! chown -R "$USER:$USER" "$LOCAL_DOWNLOAD_DIR"; then
log "ERROR" "Failed to set permissions on local download directory: $LOCAL_DOWNLOAD_DIR"
fi
fi
# Ask if want to add more mappings
while true; do
read -p "Add another directory mapping? (y/n) [n]: " add_another
if [[ ! $add_another =~ ^[Yy]$ ]]; then
break
fi
read -p "Remote directory path: " remote_dir
read -p "Corresponding local directory path: " local_dir
if [ -n "$remote_dir" ] && [ -n "$local_dir" ]; then
# Escape directory paths for JSON
remote_dir_escaped=$(echo "$remote_dir" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g')
local_dir_escaped=$(echo "$local_dir" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g')
# Update mapping JSON (proper JSON manipulation)
# Remove the closing brace, add a comma and the new mapping, then close with brace
TRANSMISSION_DIR_MAPPING="${TRANSMISSION_DIR_MAPPING%\}}, \"$remote_dir_escaped\": \"$local_dir_escaped\"}"
# Create the local directory
if ! mkdir -p "$local_dir"; then
log "ERROR" "Failed to create directory: $local_dir"
else
if ! chown -R "$USER:$USER" "$local_dir"; then
log "WARN" "Failed to set permissions on directory: $local_dir"
fi
log "INFO" "Mapping added: $remote_dir$local_dir"
fi
fi
done
# Set Transmission download dir for configuration
TRANSMISSION_DOWNLOAD_DIR=$REMOTE_DOWNLOAD_DIR
else
# Local Transmission selected
echo -e "${YELLOW}You've selected to use a local Transmission installation.${NC}"
# Check if Transmission is already installed
if command -v transmission-daemon &> /dev/null; then
echo -e "${GREEN}Transmission is already installed on this system.${NC}"
else
echo -e "${YELLOW}NOTE: Transmission does not appear to be installed on this system.${NC}"
echo -e "${YELLOW}You will be prompted to install it during the dependency installation step.${NC}"
fi
# Get and validate port
while true; do
read -p "Local Transmission port [9091]: " input_trans_port
if [ -z "$input_trans_port" ]; then
break
elif validate_port "$input_trans_port"; then
TRANSMISSION_PORT="$input_trans_port"
break
else
log "WARN" "Invalid port number. Port must be between 1 and 65535."
fi
done
# Get credentials if any
read -p "Local Transmission username (leave empty if authentication is disabled) []: " input_trans_user
TRANSMISSION_USER=${input_trans_user:-$TRANSMISSION_USER}
if [ -n "$input_trans_user" ]; then
# Use read -s for password to hide it
read -s -p "Local Transmission password []: " input_trans_pass
echo # Add a newline after the password input
if [ -n "$input_trans_pass" ]; then
TRANSMISSION_PASS="$input_trans_pass"
fi
fi
read -p "Transmission download directory [/var/lib/transmission-daemon/downloads]: " input_trans_dir
if [ -n "$input_trans_dir" ]; then
if [[ ! "$input_trans_dir" =~ ^/ ]]; then
log "WARN" "Download directory must be an absolute path. Using default."
else
TRANSMISSION_DOWNLOAD_DIR="$input_trans_dir"
fi
fi
fi
echo
echo -e "${BOLD}Media Destination Configuration:${NC}"
read -p "Media destination base directory [/mnt/media]: " input_media_dir
if [ -n "$input_media_dir" ]; then
if [[ ! "$input_media_dir" =~ ^/ ]]; then
log "WARN" "Media directory must be an absolute path. Using default."
else
MEDIA_DIR="$input_media_dir"
fi
fi
# Ask about enabling book/magazine sorting
echo
echo -e "${BOLD}Content Type Configuration:${NC}"
read -p "Enable book and magazine sorting? (y/n) [y]: " input_book_sorting
if [[ $input_book_sorting =~ ^[Nn]$ ]]; then
ENABLE_BOOK_SORTING=false
else
ENABLE_BOOK_SORTING=true
fi
# Security configuration
echo
echo -e "${BOLD}Security Configuration:${NC}"
# Ask about enabling authentication
read -p "Enable authentication? (y/n) [n]: " input_auth_enabled
AUTH_ENABLED=false
ADMIN_USERNAME=""
ADMIN_PASSWORD=""
if [[ $input_auth_enabled =~ ^[Yy]$ ]]; then
AUTH_ENABLED=true
# Get admin username and password
read -p "Admin username [admin]: " input_admin_username
ADMIN_USERNAME=${input_admin_username:-"admin"}
# Use read -s for password to avoid showing it on screen
read -s -p "Admin password: " input_admin_password
echo # Add a newline after the password input
if [ -z "$input_admin_password" ]; then
# Generate a random password if none provided
ADMIN_PASSWORD=$(openssl rand -base64 12)
echo -e "${YELLOW}Generated random admin password: $ADMIN_PASSWORD${NC}"
echo -e "${YELLOW}Please save this password somewhere safe!${NC}"
else
ADMIN_PASSWORD="$input_admin_password"
fi
fi
# Ask about enabling HTTPS
read -p "Enable HTTPS? (requires SSL certificate) (y/n) [n]: " input_https_enabled
HTTPS_ENABLED=false
SSL_CERT_PATH=""
SSL_KEY_PATH=""
if [[ $input_https_enabled =~ ^[Yy]$ ]]; then
HTTPS_ENABLED=true
# Get SSL certificate paths
read -p "SSL certificate path: " input_ssl_cert_path
if [ -n "$input_ssl_cert_path" ]; then
# Check if file exists
if [ -f "$input_ssl_cert_path" ]; then
SSL_CERT_PATH="$input_ssl_cert_path"
else
log "WARN" "SSL certificate file not found. HTTPS will be disabled."
HTTPS_ENABLED=false
fi
else
log "WARN" "SSL certificate path not provided. HTTPS will be disabled."
HTTPS_ENABLED=false
fi
# Only ask for key if cert was found
if [ "$HTTPS_ENABLED" = true ]; then
read -p "SSL key path: " input_ssl_key_path
if [ -n "$input_ssl_key_path" ]; then
# Check if file exists
if [ -f "$input_ssl_key_path" ]; then
SSL_KEY_PATH="$input_ssl_key_path"
else
log "WARN" "SSL key file not found. HTTPS will be disabled."
HTTPS_ENABLED=false
fi
else
log "WARN" "SSL key path not provided. HTTPS will be disabled."
HTTPS_ENABLED=false
fi
fi
fi
echo
log "INFO" "Configuration gathering complete"
echo -e "${GREEN}Configuration complete!${NC}"
echo
}

View File

@@ -0,0 +1,184 @@
#!/bin/bash
# Dependencies module for Transmission RSS Manager Installation
function install_dependencies() {
log "INFO" "Installing dependencies..."
# Check for package manager
if command -v apt-get &> /dev/null; then
# Update package index
apt-get update
# Install Node.js and npm if not already installed
if ! command_exists node; then
log "INFO" "Installing Node.js and npm..."
apt-get install -y ca-certificates curl gnupg
mkdir -p /etc/apt/keyrings
# Check if download succeeds
if ! curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; then
log "ERROR" "Failed to download Node.js GPG key"
exit 1
fi
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main" > /etc/apt/sources.list.d/nodesource.list
# Update again after adding repo
apt-get update
# Install nodejs
if ! apt-get install -y nodejs; then
log "ERROR" "Failed to install Node.js"
exit 1
fi
else
log "INFO" "Node.js is already installed."
fi
# Check if we need to install Transmission (only if local transmission was selected and not remote)
if [ "$TRANSMISSION_REMOTE" = false ] && ([ "$TRANSMISSION_HOST" = "localhost" ] || [ "$TRANSMISSION_HOST" = "127.0.0.1" ]); then
if ! command_exists transmission-daemon; then
log "INFO" "Local Transmission installation selected, but transmission-daemon is not installed."
read -p "Would you like to install Transmission now? (y/n): " install_transmission
if [[ "$install_transmission" =~ ^[Yy]$ ]]; then
log "INFO" "Installing Transmission..."
if ! apt-get install -y transmission-daemon; then
log "ERROR" "Failed to install Transmission"
log "WARN" "You will need to install Transmission manually before using this application."
else
# Stop transmission-daemon to allow configuration changes
systemctl stop transmission-daemon
# Set default settings
TRANSMISSION_SETTINGS_DIR="/var/lib/transmission-daemon/info"
if [ -f "$TRANSMISSION_SETTINGS_DIR/settings.json" ]; then
# Backup original settings
cp "$TRANSMISSION_SETTINGS_DIR/settings.json" "$TRANSMISSION_SETTINGS_DIR/settings.json.bak"
# Update RPC settings to allow our app to connect
sed -i 's/"rpc-authentication-required": true,/"rpc-authentication-required": false,/g' "$TRANSMISSION_SETTINGS_DIR/settings.json"
sed -i 's/"rpc-whitelist-enabled": true,/"rpc-whitelist-enabled": false,/g' "$TRANSMISSION_SETTINGS_DIR/settings.json"
log "INFO" "Transmission has been configured for local access."
else
log "WARN" "Could not find Transmission settings file. You may need to configure Transmission manually."
fi
# Start transmission-daemon
systemctl start transmission-daemon
log "INFO" "Transmission has been installed and started."
fi
else
log "WARN" "Transmission installation skipped. You will need to install it manually."
fi
else
log "INFO" "Transmission is already installed."
fi
fi
# Install additional dependencies
log "INFO" "Installing additional dependencies..."
# Try to install unrar-free if unrar is not available
if ! apt-get install -y unrar 2>/dev/null; then
log "INFO" "unrar not available, trying unrar-free instead..."
apt-get install -y unrar-free
fi
# Install other dependencies
apt-get install -y unzip p7zip-full
# Try to install nginx
apt-get install -y nginx || log "WARN" "Nginx installation failed, web interface may not be accessible"
else
log "ERROR" "This installer requires apt-get package manager"
log "INFO" "Please install the following dependencies manually:"
log "INFO" "- Node.js (v18.x)"
log "INFO" "- npm"
log "INFO" "- unrar"
log "INFO" "- unzip"
log "INFO" "- p7zip-full"
log "INFO" "- nginx"
if [ "$TRANSMISSION_REMOTE" = false ] && ([ "$TRANSMISSION_HOST" = "localhost" ] || [ "$TRANSMISSION_HOST" = "127.0.0.1" ]); then
log "INFO" "- transmission-daemon"
fi
exit 1
fi
# Check if all dependencies were installed successfully
local dependencies=("node" "npm" "unzip" "nginx")
local missing_deps=()
# Add transmission to dependencies check if local installation was selected and not remote
if [ "$TRANSMISSION_REMOTE" = false ] && ([ "$TRANSMISSION_HOST" = "localhost" ] || [ "$TRANSMISSION_HOST" = "127.0.0.1" ]); then
dependencies+=("transmission-daemon")
fi
for dep in "${dependencies[@]}"; do
if ! command_exists "$dep"; then
missing_deps+=("$dep")
fi
done
# Check for either unrar or unrar-free
if ! command_exists "unrar" && ! command_exists "unrar-free"; then
missing_deps+=("unrar")
fi
# Check for either 7z or 7za (from p7zip-full)
if ! command_exists "7z" && ! command_exists "7za"; then
missing_deps+=("p7zip")
fi
if [ ${#missing_deps[@]} -eq 0 ]; then
log "INFO" "All dependencies installed successfully."
else
log "ERROR" "Failed to install some dependencies: ${missing_deps[*]}"
log "WARN" "Please install them manually and rerun this script."
# More helpful information based on which deps are missing
if [[ " ${missing_deps[*]} " =~ " node " ]]; then
log "INFO" "To install Node.js manually, visit: https://nodejs.org/en/download/"
fi
if [[ " ${missing_deps[*]} " =~ " nginx " ]]; then
log "INFO" "To install nginx manually: sudo apt-get install nginx"
fi
if [[ " ${missing_deps[*]} " =~ " transmission-daemon " ]]; then
log "INFO" "To install Transmission manually: sudo apt-get install transmission-daemon"
log "INFO" "After installation, you may need to configure it by editing /var/lib/transmission-daemon/info/settings.json"
fi
exit 1
fi
}
function create_directories() {
log "INFO" "Creating installation directories..."
# Check if INSTALL_DIR is defined
if [ -z "$INSTALL_DIR" ]; then
log "ERROR" "INSTALL_DIR is not defined"
exit 1
fi
# Create directories and check for errors
DIRECTORIES=(
"$INSTALL_DIR"
"$INSTALL_DIR/logs"
"$INSTALL_DIR/public/js"
"$INSTALL_DIR/public/css"
"$INSTALL_DIR/modules"
"$INSTALL_DIR/data"
)
for dir in "${DIRECTORIES[@]}"; do
if ! mkdir -p "$dir"; then
log "ERROR" "Failed to create directory: $dir"
exit 1
fi
done
log "INFO" "Directories created successfully."
}

View File

@@ -0,0 +1,241 @@
#!/bin/bash
# Transmission RSS Manager Modular Installer
# Modified to work with the git-based approach
# Set script to exit on error
set -e
# Text formatting
BOLD='\033[1m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Print header
echo -e "${BOLD}==================================================${NC}"
echo -e "${BOLD} Transmission RSS Manager Installer ${NC}"
echo -e "${BOLD} Version 1.2.0 - Git Edition ${NC}"
echo -e "${BOLD}==================================================${NC}"
echo
# Check if script is run with sudo
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Please run as root (use sudo)${NC}"
exit 1
fi
# Get current directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# Check for installation type
IS_UPDATE=false
if [ -f "${SCRIPT_DIR}/config.json" ]; then
IS_UPDATE=true
echo -e "${YELLOW}Existing installation detected. 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}"
fi
export IS_UPDATE
# Check if required module files exist
REQUIRED_MODULES=(
"${SCRIPT_DIR}/modules/config-module.sh"
"${SCRIPT_DIR}/modules/utils-module.sh"
"${SCRIPT_DIR}/modules/dependencies-module.sh"
"${SCRIPT_DIR}/modules/service-setup-module.sh"
)
for module in "${REQUIRED_MODULES[@]}"; do
if [ ! -f "$module" ]; then
echo -e "${RED}Error: Required module file not found: $module${NC}"
echo -e "${YELLOW}The module files should be included in the git repository.${NC}"
exit 1
fi
done
# Source the module files
source "${SCRIPT_DIR}/modules/utils-module.sh" # Load utilities first for logging
source "${SCRIPT_DIR}/modules/config-module.sh"
source "${SCRIPT_DIR}/modules/dependencies-module.sh"
source "${SCRIPT_DIR}/modules/service-setup-module.sh"
# Function to handle cleanup on error
function cleanup_on_error() {
log "ERROR" "Installation failed: $1"
log "INFO" "Cleaning up..."
# Add any cleanup steps here if needed
log "INFO" "You can try running the installer again after fixing the issues."
exit 1
}
# Set trap for error handling
trap 'cleanup_on_error "$BASH_COMMAND"' ERR
# Execute the installation steps in sequence
log "INFO" "Starting installation process..."
# Step 1: Gather configuration from user
log "INFO" "Gathering configuration..."
gather_configuration || {
log "ERROR" "Configuration gathering failed"
exit 1
}
# Step 2: Install dependencies
log "INFO" "Installing dependencies..."
install_dependencies || {
log "ERROR" "Dependency installation failed"
exit 1
}
# Step 3: Create installation directories
log "INFO" "Creating directories..."
create_directories || {
log "ERROR" "Directory creation failed"
exit 1
}
# Step 4: Create configuration files only (no application files since they're from git)
log "INFO" "Creating configuration files..."
create_config_files || {
log "ERROR" "Configuration file creation failed"
exit 1
}
# Step 5: Create service files and install the service
log "INFO" "Setting up service..."
setup_service || {
log "ERROR" "Service setup failed"
exit 1
}
# Step 6: Install npm dependencies
log "INFO" "Installing npm dependencies..."
cd "$SCRIPT_DIR"
npm install || {
log "ERROR" "NPM installation failed"
exit 1
}
# 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
cat > "${SCRIPT_DIR}/scripts/update.sh" << 'EOL'
#!/bin/bash
# Transmission RSS Manager - Update Script
# This script pulls the latest version from git and runs necessary updates
# Color and formatting
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
BOLD='\033[1m'
# Installation directory (should be current directory)
INSTALL_DIR=$(pwd)
# Check if we're in the right directory
if [ ! -f "$INSTALL_DIR/package.json" ] || [ ! -d "$INSTALL_DIR/modules" ]; then
echo -e "${RED}Error: This script must be run from the installation directory.${NC}"
exit 1
fi
# Get the current version
CURRENT_VERSION=$(grep -oP '"version": "\K[^"]+' package.json)
echo -e "${YELLOW}Current version: ${BOLD}$CURRENT_VERSION${NC}"
# Check for git repository
if [ ! -d ".git" ]; then
echo -e "${RED}Error: This installation was not set up using git.${NC}"
echo -e "Please use the bootstrap installer to perform a fresh installation."
exit 1
fi
# Stash any local changes
echo -e "${YELLOW}Backing up any local configuration changes...${NC}"
git stash -q
# Pull the latest changes
echo -e "${YELLOW}Pulling latest updates from git...${NC}"
git pull
if [ $? -ne 0 ]; then
echo -e "${RED}Failed to pull updates. Restoring original state...${NC}"
git stash pop -q
exit 1
fi
# Get the new version
NEW_VERSION=$(grep -oP '"version": "\K[^"]+' package.json)
echo -e "${GREEN}New version: ${BOLD}$NEW_VERSION${NC}"
# Check if update is needed
if [ "$CURRENT_VERSION" == "$NEW_VERSION" ]; then
echo -e "${GREEN}You already have the latest version.${NC}"
exit 0
fi
# Install any new npm dependencies
echo -e "${YELLOW}Installing dependencies...${NC}"
npm install
# Apply any local configuration changes
if git stash list | grep -q "stash@{0}"; then
echo -e "${YELLOW}Restoring local configuration changes...${NC}"
git stash pop -q
# Handle conflicts if any
if [ $? -ne 0 ]; then
echo -e "${RED}There were conflicts when restoring your configuration.${NC}"
echo -e "Please check the files and resolve conflicts manually."
echo -e "Your original configuration is saved in .git/refs/stash"
fi
fi
# Restart the service
echo -e "${YELLOW}Restarting service...${NC}"
if command -v systemctl &> /dev/null; then
sudo systemctl restart transmission-rss-manager
else
echo -e "${RED}Could not restart service automatically.${NC}"
echo -e "Please restart the service manually."
fi
# Update complete
echo -e "${GREEN}${BOLD}Update complete!${NC}"
echo -e "Updated from version $CURRENT_VERSION to $NEW_VERSION"
echo -e "Changes will take effect immediately."
EOL
chmod +x "${SCRIPT_DIR}/scripts/update.sh"
}
# Step 8: Final setup and permissions
log "INFO" "Finalizing setup..."
finalize_setup || {
log "ERROR" "Setup finalization failed"
exit 1
}
# Installation complete
echo
echo -e "${BOLD}${GREEN}==================================================${NC}"
echo -e "${BOLD}${GREEN} Installation Complete! ${NC}"
echo -e "${BOLD}${GREEN}==================================================${NC}"
echo -e "You can access the web interface at: ${BOLD}http://localhost:$PORT${NC} or ${BOLD}http://your-server-ip:$PORT${NC}"
echo -e "You may need to configure your firewall to allow access to port $PORT"
echo
echo -e "${BOLD}Useful Commands:${NC}"
echo -e " To check the service status: ${YELLOW}systemctl status $SERVICE_NAME${NC}"
echo -e " To view logs: ${YELLOW}journalctl -u $SERVICE_NAME${NC}"
echo -e " To restart the service: ${YELLOW}systemctl restart $SERVICE_NAME${NC}"
echo -e " To update the application: ${YELLOW}Use the Update button in the System Status section${NC}"
echo
echo -e "Thank you for installing Transmission RSS Manager!"
echo -e "${BOLD}==================================================${NC}"

View File

@@ -0,0 +1,84 @@
#!/bin/bash
# Transmission RSS Manager - Update Script
# This script pulls the latest version from git and runs necessary updates
# Color and formatting
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
BOLD='\033[1m'
# Installation directory (should be current directory)
INSTALL_DIR=$(pwd)
# Check if we're in the right directory
if [ ! -f "$INSTALL_DIR/package.json" ] || [ ! -d "$INSTALL_DIR/modules" ]; then
echo -e "${RED}Error: This script must be run from the installation directory.${NC}"
exit 1
fi
# Get the current version
CURRENT_VERSION=$(grep -oP '"version": "\K[^"]+' package.json)
echo -e "${YELLOW}Current version: ${BOLD}$CURRENT_VERSION${NC}"
# Check for git repository
if [ ! -d ".git" ]; then
echo -e "${RED}Error: This installation was not set up using git.${NC}"
echo -e "Please use the bootstrap installer to perform a fresh installation."
exit 1
fi
# Stash any local changes
echo -e "${YELLOW}Backing up any local configuration changes...${NC}"
git stash -q
# Pull the latest changes
echo -e "${YELLOW}Pulling latest updates from git...${NC}"
git pull
if [ $? -ne 0 ]; then
echo -e "${RED}Failed to pull updates. Restoring original state...${NC}"
git stash pop -q
exit 1
fi
# Get the new version
NEW_VERSION=$(grep -oP '"version": "\K[^"]+' package.json)
echo -e "${GREEN}New version: ${BOLD}$NEW_VERSION${NC}"
# Check if update is needed
if [ "$CURRENT_VERSION" == "$NEW_VERSION" ]; then
echo -e "${GREEN}You already have the latest version.${NC}"
exit 0
fi
# Install any new npm dependencies
echo -e "${YELLOW}Installing dependencies...${NC}"
npm install
# Apply any local configuration changes
if git stash list | grep -q "stash@{0}"; then
echo -e "${YELLOW}Restoring local configuration changes...${NC}"
git stash pop -q
# Handle conflicts if any
if [ $? -ne 0 ]; then
echo -e "${RED}There were conflicts when restoring your configuration.${NC}"
echo -e "Please check the files and resolve conflicts manually."
echo -e "Your original configuration is saved in .git/refs/stash"
fi
fi
# Restart the service
echo -e "${YELLOW}Restarting service...${NC}"
if command -v systemctl &> /dev/null; then
sudo systemctl restart transmission-rss-manager
else
echo -e "${RED}Could not restart service automatically.${NC}"
echo -e "Please restart the service manually."
fi
# Update complete
echo -e "${GREEN}${BOLD}Update complete!${NC}"
echo -e "Updated from version $CURRENT_VERSION to $NEW_VERSION"
echo -e "Changes will take effect immediately."

View File

@@ -0,0 +1,146 @@
// Version and update endpoints to be added to server.js
// Add these imports at the top of server.js
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
const fs = require('fs');
const path = require('path');
// Add these endpoints
// Get system status including version and uptime
app.get('/api/system/status', async (req, res) => {
try {
// Get package.json for version info
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
const version = packageJson.version;
// 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 {
await transmissionClient.sessionGet();
} catch (err) {
transmissionStatus = 'Disconnected';
}
res.json({
status: 'success',
data: {
version,
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', async (req, res) => {
try {
// Check if git is available and if this is a git repository
const isGitRepo = fs.existsSync(path.join(__dirname, '.git'));
if (!isGitRepo) {
return res.json({
status: 'error',
message: 'This installation is not set up for updates. Please use the bootstrap installer.'
});
}
// Get current version
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
const currentVersion = packageJson.version;
// 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;
return res.json({
status: 'success',
data: {
updateAvailable: true,
currentVersion,
remoteVersion,
commitsBehind: behindCount
}
});
} else {
return res.json({
status: 'success',
data: {
updateAvailable: false,
currentVersion
}
});
}
} catch (error) {
console.error('Error checking for updates:', error);
res.status(500).json({
status: 'error',
message: 'Failed to check for updates'
});
}
});
// Apply updates
app.post('/api/system/update', async (req, res) => {
try {
// Check if git is available and if this is a git repository
const isGitRepo = fs.existsSync(path.join(__dirname, '.git'));
if (!isGitRepo) {
return res.status(400).json({
status: 'error',
message: 'This installation is not set up for updates. 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: error.message
});
}
});

View File

@@ -0,0 +1,41 @@
<!-- This HTML block will be inserted into the index.html dashboard -->
<div class="row">
<div class="col-md-4 mb-3">
<div class="card">
<div class="card-header">
<h4><i class="fas fa-info-circle"></i> System Status</h4>
</div>
<div class="card-body">
<div class="status-item">
<span class="status-label">Version:</span>
<span id="system-version" class="status-value">Loading...</span>
</div>
<div class="status-item">
<span class="status-label">Running since:</span>
<span id="uptime" class="status-value">Loading...</span>
</div>
<div class="status-item">
<span class="status-label">Transmission:</span>
<span id="transmission-status" class="status-value">
<i class="fas fa-circle-notch fa-spin"></i> Checking...
</span>
</div>
<div class="status-item">
<span class="status-label">Update:</span>
<span id="update-status" class="status-value">
<i class="fas fa-circle-notch fa-spin"></i> Checking...
</span>
</div>
<div id="update-available" class="mt-3 d-none">
<div class="alert alert-info">
<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">
<i class="fas fa-download"></i> Update Now
</button>
</div>
</div>
</div>
</div>
</div>
</div>

84
temp_work/update.sh Normal file
View File

@@ -0,0 +1,84 @@
#!/bin/bash
# Transmission RSS Manager - Update Script
# This script pulls the latest version from git and runs necessary updates
# Color and formatting
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
BOLD='\033[1m'
# Installation directory (should be current directory)
INSTALL_DIR=$(pwd)
# Check if we're in the right directory
if [ ! -f "$INSTALL_DIR/package.json" ] || [ ! -d "$INSTALL_DIR/modules" ]; then
echo -e "${RED}Error: This script must be run from the installation directory.${NC}"
exit 1
fi
# Get the current version
CURRENT_VERSION=$(grep -oP '"version": "\K[^"]+' package.json)
echo -e "${YELLOW}Current version: ${BOLD}$CURRENT_VERSION${NC}"
# Check for git repository
if [ ! -d ".git" ]; then
echo -e "${RED}Error: This installation was not set up using git.${NC}"
echo -e "Please use the bootstrap installer to perform a fresh installation."
exit 1
fi
# Stash any local changes
echo -e "${YELLOW}Backing up any local configuration changes...${NC}"
git stash -q
# Pull the latest changes
echo -e "${YELLOW}Pulling latest updates from git...${NC}"
git pull
if [ $? -ne 0 ]; then
echo -e "${RED}Failed to pull updates. Restoring original state...${NC}"
git stash pop -q
exit 1
fi
# Get the new version
NEW_VERSION=$(grep -oP '"version": "\K[^"]+' package.json)
echo -e "${GREEN}New version: ${BOLD}$NEW_VERSION${NC}"
# Check if update is needed
if [ "$CURRENT_VERSION" == "$NEW_VERSION" ]; then
echo -e "${GREEN}You already have the latest version.${NC}"
exit 0
fi
# Install any new npm dependencies
echo -e "${YELLOW}Installing dependencies...${NC}"
npm install
# Apply any local configuration changes
if git stash list | grep -q "stash@{0}"; then
echo -e "${YELLOW}Restoring local configuration changes...${NC}"
git stash pop -q
# Handle conflicts if any
if [ $? -ne 0 ]; then
echo -e "${RED}There were conflicts when restoring your configuration.${NC}"
echo -e "Please check the files and resolve conflicts manually."
echo -e "Your original configuration is saved in .git/refs/stash"
fi
fi
# Restart the service
echo -e "${YELLOW}Restarting service...${NC}"
if command -v systemctl &> /dev/null; then
sudo systemctl restart transmission-rss-manager
else
echo -e "${RED}Could not restart service automatically.${NC}"
echo -e "Please restart the service manually."
fi
# Update complete
echo -e "${GREEN}${BOLD}Update complete!${NC}"
echo -e "Updated from version $CURRENT_VERSION to $NEW_VERSION"
echo -e "Changes will take effect immediately."