From 2b4f369c8bcbce30b32e71c144de27f106c24eff Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 17 Jul 2025 00:20:15 -0400 Subject: [PATCH] Added more missing files. --- home/stormux/.local/files/10-headless.conf | 28 + home/stormux/.local/files/10-screen.conf | 4 + .../.local/files/clipboard_translator.sh | 112 ++++ .../.local/files/speak_window_title.sh | 29 + .../upload_server/templates/classify.html | 119 ++++ .../.local/upload_server/templates/error.html | 24 + .../.local/upload_server/templates/index.html | 377 +++++++++++ .../upload_server/templates/shutdown.html | 49 ++ .../templates/shutdown_confirm.html | 86 +++ .../upload_server/templates/success.html | 308 +++++++++ .../templates/voxin_results.html | 212 ++++++ home/stormux/.local/upload_server/uploader.py | 609 ++++++++++++++++++ 12 files changed, 1957 insertions(+) create mode 100644 home/stormux/.local/files/10-headless.conf create mode 100644 home/stormux/.local/files/10-screen.conf create mode 100755 home/stormux/.local/files/clipboard_translator.sh create mode 100755 home/stormux/.local/files/speak_window_title.sh create mode 100644 home/stormux/.local/upload_server/templates/classify.html create mode 100644 home/stormux/.local/upload_server/templates/error.html create mode 100644 home/stormux/.local/upload_server/templates/index.html create mode 100644 home/stormux/.local/upload_server/templates/shutdown.html create mode 100644 home/stormux/.local/upload_server/templates/shutdown_confirm.html create mode 100644 home/stormux/.local/upload_server/templates/success.html create mode 100644 home/stormux/.local/upload_server/templates/voxin_results.html create mode 100755 home/stormux/.local/upload_server/uploader.py diff --git a/home/stormux/.local/files/10-headless.conf b/home/stormux/.local/files/10-headless.conf new file mode 100644 index 0000000..053bc44 --- /dev/null +++ b/home/stormux/.local/files/10-headless.conf @@ -0,0 +1,28 @@ +Section "Monitor" + Identifier "dummy_monitor" + HorizSync 28.0-80.0 + VertRefresh 48.0-75.0 + Modeline "1920x1080" 172.80 1920 2040 2248 2576 1080 1081 1084 1118 +EndSection + +Section "Device" + Identifier "dummy_card" + Driver "dummy" + VideoRam 256000 + Option "UseFBDev" "false" +EndSection + +Section "Screen" + Identifier "dummy_screen" + Device "dummy_card" + Monitor "dummy_monitor" + SubSection "Display" + Depth 24 + Modes "1920x1080" + EndSubSection +EndSection + +Section "ServerLayout" + Identifier "dummy_layout" + Screen 0 "dummy_screen" +EndSection diff --git a/home/stormux/.local/files/10-screen.conf b/home/stormux/.local/files/10-screen.conf new file mode 100644 index 0000000..44c5d46 --- /dev/null +++ b/home/stormux/.local/files/10-screen.conf @@ -0,0 +1,4 @@ +Section "Device" + Identifier "FBdev" + Driver "fbdev" +EndSection diff --git a/home/stormux/.local/files/clipboard_translator.sh b/home/stormux/.local/files/clipboard_translator.sh new file mode 100755 index 0000000..7e1675b --- /dev/null +++ b/home/stormux/.local/files/clipboard_translator.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash + +# Modified from the script at: +# https://gist.github.com/fdietze/6768a0970d7d732b7fbd7930ccceee2a + +# The next line has been commented because if there's nothing in clipboard the script breaks. +# set -Eeuo pipefail # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/#:~:text=set%20%2Du,is%20often%20highly%20desirable%20behavior. +shopt -s expand_aliases + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 \"application name\" \"file name\"." + exit 1 +fi + +# Wait for the application to start +while ! pgrep -u "$USER" ^$1 &> /dev/null ; do + sleep 0.05 +done + +fileName="${2,,}" +fileName="${fileName//[[:space:]]/-}.sqlite" +translationFile="/home/stormux/.local/files/translations/${fileName}" + +# Make sure the directory exists +mkdir -p "$(dirname "$translationFile")" + +# Initialize database if it doesn't exist +if [[ ! -s "$translationFile" ]]; then + rm -f "$translationFile" + sqlite3 "$translationFile" < /dev/null ; do + sleep 0.05 + text="$(xclip -d "${DISPLAY:-:0}" -selection clipboard -o 2> /dev/null)" + if [[ -f ~/.agmsilent ]]; then + continue + fi + if [[ "${text}" =~ ^[0-9A-Za-z[:space:][:punct:]]+$ ]]; then + spd-say -- "$text" + echo "" | xclip -d "${DISPLAY:-:0}" -selection clipboard 2> /dev/null + continue + fi + if [[ -z "$text" ]]; then + continue + fi + + # https://en.wikipedia.org/wiki/Unicode_equivalence#Combining_and_precomposed_characters + # https://www.effectiveperlprogramming.com/2011/09/normalize-your-perl-source/ + alias nfc="perl -MUnicode::Normalize -CS -ne 'print NFC(\$_)'" # composed characters + + # Normalize different unicode space characters to the same space + # https://stackoverflow.com/a/43640405 + alias normalize_spaces="perl -CSDA -plE 's/[^\\S\\t]/ /g'" + alias normalize_unicode="normalize_spaces | nfc" + + # Normalize text + normalized_text="$(echo "$text" | normalize_unicode)" + + # Create a temporary database for import + sqlite3 "$translationFile" "CREATE TABLE IF NOT EXISTS temp_import(text TEXT, translation TEXT);" + + # Check if we already have a translation + translated=$(sqlite3 "$translationFile" "SELECT translation FROM translations WHERE text = '$normalized_text' LIMIT 1;" 2>/dev/null) + + if [[ -z "$translated" ]]; then + # Get translation from the trans utility + translated="$(trans -no-autocorrect -no-warn -brief "$normalized_text" | head -1 | sed 's/\s*$//' | normalize_unicode)" + + if [[ -n "$translated" ]]; then + # Insert using echo piping to avoid escaping issues + echo "$normalized_text|$translated" | sqlite3 -separator "|" "$translationFile" ".import /dev/stdin temp_import" + sqlite3 "$translationFile" "INSERT OR IGNORE INTO translations SELECT * FROM temp_import; DELETE FROM temp_import;" + fi + fi + + # If we got a translation, speak it + if [[ -n "$translated" ]]; then + spd-say -- "$translated" + fi + + # Clear clipboard + echo "" | xclip -d "${DISPLAY:-:0}" -selection clipboard 2> /dev/null +done + +exit 0 diff --git a/home/stormux/.local/files/speak_window_title.sh b/home/stormux/.local/files/speak_window_title.sh new file mode 100755 index 0000000..eb73553 --- /dev/null +++ b/home/stormux/.local/files/speak_window_title.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Adapted from the bash snippet found at: +# https://bbs.archlinux.org/viewtopic.php?id=117031 + +# Wait for the application to start +while ! pgrep -u "$USER" ^$1 &> /dev/null ; do + sleep 0.05 +done + +# Read so long as the application is running +while pgrep -u "$USER" ^$1 &> /dev/null ; do + sleep 0.05 + if [[ -f ~/.agmsilent ]]; then + continue + fi + wnd_focus=$(xdotool getwindowfocus) + wnd_title=$(xprop -id $wnd_focus WM_NAME) + lookfor='"(.*)"' + + if [[ "$wnd_title" =~ $lookfor ]]; then + wnd_title=${BASH_REMATCH[1]} + if [[ "$old_title" != "$wnd_title" ]]; then + spd-say -- "$wnd_title" + old_title="$wnd_title" + fi + fi +done + +exit 0 diff --git a/home/stormux/.local/upload_server/templates/classify.html b/home/stormux/.local/upload_server/templates/classify.html new file mode 100644 index 0000000..7346e58 --- /dev/null +++ b/home/stormux/.local/upload_server/templates/classify.html @@ -0,0 +1,119 @@ + + + + + + Classify Files + + + + + + +
+ +
+

