#!/bin/bash # Exit on error with unset variables, and fail pipe if any command in it fails. set -u set -o pipefail APPNAME="Auto Subtitle Translator" VERSION=$(date +%Y-%m-%d) # Use current date as version BOLD=$(tput bold) BLUE=$(tput setaf 4) RED=$(tput setaf 1) CYAN=$(tput setaf 6) YELLOW=$(tput setaf 3) MAGENTA=$(tput setaf 5) GREEN=$(tput setaf 2) STOP_COLOR=$(tput sgr0) # --- Configuration for the Auto Subtitle Translator --- INSTALL_DIR_BASE="$HOME/scripts/auto-subtitle-translator" VENV_DIR="$INSTALL_DIR_BASE/venv" PYTHON_SCRIPT_NAME="media_monitor.py" # Name of the Python script to be installed TMUX_RUNNER_SCRIPT_NAME="start-auto-subtitle-translator.sh" # Name of the tmux runner script TMUX_SESSION_NAME="auto-subtitle-translator" # Name used for the tmux session # --- Helper Functions --- print_welcome_message() { term_width=$(tput cols) welcome_message="[[ Welcome to the ${APPNAME} Setup Script v${VERSION} ]]" padding_length=$(( (term_width - ${#welcome_message}) / 2 )) # Ensure padding_length is not negative if (( padding_length < 0 )); then padding_length=0; fi padding=$(printf '%*s' $padding_length '') echo -e "\n${CYAN}${BOLD}${padding}${welcome_message}${STOP_COLOR}\n" echo -e "${YELLOW}${BOLD}[IMPORTANT] Execute this script directly (e.g., 'bash $0' or './$0'). Do NOT source it (e.g., 'source $0').${STOP_COLOR}\n" } spinner() { local pid=$1 local delay=0.1 local spinstr='◐◓◑◒' # Unicode spinner local i=0 printf " " # Initial space for the spinner while ps -p "$pid" > /dev/null 2>&1; do i=$(( (i+1) % ${#spinstr} )) printf "\b${spinstr:$i:1}" sleep "$delay" done printf "\b " # Clear spinner } # --- Core Functions --- check_if_installed() { if [[ ! -d "$INSTALL_DIR_BASE" ]] || [[ ! -f "$INSTALL_DIR_BASE/$PYTHON_SCRIPT_NAME" ]]; then echo -e "${RED}${BOLD}[ERROR] ${APPNAME} does not seem to be installed at '$INSTALL_DIR_BASE'. Please install it first.${STOP_COLOR}" return 1 fi return 0 } write_api_key_to_shell_config() { local key_to_write="$1" local config_file="$2" if [[ -z "$key_to_write" ]]; then echo -e "${RED}${BOLD}[INTERNAL ERROR] No API key provided to write_api_key_to_shell_config.${STOP_COLOR}" return 1 fi if [[ -z "$config_file" ]]; then echo -e "${RED}${BOLD}[INTERNAL ERROR] No shell config file path provided to write_api_key_to_shell_config.${STOP_COLOR}" return 1 fi # Ensure the config file exists, create if not (though .profile should exist, .bashrc/.zshrc might not if minimal setup) touch "$config_file" # Remove any existing GEMINI_API_KEY lines to avoid duplicates, then add the new one # Using a temporary file for sed to avoid issues with in-place editing on some systems/versions tmp_sed_file=$(mktemp) if grep -q "^export GEMINI_API_KEY=" "$config_file"; then sed "/^export GEMINI_API_KEY=/d" "$config_file" > "$tmp_sed_file" && mv "$tmp_sed_file" "$config_file" else rm -f "$tmp_sed_file" # cleanup if not used fi echo "export GEMINI_API_KEY=\"${key_to_write}\"" >> "$config_file" echo -e "${GREEN}${BOLD}[INFO] GEMINI_API_KEY has been set/updated in '$config_file'.${STOP_COLOR}" echo -e "${YELLOW}${BOLD}[ACTION REQUIRED] Please run 'source $config_file' or open a new terminal for the API key to take effect.${STOP_COLOR}" } install_app() { echo -e "${MAGENTA}${BOLD}[STAGE-1] Initial Checks and Configuration${STOP_COLOR}" sleep 1 if [[ -d "$INSTALL_DIR_BASE" ]]; then echo -e "${RED}${BOLD}[ERROR] ${APPNAME} installation directory already exists at:${STOP_COLOR} '$INSTALL_DIR_BASE'" echo -e "${YELLOW}${BOLD}[INFO] To reinstall, please uninstall first or manually remove the directory.${STOP_COLOR}" exit 1 fi # Determine shell configuration file early for API key checks SHELL_CONFIG_FILE="" if [[ -n "${BASH_VERSION-}" ]]; then # Check if BASH_VERSION is set SHELL_CONFIG_FILE="$HOME/.bashrc" elif [[ -n "${ZSH_VERSION-}" ]]; then # Check if ZSH_VERSION is set SHELL_CONFIG_FILE="$HOME/.zshrc" elif [[ -f "$HOME/.profile" ]]; then SHELL_CONFIG_FILE="$HOME/.profile" fi GEMINI_API_KEY_INPUT="" EXISTING_KEY_FOUND=false KEY_SOURCE="" # 1. Check current environment variable if [[ -n "${GEMINI_API_KEY-}" ]]; then # Check if GEMINI_API_KEY is set in env GEMINI_API_KEY_INPUT="$GEMINI_API_KEY" EXISTING_KEY_FOUND=true KEY_SOURCE="current environment" # 2. If not in env, check shell config file elif [[ -n "$SHELL_CONFIG_FILE" ]] && [[ -f "$SHELL_CONFIG_FILE" ]]; then # Extract key from config file, careful about quotes # This grep/sed aims to get the value between the first pair of double quotes EXISTING_KEY_IN_FILE=$(grep "^export GEMINI_API_KEY=" "$SHELL_CONFIG_FILE" | sed -n 's/export GEMINI_API_KEY="\([^"]*\)".*/\1/p' | tail -n 1) if [[ -n "$EXISTING_KEY_IN_FILE" ]]; then GEMINI_API_KEY_INPUT="$EXISTING_KEY_IN_FILE" EXISTING_KEY_FOUND=true KEY_SOURCE="$SHELL_CONFIG_FILE" fi fi if [[ "$EXISTING_KEY_FOUND" == true ]]; then echo -e "${YELLOW}${BOLD}[INFO] An existing GEMINI_API_KEY was found in your ${KEY_SOURCE}.${STOP_COLOR}" read -rp "${BLUE}${BOLD}[INPUT REQUIRED] Do you want to use this existing key? (yes/no): ${STOP_COLOR}" USE_EXISTING_KEY if [[ "${USE_EXISTING_KEY,,}" == "yes" ]] || [[ "${USE_EXISTING_KEY,,}" == "y" ]]; then echo -e "${GREEN}${BOLD}[INFO] Using existing GEMINI_API_KEY.${STOP_COLOR}" # If found in env but not guaranteed in shell config, ensure it's written to shell config now if [[ "$KEY_SOURCE" == "current environment" ]] && [[ -n "$SHELL_CONFIG_FILE" ]]; then echo -e "${YELLOW}${BOLD}[INFO] Ensuring the key from environment is also saved to $SHELL_CONFIG_FILE...${STOP_COLOR}" write_api_key_to_shell_config "$GEMINI_API_KEY_INPUT" "$SHELL_CONFIG_FILE" elif [[ -n "$SHELL_CONFIG_FILE" ]] && ! grep -q "export GEMINI_API_KEY=\"${GEMINI_API_KEY_INPUT}\"" "$SHELL_CONFIG_FILE"; then # If found in file but line is somehow different or user wants to ensure it's the 'active' one. echo -e "${YELLOW}${BOLD}[INFO] Re-saving the key to $SHELL_CONFIG_FILE to ensure it's active...${STOP_COLOR}" write_api_key_to_shell_config "$GEMINI_API_KEY_INPUT" "$SHELL_CONFIG_FILE" fi else read -rp "${BLUE}${BOLD}[INPUT REQUIRED] Enter your NEW GEMINI_API_KEY: ${STOP_COLOR}" GEMINI_API_KEY_INPUT_NEW if [[ -z "$GEMINI_API_KEY_INPUT_NEW" ]]; then echo -e "${RED}${BOLD}[ERROR] GEMINI_API_KEY cannot be empty.${STOP_COLOR}" exit 1 fi GEMINI_API_KEY_INPUT="$GEMINI_API_KEY_INPUT_NEW" # Update to the new key # And write this new/updated key to shell config if [[ -n "$SHELL_CONFIG_FILE" ]]; then write_api_key_to_shell_config "$GEMINI_API_KEY_INPUT" "$SHELL_CONFIG_FILE" else echo -e "${RED}${BOLD}[WARNING] Could not automatically find a common shell configuration file to save the new key.${STOP_COLOR}" echo -e "${YELLOW}${BOLD}[ACTION REQUIRED] You will need to set the GEMINI_API_KEY environment variable manually for the new key.${STOP_COLOR}" fi fi else read -rp "${BLUE}${BOLD}[INPUT REQUIRED] Enter your GEMINI_API_KEY: ${STOP_COLOR}" GEMINI_API_KEY_INPUT_FRESH if [[ -z "$GEMINI_API_KEY_INPUT_FRESH" ]]; then echo -e "${RED}${BOLD}[ERROR] GEMINI_API_KEY cannot be empty.${STOP_COLOR}" exit 1 fi GEMINI_API_KEY_INPUT="$GEMINI_API_KEY_INPUT_FRESH" # Write this freshly entered key to shell config if [[ -n "$SHELL_CONFIG_FILE" ]]; then write_api_key_to_shell_config "$GEMINI_API_KEY_INPUT" "$SHELL_CONFIG_FILE" else echo -e "${RED}${BOLD}[WARNING] Could not automatically find a common shell configuration file to save the key.${STOP_COLOR}" echo -e "${YELLOW}${BOLD}[ACTION REQUIRED] You will need to set the GEMINI_API_KEY environment variable manually.${STOP_COLOR}" fi fi read -rp "${BLUE}${BOLD}[INPUT REQUIRED] Enter the target language for subtitles (e.g., English, Spanish, Brazilian Portuguese): ${STOP_COLOR}" TARGET_LANGUAGE_INPUT if [[ -z "$TARGET_LANGUAGE_INPUT" ]]; then echo -e "${RED}${BOLD}[ERROR] Target language cannot be empty.${STOP_COLOR}" exit 1 fi DEFAULT_MEDIA_DIR="~/media" read -rp "${BLUE}${BOLD}[INPUT REQUIRED] Enter the media folder to monitor (leave empty for default: ${DEFAULT_MEDIA_DIR}): ${STOP_COLOR}" CHOSEN_MEDIA_DIR if [[ -z "$CHOSEN_MEDIA_DIR" ]]; then CHOSEN_MEDIA_DIR="$DEFAULT_MEDIA_DIR" echo -e "${YELLOW}${BOLD}[INFO] Using default media folder: $CHOSEN_MEDIA_DIR${STOP_COLOR}" else echo -e "${YELLOW}${BOLD}[INFO] Using media folder: $CHOSEN_MEDIA_DIR${STOP_COLOR}" fi echo -e "\n${MAGENTA}${BOLD}[STAGE-3] Creating Directories and Virtual Environment${STOP_COLOR}" sleep 1 echo "Creating application directory: $INSTALL_DIR_BASE" mkdir -p "$INSTALL_DIR_BASE" if [ $? -ne 0 ]; then echo -e "${RED}${BOLD}[ERROR] Failed to create directory $INSTALL_DIR_BASE ${STOP_COLOR}"; exit 1; fi echo "Creating Python virtual environment in $VENV_DIR using 'python'..." python -m venv "$VENV_DIR" >/dev/null 2>&1 & spinner $! wait $! if [ $? -ne 0 ]; then echo -e "\n${RED}${BOLD}[ERROR] Failed to create virtual environment. Make sure 'python' (Python 3.10+ recommended) and its 'venv' module are available.${STOP_COLOR}" exit 1; fi echo -e "${GREEN}Virtual environment created successfully.${STOP_COLOR}" sleep 1 echo -e "\n${MAGENTA}${BOLD}[STAGE-4] Installing Python Packages into Virtual Environment${STOP_COLOR}" sleep 1 echo "Activating venv and installing 'gemini-srt-translator' and 'watchdog'..." ( # shellcheck source=/dev/null source "$VENV_DIR/bin/activate" pip install --upgrade pip >/dev/null 2>&1 pip install gemini-srt-translator watchdog >/dev/null 2>&1 & spinner $! wait $! if [ $? -ne 0 ]; then echo -e "\n${RED}${BOLD}[ERROR] Failed to install Python packages into the virtual environment.${STOP_COLOR}"; exit 1; fi deactivate ) echo -e "${GREEN}Python packages installed successfully.${STOP_COLOR}" sleep 1 echo -e "\n${MAGENTA}${BOLD}[STAGE-5] Copying and Configuring Scripts${STOP_COLOR}" sleep 1 MEDIA_MONITOR_DEST="$INSTALL_DIR_BASE/$PYTHON_SCRIPT_NAME" echo "Creating $PYTHON_SCRIPT_NAME at $MEDIA_MONITOR_DEST..." cat << EOF_MEDIA_MONITOR > "$MEDIA_MONITOR_DEST" import logging import os import re import subprocess import threading import time import datetime from concurrent.futures import ThreadPoolExecutor from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer # --- Determine script's own directory for relative paths --- SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) VENV_DIR_ABS = os.path.join(SCRIPT_DIR, "venv") # --- Configuration --- MEDIA_DIR = "PLACEHOLDER_MEDIA_DIR" GST_SCRIPT_IN_VENV = os.path.join(VENV_DIR_ABS, "bin", "gst") TARGET_LANGUAGE = "PLACEHOLDER_TARGET_LANGUAGE" MAX_CONCURRENT_PROCESSES = 3 MONITOR_LOG_FILENAME = "monitor.log" PROCESS_LOG_FILENAME = "process.log" SUPPORTED_EXTENSIONS = (".mkv", ".mp4") FILE_STABLE_CHECK_INTERVAL = 3 # seconds FILE_STABLE_CHECK_RETRIES = 5 # number of checks for stability # --- Helper for console printing --- def print_to_console(message): """Prints message to console with timestamp, matching log format.""" timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") print(f"[{timestamp}] {message}") # --- Set up Logging --- def setup_logger(name, log_filename, base_dir, level=logging.INFO): """Configures and returns a logger for file logging. Ensures no duplicate file handlers.""" logger = logging.getLogger(name) logger.setLevel(level) log_file_path = os.path.join(base_dir, log_filename) handler_exists = False for handler in logger.handlers: if isinstance(handler, logging.FileHandler) and handler.baseFilename == log_file_path: handler_exists = True break if not handler_exists: file_formatter = logging.Formatter( "[%(asctime)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S" ) file_handler = logging.FileHandler(log_file_path, encoding="utf-8") file_handler.setFormatter(file_formatter) file_handler.setLevel(level) logger.addHandler(file_handler) if logger.hasHandlers(): logger.propagate = False return logger monitor_logger = setup_logger("MonitorLogger", MONITOR_LOG_FILENAME, SCRIPT_DIR) process_logger = setup_logger("ProcessLogger", PROCESS_LOG_FILENAME, SCRIPT_DIR) active_files_lock = threading.Lock() active_files = set() def is_file_stable(file_path, interval=FILE_STABLE_CHECK_INTERVAL, retries=FILE_STABLE_CHECK_RETRIES): """Check indefinitely until file size is stable for 'retries' consecutive intervals.""" stable_count = 0 try: last_size = os.path.getsize(file_path) except Exception: return False while True: time.sleep(interval) try: current_size = os.path.getsize(file_path) except Exception: return False if current_size == last_size: stable_count += 1 if stable_count >= retries: return True else: stable_count = 0 last_size = current_size def run_gst_translate(file_path): msg_start = f"Processing task started for: {file_path}" monitor_logger.info(msg_start) print_to_console(msg_start) command = [ GST_SCRIPT_IN_VENV, "translate", "-v", file_path, "-l", TARGET_LANGUAGE, "-s", "1", "--resume", "--skip-upgrade", ] try: msg_exec = f"Executing command: {' '.join(command)}" monitor_logger.info(msg_exec) print_to_console(msg_exec) process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8", errors="replace" ) stdout, stderr = process.communicate() def get_last_text_block(text_content): if not text_content or not text_content.strip(): return "" ansi_sequence = "\x1b[F\x1b[K" parts = text_content.split(ansi_sequence) last_block = parts[-1] cleaned_block = last_block.strip() return cleaned_block last_block_stdout = get_last_text_block(stdout) last_block_stderr = get_last_text_block(stderr) if process.returncode == 0: msg_success = f"Successfully translated: {file_path}" monitor_logger.info(msg_success) print_to_console(msg_success) if last_block_stdout: process_logger.info(f"Last process output block for {file_path}:\n{last_block_stdout}\n") elif last_block_stderr: process_logger.info(f"Last process warning block for {file_path}:\n{last_block_stderr}\n") else: process_logger.info(f"Process completed for {file_path} with no specific last output block found (or block was empty after last marker).\n") else: msg_err_translate = f"Error translating {file_path}. Return code: {process.returncode}" monitor_logger.error(msg_err_translate) print_to_console(f"ERROR: {msg_err_translate}") if last_block_stderr: process_logger.error(f"Last process error block for {file_path}:\n{last_block_stderr}\n") elif last_block_stdout: process_logger.error(f"Last process output block (on error) for {file_path}:\n{last_block_stdout}\n") else: process_logger.error(f"Process failed for {file_path} with no specific last error block found (or block was empty after last marker).\n") except FileNotFoundError: msg_fnf = f"GST script not found at {GST_SCRIPT_IN_VENV}. Ensure 'gemini-srt-translator' is installed." monitor_logger.error(msg_fnf) print_to_console(f"ERROR: {msg_fnf}") except Exception as e: msg_exc = f"An unexpected error occurred during translation of {file_path}: {e}" monitor_logger.error(msg_exc, exc_info=True) print_to_console(f"ERROR: {msg_exc}") finally: with active_files_lock: if file_path in active_files: active_files.remove(file_path) msg_finish = f"Processing task finished for: {file_path}" monitor_logger.info(msg_finish) print_to_console(msg_finish) class MediaFileHandler(FileSystemEventHandler): def __init__(self, executor_pool): self.executor = executor_pool def on_created(self, event): if event.is_directory: return file_path = event.src_path if os.path.splitext(file_path)[1].lower() in SUPPORTED_EXTENSIONS: msg_detect = f"New media file detected: {file_path}" monitor_logger.info(msg_detect) print_to_console(msg_detect) with active_files_lock: if file_path in active_files: msg_skip = f"File {file_path} is already queued/processing. Skipping." monitor_logger.info(msg_skip) print_to_console(msg_skip) return active_files.add(file_path) # Wait for file to become stable before processing msg_wait = f"Waiting for file to become stable: {file_path}" monitor_logger.info(msg_wait) print_to_console(msg_wait) if not is_file_stable(file_path): msg_unstable = f"File {file_path} did not become stable in time. Skipping." monitor_logger.warning(msg_unstable) print_to_console(msg_unstable) with active_files_lock: if file_path in active_files: active_files.remove(file_path) return msg_add_queue = f"Adding {file_path} to processing queue." monitor_logger.info(msg_add_queue) print_to_console(msg_add_queue) self.executor.submit(run_gst_translate, file_path) if __name__ == "__main__": actual_media_dir = os.path.expanduser(MEDIA_DIR) msg_main_start = f"Starting media monitor script in {SCRIPT_DIR}" monitor_logger.info(msg_main_start) print_to_console(msg_main_start) msg_log_loc = f"Log files will be created in: {SCRIPT_DIR}" monitor_logger.info(msg_log_loc) print_to_console(msg_log_loc) msg_gst_loc = f"Using GST from: {GST_SCRIPT_IN_VENV}" monitor_logger.info(msg_gst_loc) print_to_console(msg_gst_loc) if not os.path.isdir(actual_media_dir): msg_err_media_dir = f"Media directory to monitor ('{actual_media_dir}') does not exist. Please create it or update the path." monitor_logger.error(msg_err_media_dir) print_to_console(f"ERROR: {msg_err_media_dir}") exit(1) if not os.path.isfile(GST_SCRIPT_IN_VENV): msg_err_gst_script = f"GST script {GST_SCRIPT_IN_VENV} not found. Ensure 'gemini-srt-translator' is installed." monitor_logger.error(msg_err_gst_script) print_to_console(f"ERROR: {msg_err_gst_script}") exit(1) configs_to_log_and_print = [ f"Monitoring directory: {actual_media_dir} (and subdirectories)", f"Looking for file types: {', '.join(SUPPORTED_EXTENSIONS)}", f"Max concurrent translation processes: {MAX_CONCURRENT_PROCESSES}", f"Target language for translation: {TARGET_LANGUAGE}" ] for config_msg in configs_to_log_and_print: monitor_logger.info(config_msg) print_to_console(config_msg) executor = ThreadPoolExecutor(max_workers=MAX_CONCURRENT_PROCESSES) event_handler = MediaFileHandler(executor) observer = Observer() observer.schedule(event_handler, actual_media_dir, recursive=True) msg_observer_start = "Starting file system observer." monitor_logger.info(msg_observer_start) print_to_console(msg_observer_start) observer.start() try: while True: time.sleep(5) except KeyboardInterrupt: msg_interrupt = "Keyboard interrupt received. Shutting down..." monitor_logger.info(msg_interrupt) print_to_console(msg_interrupt) finally: msg_observer_stop = "Stopping file system observer." monitor_logger.info(msg_observer_stop) print_to_console(msg_observer_stop) observer.stop() observer.join() msg_executor_shutdown = f"Shutting down thread pool executor. Waiting for {len(active_files)} active tasks to complete..." monitor_logger.info(msg_executor_shutdown) print_to_console(msg_executor_shutdown) executor.shutdown(wait=True) msg_shutdown_complete = "All tasks completed. Shutdown complete." monitor_logger.info(msg_shutdown_complete) print_to_console(msg_shutdown_complete) EOF_MEDIA_MONITOR ESCAPED_TARGET_LANGUAGE_INPUT=$(printf '%s\n' "$TARGET_LANGUAGE_INPUT" | sed 's:[&/\]:\\&:g ; s/"/\\"/g') # Use a different delimiter for sed if path contains slashes sed -i.bak "s|TARGET_LANGUAGE = \"PLACEHOLDER_TARGET_LANGUAGE\"|TARGET_LANGUAGE = \"${ESCAPED_TARGET_LANGUAGE_INPUT}\"|" "$MEDIA_MONITOR_DEST" ESCAPED_CHOSEN_MEDIA_DIR=$(printf '%s\n' "$CHOSEN_MEDIA_DIR" | sed 's:[&/\]:\\&:g ; s/"/\\"/g') sed -i.bak "s|MEDIA_DIR = \"PLACEHOLDER_MEDIA_DIR\"|MEDIA_DIR = \"${ESCAPED_CHOSEN_MEDIA_DIR}\"|" "$MEDIA_MONITOR_DEST" rm -f "${MEDIA_MONITOR_DEST}.bak" echo -e "${GREEN}Created and configured $PYTHON_SCRIPT_NAME.${STOP_COLOR}" TMUX_RUNNER_DEST="$INSTALL_DIR_BASE/$TMUX_RUNNER_SCRIPT_NAME" echo "Creating $TMUX_RUNNER_SCRIPT_NAME at $TMUX_RUNNER_DEST..." cat << EOF_TMUX_RUNNER > "$TMUX_RUNNER_DEST" #!/bin/bash # shellcheck disable=SC2034 # TMUX_SESSION_NAME is used by tmux commands TMUX_SESSION_NAME="auto-subtitle-translator" SCRIPT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" &> /dev/null && pwd)" PYTHON_SCRIPT_NAME="media_monitor.py" PYTHON_SCRIPT_PATH="\$SCRIPT_DIR/\$PYTHON_SCRIPT_NAME" PYTHON_VENV_PATH="\$SCRIPT_DIR/venv/bin/python" PYTHON_EXECUTABLE="\$PYTHON_VENV_PATH" if [ ! -f "\$PYTHON_SCRIPT_PATH" ]; then echo "Error: Python script '\$PYTHON_SCRIPT_PATH' not found."; exit 1; fi if [ ! -x "\$PYTHON_EXECUTABLE" ]; then echo "Error: Python interpreter not found or not executable at '\$PYTHON_EXECUTABLE'."; exit 1; fi echo "Checking for existing tmux session: \$TMUX_SESSION_NAME" if tmux has-session -t "\$TMUX_SESSION_NAME" 2>/dev/null; then echo "Tmux session '\$TMUX_SESSION_NAME' already exists." echo "To attach: tmux attach -t \$TMUX_SESSION_NAME" echo "To kill: tmux kill-session -t \$TMUX_SESSION_NAME" else echo "Starting new tmux session '\$TMUX_SESSION_NAME' for: \$PYTHON_SCRIPT_PATH" tmux new-session -d -s "\$TMUX_SESSION_NAME" "cd '\$SCRIPT_DIR' && \$PYTHON_EXECUTABLE \$PYTHON_SCRIPT_NAME ; read -p 'Script finished. Press Enter...'" if [ \$? -eq 0 ]; then echo "Successfully started tmux session '\$TMUX_SESSION_NAME'." echo "Attach: tmux attach -t \$TMUX_SESSION_NAME" else echo "Error: Failed to start tmux session." fi fi exit 0 EOF_TMUX_RUNNER chmod +x "$TMUX_RUNNER_DEST" echo -e "${GREEN}Created and configured $TMUX_RUNNER_SCRIPT_NAME.${STOP_COLOR}" sleep 1 echo -e "\n${GREEN}${BOLD}[SUCCESS] ${APPNAME} installation has been completed successfully!${STOP_COLOR}" echo -e "${YELLOW}${BOLD}[INFO] To start the monitor, navigate to the installation directory and run the start script:${STOP_COLOR}" echo -e " cd \"$INSTALL_DIR_BASE\" && ./$TMUX_RUNNER_SCRIPT_NAME" echo -e "${YELLOW}${BOLD}[INFO] Remember to 'source your shell config file' or open a new terminal if this is the first time setting GEMINI_API_KEY.${STOP_COLOR}" } uninstall_app() { echo -e "\n${MAGENTA}${BOLD}[UNINSTALL] ${APPNAME}${STOP_COLOR}" read -rp "${RED}${BOLD}[CONFIRMATION REQUIRED] Uninstall ${APPNAME}? (yes/no): ${STOP_COLOR}" CONFIRMATION if [[ "${CONFIRMATION,,}" != "yes" && "${CONFIRMATION,,}" != "y" ]]; then echo -e "${YELLOW}[INFO] Uninstallation cancelled.${STOP_COLOR}" exit 0 fi echo -e "\n${MAGENTA}[UNINSTALL-STEP] Stopping active tmux session...${STOP_COLOR}" if tmux has-session -t "$TMUX_SESSION_NAME" 2>/dev/null; then echo -e "${YELLOW}[INFO] Stopping tmux session '$TMUX_SESSION_NAME'...${STOP_COLOR}" tmux kill-session -t "$TMUX_SESSION_NAME" &>/dev/null & spinner $! && wait $! if tmux has-session -t "$TMUX_SESSION_NAME" 2>/dev/null; then echo -e "${RED}[WARNING] Failed to stop tmux session '$TMUX_SESSION_NAME'. Manual check needed.${STOP_COLOR}" else echo -e "${GREEN}[SUCCESS] Tmux session stopped.${STOP_COLOR}" fi else echo -e "${YELLOW}[INFO] Tmux session '$TMUX_SESSION_NAME' not found.${STOP_COLOR}" fi echo -e "\n${MAGENTA}[UNINSTALL-STEP] Removing application files...${STOP_COLOR}" if [[ -d "$INSTALL_DIR_BASE" ]]; then rm -rf "$INSTALL_DIR_BASE" echo -e "${GREEN}[SUCCESS] Application directory removed.${STOP_COLOR}" else echo -e "${YELLOW}[INFO] Application directory not found.${STOP_COLOR}" fi echo -e "\n${MAGENTA}[UNINSTALL-STEP] Handling GEMINI_API_KEY...${STOP_COLOR}" read -rp "${BLUE}[INPUT REQUIRED] Remove GEMINI_API_KEY from shell config? (yes/no): ${STOP_COLOR}" REMOVE_API_KEY_CHOICE if [[ "${REMOVE_API_KEY_CHOICE,,}" == "yes" || "${REMOVE_API_KEY_CHOICE,,}" == "y" ]]; then SHELL_CONFIG_FILE="" if [[ -n "${BASH_VERSION-}" ]]; then SHELL_CONFIG_FILE="$HOME/.bashrc"; elif [[ -n "${ZSH_VERSION-}" ]]; then SHELL_CONFIG_FILE="$HOME/.zshrc"; elif [[ -f "$HOME/.profile" ]]; then SHELL_CONFIG_FILE="$HOME/.profile"; fi if [[ -n "$SHELL_CONFIG_FILE" ]] && [[ -f "$SHELL_CONFIG_FILE" ]]; then if grep -q "^export GEMINI_API_KEY=" "$SHELL_CONFIG_FILE"; then # Using a temporary file for sed to avoid issues with in-place editing on some systems/versions tmp_sed_file_uninstall=$(mktemp) sed "/^export GEMINI_API_KEY=/d" "$SHELL_CONFIG_FILE" > "$tmp_sed_file_uninstall" && mv "$tmp_sed_file_uninstall" "$SHELL_CONFIG_FILE" echo -e "${GREEN}[SUCCESS] GEMINI_API_KEY removed from $SHELL_CONFIG_FILE.${STOP_COLOR}" echo -e "${YELLOW}[ACTION REQUIRED] Source shell config or open new terminal.${STOP_COLOR}" else echo -e "${YELLOW}[INFO] GEMINI_API_KEY not found in $SHELL_CONFIG_FILE.${STOP_COLOR}"; fi else echo -e "${YELLOW}[INFO] Shell config file not found for GEMINI_API_KEY removal.${STOP_COLOR}"; fi else echo -e "${YELLOW}[INFO] GEMINI_API_KEY not removed from shell config.${STOP_COLOR}"; fi echo -e "\n${GREEN}${BOLD}[SUCCESS] ${APPNAME} uninstallation complete.${STOP_COLOR}\n" } upgrade_translator_package() { if ! check_if_installed; then return 1; fi echo -e "\n${MAGENTA}${BOLD}[UPGRADE] Upgrading gemini-srt-translator package...${STOP_COLOR}" ( # shellcheck source=/dev/null source "$VENV_DIR/bin/activate" echo "Current version(s):" pip list | grep -E "gemini-srt-translator|watchdog" echo "Attempting upgrade..." pip install --upgrade gemini-srt-translator >/dev/null 2>&1 & spinner $! wait $! if [ $? -ne 0 ]; then echo -e "\n${RED}${BOLD}[ERROR] Failed to upgrade gemini-srt-translator.${STOP_COLOR}" else echo -e "\n${GREEN}${BOLD}[SUCCESS] gemini-srt-translator upgrade attempt finished.${STOP_COLOR}" echo "New version(s):" pip list | grep -E "gemini-srt-translator|watchdog" fi deactivate ) echo -e "${YELLOW}${BOLD}[INFO] If the monitor was running, restart it for changes to apply: ./$TMUX_RUNNER_SCRIPT_NAME${STOP_COLOR}" } update_api_key_env() { # No need to check if installed, this function can be used to set it initially too if needed by other logic. # However, for menu usage, it's better to keep it consistent that app should be installed. if ! check_if_installed; then echo -e "${YELLOW}${BOLD}[INFO] This option is intended for an existing installation. To set an API key for a new install, use the install option.${STOP_COLOR}" return 1; fi echo -e "\n${MAGENTA}${BOLD}[UPDATE] Updating GEMINI_API_KEY...${STOP_COLOR}" read -rp "${BLUE}${BOLD}[INPUT REQUIRED] Enter your NEW GEMINI_API_KEY: ${STOP_COLOR}" NEW_API_KEY if [[ -z "$NEW_API_KEY" ]]; then echo -e "${RED}${BOLD}[ERROR] API Key cannot be empty.${STOP_COLOR}"; return 1; fi SHELL_CONFIG_FILE="" if [[ -n "${BASH_VERSION-}" ]]; then SHELL_CONFIG_FILE="$HOME/.bashrc"; elif [[ -n "${ZSH_VERSION-}" ]]; then SHELL_CONFIG_FILE="$HOME/.zshrc"; elif [[ -f "$HOME/.profile" ]]; then SHELL_CONFIG_FILE="$HOME/.profile"; fi if [[ -n "$SHELL_CONFIG_FILE" ]] && [[ -f "$SHELL_CONFIG_FILE" ]]; then write_api_key_to_shell_config "$NEW_API_KEY" "$SHELL_CONFIG_FILE" # Use the helper else echo -e "${RED}${BOLD}[WARNING] Shell config file not found. Set API key manually:${STOP_COLOR}" echo -e " export GEMINI_API_KEY=\"${NEW_API_KEY}\"" fi } update_monitored_folder_path() { if ! check_if_installed; then return 1; fi INSTALLED_PYTHON_SCRIPT="$INSTALL_DIR_BASE/$PYTHON_SCRIPT_NAME" echo -e "\n${MAGENTA}${BOLD}[UPDATE] Updating Monitored Media Folder...${STOP_COLOR}" CURRENT_MEDIA_DIR_LINE=$(grep -E "^MEDIA_DIR\s*=\s*" "$INSTALLED_PYTHON_SCRIPT") CURRENT_MEDIA_DIR_VALUE=$(echo "$CURRENT_MEDIA_DIR_LINE" | sed -n 's/MEDIA_DIR\s*=\s*"\(.*\)"/\1/p') if [[ -z "$CURRENT_MEDIA_DIR_VALUE" ]]; then CURRENT_MEDIA_DIR_VALUE="Could not determine current value automatically." fi echo -e "${YELLOW}${BOLD}[INFO] Current monitored folder in script: $CURRENT_MEDIA_DIR_VALUE${STOP_COLOR}" read -rp "${BLUE}${BOLD}[INPUT REQUIRED] Enter the NEW media folder to monitor (e.g., ~/videos or /mnt/storage/media): ${STOP_COLOR}" NEW_MEDIA_DIR if [[ -z "$NEW_MEDIA_DIR" ]]; then echo -e "${RED}${BOLD}[ERROR] Media folder path cannot be empty.${STOP_COLOR}"; return 1; fi ESCAPED_NEW_MEDIA_DIR=$(printf '%s\n' "$NEW_MEDIA_DIR" | sed 's:[&/\]:\\&:g ; s/"/\\"/g') # Escape for sed value # Use a different delimiter for sed if path contains slashes sed -i.bak "s|^MEDIA_DIR\s*=\s*\".*\"|MEDIA_DIR = \"${ESCAPED_NEW_MEDIA_DIR}\"|" "$INSTALLED_PYTHON_SCRIPT" if [ $? -eq 0 ]; then rm -f "${INSTALLED_PYTHON_SCRIPT}.bak" echo -e "${GREEN}${BOLD}[SUCCESS] Monitored media folder updated in '$INSTALLED_PYTHON_SCRIPT' to '$NEW_MEDIA_DIR'.${STOP_COLOR}" echo -e "${YELLOW}${BOLD}[INFO] If the monitor script was running, you'll need to stop and restart it for the change to take effect:${STOP_COLOR}" echo -e " (Inside $INSTALL_DIR_BASE: ./$TMUX_RUNNER_SCRIPT_NAME, or kill and restart session)" else echo -e "${RED}${BOLD}[ERROR] Failed to update the media folder in '$INSTALLED_PYTHON_SCRIPT'. Backup may exist as .bak${STOP_COLOR}" fi } main_fn() { clear print_welcome_message echo -e "${YELLOW}${BOLD}[WARNING] This script manages '${APPNAME}'. Ensure Python 3.10+ is available.${STOP_COLOR}\n" echo -e "${BLUE}${BOLD}[LIST] Operations available for ${APPNAME}:${STOP_COLOR}" echo "1) Install ${APPNAME}" echo "2) Uninstall ${APPNAME}" echo "3) Upgrade gemini-srt-translator Package" echo "4) Update GEMINI_API_KEY" echo -e "5) Update Monitored Media Folder\n" read -rp "${BLUE}${BOLD}[INPUT REQUIRED] Enter your operation choice${STOP_COLOR} '[1-5]'${BLUE}${BOLD}: ${STOP_COLOR}" OPERATION_CHOICE echo case "$OPERATION_CHOICE" in 1) install_app ;; 2) uninstall_app ;; 3) upgrade_translator_package ;; 4) update_api_key_env ;; 5) update_monitored_folder_path ;; *) echo -e "${RED}${BOLD}[ERROR] Invalid choice. Please enter a number 1-5.${STOP_COLOR}"; exit 1 ;; esac } # Call the main function main_fn