From 9b2dcb2b82f945b4a31f9c82bdb06b025a04acbd Mon Sep 17 00:00:00 2001 From: Sylvain Kraisin Date: Wed, 13 Aug 2025 13:27:37 +0200 Subject: [PATCH] symlink feature --- SYMLINK_FEATURE.md | 77 ++++++++++++++++++++++++++++++++++ ports/RGSX/controls.py | 8 ++++ ports/RGSX/display.py | 10 ++++- ports/RGSX/languages/de.json | 8 +++- ports/RGSX/languages/en.json | 8 +++- ports/RGSX/languages/es.json | 8 +++- ports/RGSX/languages/fr.json | 8 +++- ports/RGSX/network.py | 18 ++++++-- ports/RGSX/symlink_settings.py | 63 ++++++++++++++++++++++++++++ 9 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 SYMLINK_FEATURE.md create mode 100644 ports/RGSX/symlink_settings.py diff --git a/SYMLINK_FEATURE.md b/SYMLINK_FEATURE.md new file mode 100644 index 0000000..2aa6508 --- /dev/null +++ b/SYMLINK_FEATURE.md @@ -0,0 +1,77 @@ +# Symlink Option Feature + +## Overview + +This feature adds a simple toggle option to append the platform folder name to the download path, creating a symlink-friendly structure for external storage. + +## How It Works + +When the symlink option is **disabled** (default): +- Super Nintendo ROMs download to: `../roms/snes/` +- PlayStation 2 ROMs download to: `../roms/ps2/` + +When the symlink option is **enabled**: +- Super Nintendo ROMs download to: `../roms/snes/snes/` +- PlayStation 2 ROMs download to: `../roms/ps2/ps2/` + +This allows users to create symlinks from the platform folder to external storage locations. + +## Usage + +1. Open the pause menu (P key or Start button) +2. Navigate to "Symlink Option" (last option in the menu) +3. Press Enter to toggle the option on/off +4. The menu will show the current status: "Symlink option enabled" or "Symlink option disabled" + +## Implementation Details + +### Files Added +- `symlink_settings.py` - Core functionality for managing the symlink option + +### Files Modified +- `display.py` - Added symlink option to pause menu with dynamic status display +- `controls.py` - Added handling for symlink option toggle +- `network.py` - Modified download functions to use symlink paths when enabled +- Language files - Added translation strings for all supported languages + +### Configuration + +The symlink setting is stored in `symlink_settings.json` in the save folder: + +```json +{ + "use_symlink_path": false +} +``` + +### API Functions + +- `get_symlink_option()` - Get current symlink option status +- `set_symlink_option(enabled)` - Enable/disable the symlink option +- `apply_symlink_path(base_path, platform_folder)` - Apply symlink path modification + +## Example Use Case + +1. Enable the symlink option +2. **Optional**: Create a symlink: `ln -s /external/storage/snes ../roms/snes/snes` + - If you don't create the symlink, the nested directories will be created automatically when you download ROMs +3. Download ROMs - the nested directories (like `../roms/snes/snes/`) will be created automatically if they don't exist +4. Now Super Nintendo ROMs will download to the external storage via the symlink (if created) or to the local nested directory + +## Features + +- **Simple Toggle**: Easy on/off switch in the pause menu +- **Persistent Settings**: Option is remembered between sessions +- **Multi-language Support**: Full internationalization +- **Backward Compatible**: Disabled by default, doesn't affect existing setups +- **Platform Agnostic**: Works with all platforms automatically +- **Automatic Directory Creation**: Nested directories are created automatically if they don't exist + +## Technical Notes + +- The option is disabled by default +- Settings are stored in JSON format +- Path modification is applied at download time +- Works with both regular downloads and 1fichier downloads +- No impact on existing ROMs or folder structure +- Missing directories are automatically created using `os.makedirs(dest_dir, exist_ok=True)` diff --git a/ports/RGSX/controls.py b/ports/RGSX/controls.py index ea7d93e..8f8d8e8 100644 --- a/ports/RGSX/controls.py +++ b/ports/RGSX/controls.py @@ -880,6 +880,14 @@ def handle_controls(event, sources, joystick, screen): config.confirm_selection = 0 config.needs_redraw = True logger.debug(f"Passage à confirm_exit depuis pause_menu") + elif config.selected_option == 8: # Symlink option + from symlink_settings import set_symlink_option, get_symlink_option + current_status = get_symlink_option() + success, message = set_symlink_option(not current_status) + config.popup_message = message + config.popup_timer = 3000 if success else 5000 + config.needs_redraw = True + logger.info(f"Symlink option {'activée' if not current_status else 'désactivée'} via menu pause") elif is_input_matched(event, "cancel"): config.menu_state = validate_menu_state(config.previous_menu_state) config.needs_redraw = True diff --git a/ports/RGSX/display.py b/ports/RGSX/display.py index c2f08bb..56b81e8 100644 --- a/ports/RGSX/display.py +++ b/ports/RGSX/display.py @@ -1214,6 +1214,13 @@ def draw_pause_menu(screen, selected_option): else: music_option = _("menu_music_disabled") + # Option symlink dynamique + from symlink_settings import get_symlink_option + if get_symlink_option(): + symlink_option = _("symlink_option_enabled") + else: + symlink_option = _("symlink_option_disabled") + options = [ _("menu_controls"), _("menu_remap_controls"), @@ -1222,7 +1229,8 @@ def draw_pause_menu(screen, selected_option): _("menu_accessibility"), _("menu_redownload_cache"), music_option, # Ici l'option dynamique - _("menu_quit") + _("menu_quit"), + symlink_option ] menu_width = int(config.screen_width * 0.8) diff --git a/ports/RGSX/languages/de.json b/ports/RGSX/languages/de.json index f4b5fcb..299b569 100644 --- a/ports/RGSX/languages/de.json +++ b/ports/RGSX/languages/de.json @@ -186,5 +186,11 @@ "utils_permission_denied": "Berechtigung während der Extraktion verweigert: {0}", "utils_extraction_failed": "Extraktion fehlgeschlagen: {0}", "utils_unrar_unavailable": "Befehl unrar nicht verfügbar", - "utils_rar_list_failed": "Fehler beim Auflisten der RAR-Dateien: {0}" + "utils_rar_list_failed": "Fehler beim Auflisten der RAR-Dateien: {0}", + + "menu_symlink_option": "Symlink-Option", + "symlink_option_enabled": "Symlink-Option aktiviert", + "symlink_option_disabled": "Symlink-Option deaktiviert", + "symlink_settings_saved_successfully": "Symlink-Einstellungen erfolgreich gespeichert", + "symlink_settings_save_error": "Fehler beim Speichern der Symlink-Einstellungen" } \ No newline at end of file diff --git a/ports/RGSX/languages/en.json b/ports/RGSX/languages/en.json index b09bd7b..7a0c9e6 100644 --- a/ports/RGSX/languages/en.json +++ b/ports/RGSX/languages/en.json @@ -177,5 +177,11 @@ "controls_cancel_back": "Cancel/Back", "controls_history": "History", "controls_clear_history": "Clear History", - "controls_filter_search": "Filter/Search" + "controls_filter_search": "Filter/Search", + + "menu_symlink_option": "Symlink Option", + "symlink_option_enabled": "Symlink option enabled", + "symlink_option_disabled": "Symlink option disabled", + "symlink_settings_saved_successfully": "Symlink settings saved successfully", + "symlink_settings_save_error": "Error saving symlink settings" } \ No newline at end of file diff --git a/ports/RGSX/languages/es.json b/ports/RGSX/languages/es.json index ddc8370..888bf1d 100644 --- a/ports/RGSX/languages/es.json +++ b/ports/RGSX/languages/es.json @@ -188,5 +188,11 @@ "utils_permission_denied": "Permiso denegado durante la extracción: {0}", "utils_extraction_failed": "Error en la extracción: {0}", "utils_unrar_unavailable": "Comando unrar no disponible", - "utils_rar_list_failed": "Error al listar los archivos RAR: {0}" + "utils_rar_list_failed": "Error al listar los archivos RAR: {0}", + + "menu_symlink_option": "Opción Symlink", + "symlink_option_enabled": "Opción symlink habilitada", + "symlink_option_disabled": "Opción symlink deshabilitada", + "symlink_settings_saved_successfully": "Configuración symlink guardada con éxito", + "symlink_settings_save_error": "Error al guardar la configuración symlink" } \ No newline at end of file diff --git a/ports/RGSX/languages/fr.json b/ports/RGSX/languages/fr.json index d245e2b..ac0d6ad 100644 --- a/ports/RGSX/languages/fr.json +++ b/ports/RGSX/languages/fr.json @@ -187,5 +187,11 @@ "controls_mapping_title": "Controls configuration", "controls_mapping_instruction": "Hold for 3s to configure:", "controls_mapping_waiting": "Waiting for a key or button...", - "controls_mapping_press": "Press a key or button" + "controls_mapping_press": "Press a key or button", + + "menu_symlink_option": "Option Symlink", + "symlink_option_enabled": "Option symlink activée", + "symlink_option_disabled": "Option symlink désactivée", + "symlink_settings_saved_successfully": "Paramètres symlink sauvegardés avec succès", + "symlink_settings_save_error": "Erreur lors de la sauvegarde des paramètres symlink" } \ No newline at end of file diff --git a/ports/RGSX/network.py b/ports/RGSX/network.py index a96ae33..65dba4f 100644 --- a/ports/RGSX/network.py +++ b/ports/RGSX/network.py @@ -176,14 +176,19 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas def download_thread(): logger.debug(f"Thread téléchargement démarré pour {url}, task_id={task_id}") try: + # Use symlink path if enabled + from symlink_settings import apply_symlink_path + dest_dir = None for platform_dict in config.platform_dicts: if platform_dict["platform"] == platform: - dest_dir = os.path.join(config.ROMS_FOLDER, platform_dict.get("folder", normalize_platform_name(platform))) + platform_folder = platform_dict.get("folder", normalize_platform_name(platform)) + dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder) logger.debug(f"Répertoire de destination trouvé pour {platform}: {dest_dir}") break if not dest_dir: - dest_dir = os.path.join(os.path.dirname(os.path.dirname(config.APP_FOLDER)), normalize_platform_name(platform)) + platform_folder = normalize_platform_name(platform) + dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder) os.makedirs(dest_dir, exist_ok=True) if not os.access(dest_dir, os.W_OK): @@ -377,14 +382,19 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= try: link = url.split('&af=')[0] logger.debug(f"URL nettoyée: {link}") + # Use symlink path if enabled + from symlink_settings import apply_symlink_path + dest_dir = None for platform_dict in config.platform_dicts: if platform_dict["platform"] == platform: - dest_dir = os.path.join(config.ROMS_FOLDER, platform_dict.get("folder", normalize_platform_name(platform))) + platform_folder = platform_dict.get("folder", normalize_platform_name(platform)) + dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder) break if not dest_dir: logger.warning(f"Aucun dossier 'folder' trouvé pour la plateforme {platform}") - dest_dir = os.path.join(os.path.dirname(os.path.dirname(config.APP_FOLDER)), platform) + platform_folder = normalize_platform_name(platform) + dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder) logger.debug(f"Répertoire destination déterminé: {dest_dir}") logger.debug(f"Vérification répertoire destination: {dest_dir}") diff --git a/ports/RGSX/symlink_settings.py b/ports/RGSX/symlink_settings.py new file mode 100644 index 0000000..ac753c1 --- /dev/null +++ b/ports/RGSX/symlink_settings.py @@ -0,0 +1,63 @@ +import os +import json +import logging +import config +from language import _ + +logger = logging.getLogger(__name__) + +# Path for symlink settings +SYMLINK_SETTINGS_PATH = os.path.join(config.SAVE_FOLDER, "symlink_settings.json") + +def load_symlink_settings(): + """Load symlink settings from file.""" + try: + if os.path.exists(SYMLINK_SETTINGS_PATH): + with open(SYMLINK_SETTINGS_PATH, 'r', encoding='utf-8') as f: + settings = json.load(f) + if not isinstance(settings, dict): + settings = {} + if "use_symlink_path" not in settings: + settings["use_symlink_path"] = False + return settings + except Exception as e: + logger.error(f"Error loading symlink settings: {str(e)}") + + # Return default settings (disabled) + return {"use_symlink_path": False} + +def save_symlink_settings(settings): + """Save symlink settings to file.""" + try: + os.makedirs(config.SAVE_FOLDER, exist_ok=True) + with open(SYMLINK_SETTINGS_PATH, 'w', encoding='utf-8') as f: + json.dump(settings, f, indent=2) + logger.debug(f"Symlink settings saved: {settings}") + return True + except Exception as e: + logger.error(f"Error saving symlink settings: {str(e)}") + return False + +def set_symlink_option(enabled): + """Enable or disable the symlink option.""" + settings = load_symlink_settings() + settings["use_symlink_path"] = enabled + + if save_symlink_settings(settings): + return True, _("symlink_settings_saved_successfully") + else: + return False, _("symlink_settings_save_error") + +def get_symlink_option(): + """Get current symlink option status.""" + settings = load_symlink_settings() + return settings.get("use_symlink_path", False) + +def apply_symlink_path(base_path, platform_folder): + """Apply symlink path modification if enabled.""" + if get_symlink_option(): + # Append the platform folder name to create symlink path + return os.path.join(base_path, platform_folder, platform_folder) + else: + # Return original path + return os.path.join(base_path, platform_folder)