Classify Unknown Files

+ +

Please select the file type for each uploaded file:

+ +
+ {% for file in unknown_files %} +
+
{{ file.name }}
+
+ Select file type for {{ file.name }} +
+ {% for file_type in file_types %} +
+ + +
+ {% endfor %} +
+
+
+ {% endfor %} + +
+ +
+
+ + +
+ + + + diff --git a/home/stormux/.local/upload_server/templates/error.html b/home/stormux/.local/upload_server/templates/error.html new file mode 100644 index 0000000..b2f4e77 --- /dev/null +++ b/home/stormux/.local/upload_server/templates/error.html @@ -0,0 +1,24 @@ + + + + + + Error + + + +
+

Error

+ + +
+ Shutdown Server +
+
+ + diff --git a/home/stormux/.local/upload_server/templates/index.html b/home/stormux/.local/upload_server/templates/index.html new file mode 100644 index 0000000..64e0059 --- /dev/null +++ b/home/stormux/.local/upload_server/templates/index.html @@ -0,0 +1,377 @@ + + + + + + File Upload Server + + + + + + +
+ +
+

File Upload Server

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + + +
+

Upload Files

+

Upload game ROMs and other individual files.

+ +
+
+
+ + +

Selected files: None

+
+
+ +
+ +
+
+
+ + +
+

Upload Music Folder

+

Upload an entire music folder with its structure directly to your Music directory.

+ +
+
+
+ + +

Selected folder: None

+
+
+ +
+ +
+
+
+ + +
+ + + + diff --git a/home/stormux/.local/upload_server/templates/shutdown.html b/home/stormux/.local/upload_server/templates/shutdown.html new file mode 100644 index 0000000..d95d5aa --- /dev/null +++ b/home/stormux/.local/upload_server/templates/shutdown.html @@ -0,0 +1,49 @@ + + + + + + Shutdown Server + + + + + +
+

Shutdown Server

+ + +

Are you sure you want to shut down the server? You can restart it from the system menu if you want to use it again.

+ +
+ Cancel +
+ +
+
+
+ + diff --git a/home/stormux/.local/upload_server/templates/shutdown_confirm.html b/home/stormux/.local/upload_server/templates/shutdown_confirm.html new file mode 100644 index 0000000..020f82b --- /dev/null +++ b/home/stormux/.local/upload_server/templates/shutdown_confirm.html @@ -0,0 +1,86 @@ + + + + + + Server Shutting Down + + + +
+

Server Shutting Down

+ +
+ +

+ The upload server is shutting down. +

+
+ + + + diff --git a/home/stormux/.local/upload_server/templates/success.html b/home/stormux/.local/upload_server/templates/success.html new file mode 100644 index 0000000..dff68b9 --- /dev/null +++ b/home/stormux/.local/upload_server/templates/success.html @@ -0,0 +1,308 @@ + + + + + + Upload Complete + + + + + +
+

Upload Complete

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + +

Files Processed

+ + {% if uploaded_files %} +
+ Showing 1-{{ uploaded_files|length if uploaded_files|length < 10 else 10 }} of {{ uploaded_files|length }} files +
+ +
+
+
File Name
+
File Type
+
Status
+
+ +
+ {% for file in uploaded_files[:10] %} +
+
+ {{ file.name }} + {% if file.error %} +
Error: {{ file.error }}
+ {% endif %} +
+
{{ file.type|title }}
+
+ {{ 'Success' if file.success else 'Failed' }} +
+
+ {% endfor %} +
+
+ + {% if uploaded_files|length > 10 %} + + {% endif %} + + + {% else %} +

No files were processed.

+ {% endif %} + +
+ Back to main page + Shutdown Server +
+
+ + diff --git a/home/stormux/.local/upload_server/templates/voxin_results.html b/home/stormux/.local/upload_server/templates/voxin_results.html new file mode 100644 index 0000000..5b4efb2 --- /dev/null +++ b/home/stormux/.local/upload_server/templates/voxin_results.html @@ -0,0 +1,212 @@ + + + + + + Voxin Voice Installation + + + + + +
+

Voxin Voice Installation

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + +

Installation Results

+ + {% if results %} +
+
+
Voice Package
+
Status
+
Message
+
+ + {% for result in results %} +
+
{{ result.name }}
+
+ {{ 'Success' if result.success else 'Failed' }} +
+
+ {{ result.message }} + {% if result.details %} +
+ + +
+ {% endif %} +
+
+ {% endfor %} +
+ {% else %} +

No voice packages were processed.

+ {% endif %} + +
+ Back to main page + Shutdown Server +
+
+ + + + diff --git a/home/stormux/.local/upload_server/uploader.py b/home/stormux/.local/upload_server/uploader.py new file mode 100755 index 0000000..e4885ca --- /dev/null +++ b/home/stormux/.local/upload_server/uploader.py @@ -0,0 +1,609 @@ +#!/usr/bin/env python3 + +from flask import Flask, request, redirect, url_for, render_template, flash, jsonify +import os +import mimetypes +import shutil +import subprocess +import socket +import sys +import re +import logging +import threading +import termios +import tty +import select +from threading import Timer +from werkzeug.utils import secure_filename + +app = Flask(__name__) +app.secret_key = os.urandom(24) # For flash messages + +# Configure logging +def setup_logging(): + """Configure logging for the upload server""" + logDir = os.path.expanduser("~/.local/log/stormux") + os.makedirs(logDir, exist_ok=True) + logFile = os.path.join(logDir, "upload_server.log") + + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(logFile), + logging.StreamHandler() + ] + ) + return logging.getLogger(__name__) + +logger = setup_logging() + +# Configuration +UPLOAD_FOLDER = '/tmp/uploads' # Temporary storage +FINAL_DESTINATIONS = { + 'apple': '/home/stormux/.local/games/apple2e/disks', + 'NES': '/home/stormux/Roms/Nintendo Entertainment System', + 'SNES': '/home/stormux/Roms/Super Nintendo Entertainment System', + 'SMS': '/home/stormux/Roms/Sega Master System', + 'music': '/home/stormux/Music', + 'voxin': '/home/stormux/Downloads', + 'other': '/home/stormux/Downloads' +} + +# Allowed file extensions by type (can be easily expanded) +FILE_TYPES = { + 'apple': ['.do', '.dsk'], + 'NES': ['.nes'], + 'SNES': ['.fig', '.smc'], + 'SMS': ['.sms'], + 'music': ['.mp3', '.ogg', '.flac', '.wav', '.m4a', '.opus'], + 'voxin': ['.tgz'], + 'other': [] # Empty list to represent any other file type +} + +# Get local IP address +def get_local_ip(): + try: + # Create a socket connection to determine the local IP + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # Doesn't even have to be reachable + s.connect(('10.255.255.255', 1)) + IP = s.getsockname()[0] + s.close() + except Exception: + IP = '127.0.0.1' + return IP + +# Global variable to store server info for repeat announcement +server_info = {'ip': '', 'port': 0} + +# Announce server start with speech +def announce_server_start(ip, port): + formatted_ip = "" + segments = ip.split(".") + for i, segment in enumerate(segments): + # Add each digit with space between + formatted_ip += " ".join(segment) + # Add " dot " between segments (but not after the last segment) + if i < len(segments) - 1: + formatted_ip += " dot " + + try: + message = f"Upload server running on {formatted_ip} port {port}" + # Using spd-say with -Cw flag for better speech synthesis + subprocess.run(['spd-say', '-Cw', message]) + logger.info(f"Announced server start: {message}") + except Exception as e: + logger.error(f"Could not announce server start: {e}") + print(f"Could not announce server start: {e}") + +# Function to repeat the server announcement +def repeat_announcement(): + if server_info['ip'] and server_info['port']: + announce_server_start(server_info['ip'], server_info['port']) + +# Keyboard listener function +def keyboard_listener(): + """Listen for key presses and repeat server announcement""" + if not sys.stdin.isatty(): + logger.info("Not running in a terminal, keyboard listener disabled") + return + + try: + # Save original terminal settings + old_settings = termios.tcgetattr(sys.stdin) + + while True: + try: + # Set terminal to raw mode to capture individual key presses + tty.setraw(sys.stdin.fileno()) + + # Use select to check if input is available (non-blocking) + if select.select([sys.stdin], [], [], 1) == ([sys.stdin], [], []): + # Read a single character + key = sys.stdin.read(1) + + # Any key press will trigger the announcement + if key: + logger.info(f"Key pressed: {repr(key)}, repeating server announcement") + repeat_announcement() + + except KeyboardInterrupt: + # Handle Ctrl+C gracefully + logger.info("Keyboard listener interrupted") + break + except Exception as e: + logger.error(f"Error in keyboard listener: {e}") + break + finally: + # Always restore terminal settings + try: + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings) + except: + pass + + except Exception as e: + logger.error(f"Could not initialize keyboard listener: {e}") + +# Start keyboard listener in background thread +def start_keyboard_listener(): + """Start the keyboard listener in a daemon thread""" + try: + listener_thread = threading.Thread(target=keyboard_listener, daemon=True) + listener_thread.start() + logger.info("Keyboard listener started") + except Exception as e: + logger.error(f"Could not start keyboard listener: {e}") + +# Set file permissions +def set_file_permissions(filepath): + try: + # Set permissions to 644 (rw-r--r--) + os.chmod(filepath, 0o644) + return True + except OSError as e: + print(f"Warning: Could not set permissions for {filepath}: {e}") + return False + +# Ensure all directories exist and have correct permissions +def setup_directories(): + logger.info("Setting up upload directories") + # Create temp directory + if not os.path.exists(UPLOAD_FOLDER): + try: + os.makedirs(UPLOAD_FOLDER) + # Set permissions to 755 + os.chmod(UPLOAD_FOLDER, 0o755) + logger.info(f"Created temp directory: {UPLOAD_FOLDER}") + except OSError as e: + logger.error(f"Could not create or set permissions for {UPLOAD_FOLDER}: {e}") + print(f"Warning: Could not create or set permissions for {UPLOAD_FOLDER}: {e}") + + # Create all destination directories if they don't exist + for directory in FINAL_DESTINATIONS.values(): + if not os.path.exists(directory): + try: + os.makedirs(directory) + # Set permissions to 755 + os.chmod(directory, 0o755) + logger.info(f"Created destination directory: {directory}") + except OSError as e: + logger.error(f"Could not create or set permissions for {directory}: {e}") + print(f"Warning: Could not create or set permissions for {directory}: {e}") + +# Create directory if it doesn't exist +def ensure_directory_exists(directory): + if not os.path.exists(directory): + try: + os.makedirs(directory) + # Set permissions to 755 + os.chmod(directory, 0o755) + return True + except OSError as e: + print(f"Warning: Could not create or set permissions for {directory}: {e}") + return False + return True + +# Get file type from extension +def get_file_type(filename): + ext = os.path.splitext(filename.lower())[1] + for file_type, extensions in FILE_TYPES.items(): + if ext in extensions: + return file_type + return None + +@app.route('/') +def index(): + return render_template('index.html', file_types=FILE_TYPES.keys()) + +@app.route('/upload', methods=['POST']) +def upload_file(): + client_ip = request.remote_addr + logger.info(f"File upload request from {client_ip}") + + if 'file' not in request.files: + logger.warning(f"Upload request from {client_ip} missing file part") + flash('No file part', 'error') + return redirect(request.url) + + files = request.files.getlist('file') + logger.info(f"Received {len(files)} files from {client_ip}") + + uploaded_files = [] + unknown_files = [] + voxin_results = [] + + for file in files: + if file.filename == '': + continue + + filename = secure_filename(file.filename) + logger.info(f"Processing file: {filename} from {client_ip}") + file_path = os.path.join(UPLOAD_FOLDER, filename) + file.save(file_path) + + # Set file permissions + set_file_permissions(file_path) + + # Check if it's a voxin package by filename pattern + if is_voxin_package(filename): + logger.info(f"Detected voxin package: {filename}") + # Move to Downloads directory + dest_dir = FINAL_DESTINATIONS['voxin'] + dest_path = os.path.join(dest_dir, filename) + shutil.move(file_path, dest_path) + set_file_permissions(dest_path) + + # Process the voxin package + result = handle_voxin_package(dest_path) + logger.info(f"Voxin package processing result for {filename}: {result['success']}") + voxin_results.append({ + 'name': filename, + 'type': 'voxin', + 'success': result['success'], + 'message': result['message'], + 'details': result.get('details', '') + }) + else: + # Handle regular files + file_type = get_file_type(filename) + + if file_type: + logger.info(f"File {filename} categorized as: {file_type}") + # Move to appropriate directory + dest_dir = FINAL_DESTINATIONS.get(file_type, FINAL_DESTINATIONS['other']) + dest_path = os.path.join(dest_dir, filename) + shutil.move(file_path, dest_path) + set_file_permissions(dest_path) + uploaded_files.append({'name': filename, 'type': file_type}) + else: + logger.info(f"File {filename} requires manual classification") + # Keep in temporary folder for classification + unknown_files.append({'name': filename, 'path': file_path}) + + # Handle results + if voxin_results: + # If there were voxin packages, show their installation results + return render_template('voxin_results.html', results=voxin_results) + elif unknown_files: + # If there are files needing classification + return render_template('classify.html', unknown_files=unknown_files, file_types=FILE_TYPES.keys()) + else: + # All other files + flash('All files uploaded successfully!', 'success') + return render_template('success.html', uploaded_files=uploaded_files) + +@app.route('/upload_music', methods=['POST']) +def upload_music(): + if 'folder' not in request.files: + flash('No folder selected', 'error') + return redirect('/') + + files = request.files.getlist('folder') + + if len(files) == 0 or files[0].filename == '': + flash('No files in selected folder', 'error') + return redirect('/') + + # Process each file + processed_files = [] + skipped_files = [] + + # Get the root folder name from the first file + first_file = files[0] + if '/' not in first_file.filename: + flash('Invalid folder structure in uploaded files', 'error') + return redirect('/') + + # Extract the root folder name + root_folder_name = secure_filename(first_file.filename.split('/')[0]) + + # Create the main folder in Music directory + music_root_dir = os.path.join(FINAL_DESTINATIONS['music'], root_folder_name) + ensure_directory_exists(music_root_dir) + + for file in files: + if file.filename == '': + continue + + # Get the relative path within the folder + rel_path = file.filename + + # Skip if there's no path structure + if '/' not in rel_path: + skipped_files.append(rel_path) + continue + + # Extract folder structure: "FolderName/SubFolder/file.mp3" + path_parts = rel_path.split('/') + + # Create secure path components + secure_parts = [secure_filename(part) for part in path_parts] + + # We keep ALL parts including the root folder for the final path + # BUT we need to recreate the structure inside the Music directory + rel_dest_path = '/'.join(secure_parts) + + # Create the full destination path - put directly in Music/root_folder_name/... + dest_path = os.path.join(FINAL_DESTINATIONS['music'], rel_dest_path) + dest_dir = os.path.dirname(dest_path) + + # Ensure the destination directory exists + ensure_directory_exists(dest_dir) + + # Save to temp location first + temp_path = os.path.join(UPLOAD_FOLDER, secure_parts[-1]) + file.save(temp_path) + set_file_permissions(temp_path) + + # Move to final location + try: + shutil.move(temp_path, dest_path) + set_file_permissions(dest_path) + processed_files.append({ + 'name': rel_dest_path, + 'type': 'music', + 'success': True + }) + except Exception as e: + print(f"Error moving file {temp_path} to {dest_path}: {e}") + processed_files.append({ + 'name': rel_dest_path, + 'type': 'music', + 'success': False, + 'error': str(e) + }) + + # Provide feedback based on results + success_count = sum(1 for file in processed_files if file.get('success', False)) + + if success_count > 0: + flash(f'Music folder "{root_folder_name}" uploaded successfully! {success_count} files processed.', 'success') + else: + flash('No files were successfully processed. Please check the folder structure.', 'error') + + return render_template('success.html', uploaded_files=processed_files) + +# Add a function to check if a file is a voxin package +def is_voxin_package(filename): + """Check if the filename starts with 'voxin-' and ends with '.tgz'.""" + return filename.lower().startswith('voxin-') and filename.lower().endswith('.tgz') + +# Add function to handle voxin installation +def handle_voxin_package(file_path): + """ + Process a voxin package: + 1. Extract the package + 2. Run the installer script with -l option + 3. Test the new voice + + Returns: dict with status and message + """ + try: + filename = os.path.basename(file_path) + logger.info(f"Starting voxin package processing: {filename}") + extract_dir = os.path.join('/tmp', 'voxin_extract') + + # Create fresh extraction directory + if os.path.exists(extract_dir): + shutil.rmtree(extract_dir) + os.makedirs(extract_dir) + + # Extract the tarball + logger.info(f"Extracting voxin package: {filename}") + subprocess.run(['tar', 'xf', file_path, '-C', extract_dir], check=True) + + # Find the installer script + installer_path = None + for root, dirs, files in os.walk(extract_dir): + if 'voxin-installer.sh' in files: + installer_path = os.path.join(root, 'voxin-installer.sh') + break + + if not installer_path: + logger.error(f"Voxin installer script not found in package: {filename}") + return { + 'success': False, + 'message': 'Voxin installer script not found in package.' + } + + # Make the installer executable + os.chmod(installer_path, 0o755) + + # Run the installer with -l option for root's speech-dispatcher + logger.info(f"Running voxin installer for: {filename}") + install_result = subprocess.run( + ['sudo', installer_path, '-l'], + capture_output=True, + text=True, + timeout=180 # 3 minute timeout + ) + + # Run the installer with -l option for RootFS's speech-dispatcher + install_result = subprocess.run( + ['sudo', installer_path, '-l'], + capture_output=True, + text=True, + timeout=180 # 3 minute timeout + ) + + if install_result.returncode != 0: + logger.error(f"Voxin installation failed for {filename}: {install_result.stderr}") + return { + 'success': False, + 'message': 'Voxin installation failed.', + 'details': install_result.stderr + } + + # Restart speech-dispatcher + subprocess.run(['killall', 'speech-dispatcher'], + stderr=subprocess.DEVNULL) # Ignore errors if not running + + # Wait a moment for service to fully terminate + subprocess.run(['sleep', '2']) + + # Test the voice + test_message = "If you hear this message, Voxin installation was successful, and you may now change the voice from the system menu." + test_result = subprocess.run( + ['spd-say', '-o', 'voxin', test_message], + capture_output=True, + text=True + ) + + # Clean up extraction directory + shutil.rmtree(extract_dir) + + logger.info(f"Voxin package installation completed successfully: {filename}") + return { + 'success': True, + 'message': 'Voice package installed successfully!', + 'details': 'The test message should have been spoken using the voxin voice system.' + } + + except subprocess.TimeoutExpired: + logger.error(f"Voxin installation timed out for: {filename}") + return { + 'success': False, + 'message': 'Voice installation timed out.', + 'details': 'The installation process took too long and was terminated.' + } + except Exception as e: + logger.error(f"Voxin installation error for {filename}: {e}") + return { + 'success': False, + 'message': 'Voice installation error.', + 'details': str(e) + } + +@app.route('/classify', methods=['POST']) +def classify_files(): + results = [] + + for filename, file_type in request.form.items(): + if filename == 'csrf_token': + continue + + file_path = os.path.join(UPLOAD_FOLDER, filename) + + if os.path.exists(file_path): + # Move to selected directory + dest_dir = FINAL_DESTINATIONS.get(file_type, FINAL_DESTINATIONS['other']) + dest_path = os.path.join(dest_dir, filename) + shutil.move(file_path, dest_path) + set_file_permissions(dest_path) + results.append({'name': filename, 'type': file_type, 'success': True}) + else: + results.append({'name': filename, 'success': False}) + + flash('Files classified and moved successfully!', 'success') + return render_template('success.html', uploaded_files=results) + +@app.route('/check_local', methods=['GET']) +def check_local(): + # Get client IP + client_ip = request.remote_addr + + # Define local networks + local_networks = ['127.0.0.1', '192.168.', '10.', '172.16.', '172.17.', '172.18.', '172.19.', '172.20.', + '172.21.', '172.22.', '172.23.', '172.24.', '172.25.', '172.26.', '172.27.', '172.28.', + '172.29.', '172.30.', '172.31.'] + + # Check if IP is local + is_local = any(client_ip.startswith(network) for network in local_networks) + + if is_local: + return jsonify({'status': 'success', 'local': True}) + else: + return jsonify({'status': 'error', 'local': False}), 403 + +# Add a shutdown route to close the application +@app.route('/shutdown', methods=['GET', 'POST']) +def shutdown(): + # Function to shutdown the server + def shutdown_server(): + # Announce shutdown + try: + # Using spd-say with -Cw flag for better speech synthesis + subprocess.run(['spd-say', '-Cw', 'Server shutting down']) + except Exception: + pass + + # Force exit with os._exit which is more aggressive than sys.exit + os._exit(0) + + # Render confirmation page on GET + if request.method == 'GET': + return render_template('shutdown.html') + + # Process shutdown on POST + flash('Server is shutting down...', 'info') + # Use a shorter timer to give time for the response to be sent + Timer(0.5, shutdown_server).start() + return render_template('shutdown_confirm.html') + +# Middleware to check if request is from local network +@app.before_request +def check_local_network(): + # Skip for certain routes + if request.endpoint == 'check_local': + return + + # Get client IP + client_ip = request.remote_addr + + # Define local networks + local_networks = ['127.0.0.1', '192.168.', '10.', '172.16.', '172.17.', '172.18.', '172.19.', '172.20.', + '172.21.', '172.22.', '172.23.', '172.24.', '172.25.', '172.26.', '172.27.', '172.28.', + '172.29.', '172.30.', '172.31.'] + + # Check if IP is local + is_local = any(client_ip.startswith(network) for network in local_networks) + + if not is_local: + return render_template('error.html', message="Access denied: Local network only"), 403 + +if __name__ == '__main__': + logger.info("Starting upload server") + + # Set up directories + setup_directories() + + # Get local IP and port + local_ip = get_local_ip() + port = 5000 + + # Store server info for repeat announcements + server_info['ip'] = local_ip + server_info['port'] = port + + logger.info(f"Server will run on {local_ip}:{port}") + + # Start keyboard listener for repeat announcements + start_keyboard_listener() + + # Announce server start + announce_server_start(local_ip, port) + + # Run the server + logger.info("Upload server started successfully") + app.run(host='0.0.0.0', port=port, debug=True)