Compare commits

...

8 Commits

Author SHA1 Message Date
skymike03
c4913a5fc2 v2.5.0.5 (2026.03.05)
Merge pull request [#48](https://github.com/RetroGameSets/RGSX/issues/48) from elieserdejesus

- Fixing and upgrade Filters and search
- showing fbneo 'full name' instead rom name.
2026-03-05 18:45:01 +01:00
RGS
bf9d3d2de5 Merge pull request #48 from elieserdejesus/filters
Fixing Filters
2026-03-05 15:37:33 +01:00
Elieser de Jesus
9979949bdc Applying filters using 'display_name'
Adding a Game class with a display_name used do show games. The 'display_name'
is the game file name without suffix (.zip, .7z, .bin, etc) and without platform
prefix. Many platforms line NES, mega drive was showing the plaftorm name as prefix.

Now the filters are working with the 'display_name'.

I see the filters are no applyed until the "apply" button is clicked. Now the filters
are applyed everytime the game list is showed.
2026-03-05 11:06:07 -03:00
Elieser de Jesus
9ed264544f adding bootleg filter 2026-03-05 08:40:33 -03:00
Elieser de Jesus
779c060927 removing duplicated entries 2026-03-05 08:36:51 -03:00
RGS
88400e538f Merge pull request #47 from elieserdejesus/main
Showing and filtering FBneo games using 'full name' instead 'rom name'
2026-03-05 09:19:12 +01:00
Elieser de Jesus
cbab067dd6 filtering fbneo games by full name instead rom name 2026-03-04 18:45:18 -03:00
Elieser de Jesus
b4ed0b355d showing fbneo 'full name' instead rom name.
The full names are downloaded from github fbneo
repo only when user select fbneo in platforms screen
2026-03-04 18:42:21 -03:00
10 changed files with 156 additions and 80 deletions

1
.gitignore vendored
View File

@@ -23,3 +23,4 @@ pygame/
data/
docker-compose.test.yml
config/
pyrightconfig.json

View File

@@ -526,7 +526,7 @@ async def main():
# Appui long détecté, ouvrir le scraper
games = config.filtered_games if config.filter_active or config.search_mode else config.games
if games:
game_name = games[config.current_game][0]
game_name = games[config.current_game].name
platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform]
config.previous_menu_state = "game"
@@ -835,12 +835,9 @@ async def main():
logger.debug("Action quit détectée, arrêt de l'application")
elif action == "download" and config.menu_state == "game" and config.filtered_games:
game = config.filtered_games[config.current_game]
if isinstance(game, (list, tuple)):
game_name = game[0]
url = game[1] if len(game) > 1 else None
else: # fallback str
game_name = str(game)
url = None
game_name = game.name
url = game.url
# Nouveau schéma: config.platforms contient déjà platform_name (string)
platform_name = config.platforms[config.current_platform]
if url:

View File

@@ -2,6 +2,15 @@
import os
import logging
import platform
from typing import Optional
from dataclasses import dataclass
@dataclass(slots=True)
class Game:
name: str
url: str
size: str
display_name: str # name withou file extension or platform prefix
# Headless mode for CLI: set env RGSX_HEADLESS=1 to avoid pygame and noisy prints
HEADLESS = os.environ.get("RGSX_HEADLESS") == "1"
@@ -14,7 +23,7 @@ except Exception:
pygame = None # type: ignore
# Version actuelle de l'application
app_version = "2.5.0.4"
app_version = "2.5.0.5"
# Nombre de jours avant de proposer la mise à jour de la liste des jeux
GAMELIST_UPDATE_DAYS = 7
@@ -364,7 +373,8 @@ filter_platforms_dirty = False # indique si modifications non sauvegardées
filter_platforms_selection = [] # copie de travail des plateformes visibles (bool masque?) structure: list of (name, hidden_bool)
# Affichage des jeux et sélection
games = [] # Liste des jeux pour la plateforme actuelle
games: list[Game] = [] # Liste des jeux pour la plateforme actuelle
fbneo_games = {}
current_game = 0 # Index du jeu actuellement sélectionné
menu_state = "loading" # État actuel de l'interface menu
scroll_offset = 0 # Offset de défilement pour la liste des jeux
@@ -389,7 +399,7 @@ selected_language_index = 0 # Index de la langue sélectionnée dans la liste
# Recherche et filtres
filtered_games = [] # Liste des jeux filtrés par recherche ou filtre
filtered_games: list[Game] = [] # Liste des jeux filtrés par recherche ou filtre
search_mode = False # Indicateur si le mode recherche est actif
search_query = "" # Chaîne de recherche saisie par l'utilisateur
filter_active = False # Indicateur si un filtre est appliqué

View File

@@ -8,7 +8,7 @@ import datetime
import threading
import logging
import config
from config import REPEAT_DELAY, REPEAT_INTERVAL, REPEAT_ACTION_DEBOUNCE, CONTROLS_CONFIG_PATH
from config import REPEAT_DELAY, REPEAT_INTERVAL, REPEAT_ACTION_DEBOUNCE, CONTROLS_CONFIG_PATH, Game
from display import draw_validation_transition, show_toast
from network import download_rom, download_from_1fichier, is_1fichier_url, request_cancel
from utils import (
@@ -31,6 +31,8 @@ from rgsx_settings import (
from accessibility import save_accessibility_settings
from scraper import get_game_metadata, download_image_to_surface
from pathlib import Path
logger = logging.getLogger(__name__)
# Extensions d'archives pour lesquelles on ignore l'avertissement d'extension non supportée
@@ -326,6 +328,20 @@ def _launch_next_queued_download():
if config.download_queue:
_launch_next_queued_download()
def filter_games_by_search_query() -> list[Game]:
base_games = config.games
if config.game_filter_obj and config.game_filter_obj.is_active():
base_games = config.game_filter_obj.apply_filters(config.games)
filtered_games = []
for game in base_games:
game_name = game.display_name
if config.search_query.lower() in game_name.lower():
filtered_games.append(game)
return filtered_games
...
def handle_controls(event, sources, joystick, screen):
"""Gère un événement clavier/joystick/souris et la répétition automatique.
Retourne 'quit', 'download', 'redownload', ou None."""
@@ -509,7 +525,7 @@ def handle_controls(event, sources, joystick, screen):
# Jeux
elif config.menu_state == "game":
games = config.filtered_games if config.filter_active or config.search_mode else config.games
games: list[Game] = config.filtered_games if config.filter_active or config.search_mode else config.games
if config.search_mode and getattr(config, 'joystick', False):
keyboard_layout = [
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
@@ -563,10 +579,7 @@ def handle_controls(event, sources, joystick, screen):
elif is_input_matched(event, "confirm"):
config.search_query += keyboard_layout[row][col]
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
base_games = config.games
if config.game_filter_obj and config.game_filter_obj.is_active():
base_games = config.game_filter_obj.apply_filters(config.games)
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
config.filtered_games = filter_games_by_search_query()
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
@@ -575,10 +588,7 @@ def handle_controls(event, sources, joystick, screen):
if config.search_query:
config.search_query = config.search_query[:-1]
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
base_games = config.games
if config.game_filter_obj and config.game_filter_obj.is_active():
base_games = config.game_filter_obj.apply_filters(config.games)
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
config.filtered_games = filter_games_by_search_query()
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
@@ -586,10 +596,7 @@ def handle_controls(event, sources, joystick, screen):
elif is_input_matched(event, "space"):
config.search_query += " "
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
base_games = config.games
if config.game_filter_obj and config.game_filter_obj.is_active():
base_games = config.game_filter_obj.apply_filters(config.games)
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
config.filtered_games = filter_games_by_search_query()
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
@@ -642,10 +649,7 @@ def handle_controls(event, sources, joystick, screen):
if event.unicode.isalnum() or event.unicode == ' ':
config.search_query += event.unicode
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
base_games = config.games
if config.game_filter_obj and config.game_filter_obj.is_active():
base_games = config.game_filter_obj.apply_filters(config.games)
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
config.filtered_games = filter_games_by_search_query()
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
@@ -655,10 +659,7 @@ def handle_controls(event, sources, joystick, screen):
if config.search_query:
config.search_query = config.search_query[:-1]
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
base_games = config.games
if config.game_filter_obj and config.game_filter_obj.is_active():
base_games = config.game_filter_obj.apply_filters(config.games)
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
config.filtered_games = filter_games_by_search_query()
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
@@ -726,8 +727,8 @@ def handle_controls(event, sources, joystick, screen):
if games:
idx = config.current_game
game = games[idx]
url = game[1]
game_name = game[0]
url = game.url
game_name = game.name
platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform]
pending_download = check_extension_before_download(url, platform, game_name)
@@ -3184,8 +3185,8 @@ def handle_controls(event, sources, joystick, screen):
# Déclencher le téléchargement normal
games = config.filtered_games if config.filter_active or config.search_mode else config.games
if games:
url = games[config.current_game][1]
game_name = games[config.current_game][0]
url = games[config.current_game].url
game_name = games[config.current_game].name
platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform]
logger.debug(f"Appui court sur confirm ({press_duration}ms), téléchargement pour {game_name}, URL: {url}")
@@ -3316,8 +3317,8 @@ def handle_controls(event, sources, joystick, screen):
# Déclencher le téléchargement normal (même code que pour KEYUP)
games = config.filtered_games if config.filter_active or config.search_mode else config.games
if games:
url = games[config.current_game][1]
game_name = games[config.current_game][0]
url = games[config.current_game].url
game_name = games[config.current_game].name
platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform]
logger.debug(f"Appui court sur confirm ({press_duration}ms), téléchargement pour {game_name}, URL: {url}")

View File

@@ -21,6 +21,11 @@ from rgsx_settings import (load_rgsx_settings, get_light_mode, get_show_unsuppor
get_hide_premium_systems, get_symlink_option)
from game_filters import GameFilters
import json
from pathlib import Path
from typing import Dict, Any
import urllib.request
logger = logging.getLogger(__name__)
OVERLAY = None # Initialisé dans init_display()
@@ -1154,12 +1159,70 @@ def draw_platform_grid(screen):
for key in keys_to_remove:
del platform_images_cache[key]
FBNEO_GAME_LIST = "fbneo_gamelist.txt"
def download_fbneo_list(path_to_save: str) -> None:
url = "https://raw.githubusercontent.com/libretro/FBNeo/master/gamelist.txt"
path = Path(path_to_save)
if not path.exists():
logger.debug("Downloading fbneo gamelist.txt from github ...")
urllib.request.urlretrieve(url, path)
logger.debug("Download finished:", path)
...
def parse_fbneo_list(path: str) -> Dict[str, Any]:
games : Dict[str, Any] = {}
headers = None
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.rstrip()
if line.startswith("+"):
continue
if "|" not in line:
continue
parts = [p.strip() for p in line.split("|")[1:-1]]
if headers is None:
headers = parts
continue
row = dict(zip(headers, parts))
name = row["name"]
games[name] = row
return games
# Liste des jeux
def draw_game_list(screen):
"""Affiche la liste des jeux avec un style moderne."""
#logger.debug(f"[DRAW_GAME_LIST] Called - platform={config.current_platform}, search_mode={config.search_mode}, filter_active={config.filter_active}")
platform = config.platforms[config.current_platform]
platform_name = config.platform_names.get(platform, platform)
fbneo_selected = platform_name == 'Final Burn Neo'
if fbneo_selected:
fbneo_game_list_path = os.path.join(config.SAVE_FOLDER, FBNEO_GAME_LIST)
download_fbneo_list(fbneo_game_list_path) # download the fbneo game list if necessary - 10 MB file
config.fbneo_games = parse_fbneo_list(fbneo_game_list_path)
for game in config.games:
clean_name = game.display_name
if clean_name in config.fbneo_games:
fbneo_game = config.fbneo_games[clean_name]
game.display_name = fbneo_game["full name"]
...
if config.game_filter_obj and config.game_filter_obj.is_active() and not config.search_query:
config.filtered_games = config.game_filter_obj.apply_filters(config.games)
games = config.filtered_games if config.filter_active or config.search_mode else config.games
game_count = len(games)
#logger.debug(f"[DRAW_GAME_LIST] Games count={game_count}, current_game={config.current_game}, filtered_games={len(config.filtered_games) if config.filtered_games else 0}, config.games={len(config.games) if config.games else 0}")
@@ -1310,13 +1373,9 @@ def draw_game_list(screen):
for i in range(config.scroll_offset, min(config.scroll_offset + items_per_page, len(games))):
item = games[i]
if isinstance(item, (list, tuple)) and item:
game_name = item[0]
size_val = item[2] if len(item) > 2 else None
else:
game_name = str(item)
size_val = None
game_name = item.display_name
size_val = item.size
# Vérifier si le jeu est déjà téléchargé
is_downloaded = is_game_downloaded(platform_name, game_name)

View File

@@ -9,6 +9,8 @@ import re
import logging
from typing import List, Tuple, Dict, Any
from config import Game
logger = logging.getLogger(__name__)
@@ -156,9 +158,7 @@ class GameFilters:
r'\([^\)]*PRERELEASE[^\)]*\)',
r'\([^\)]*UNFINISHED[^\)]*\)',
r'\([^\)]*WIP[^\)]*\)',
r'\[[^\]]*BETA[^\]]*\]',
r'\[[^\]]*DEMO[^\]]*\]',
r'\[[^\]]*TEST[^\]]*\]'
r'\([^\)]*BOOTLEG[^\)]*\)',
]
return any(re.search(pattern, name) for pattern in non_release_patterns)
@@ -211,7 +211,7 @@ class GameFilters:
return best_priority
def apply_filters(self, games: List[Tuple]) -> List[Tuple]:
def apply_filters(self, games: list[Game]) -> list[Game]:
"""
Applique les filtres à une liste de jeux
games: Liste de tuples (game_name, game_url, size)
@@ -224,7 +224,7 @@ class GameFilters:
# Filtrage par région
for game in games:
game_name = game[0]
game_name = game.display_name
# Vérifier les filtres de région
if self.region_filters:
@@ -255,12 +255,12 @@ class GameFilters:
return filtered_games
def _apply_one_rom_per_game(self, games: List[Tuple]) -> List[Tuple]:
def _apply_one_rom_per_game(self, games: List[Game]) -> List[Game]:
"""Garde seulement une ROM par jeu selon la priorité de région"""
games_by_base = {}
for game in games:
game_name = game[0]
game_name = game.display_name
base_name = self.get_base_game_name(game_name)
if base_name not in games_by_base:
@@ -276,7 +276,7 @@ class GameFilters:
else:
# Trier par priorité de région
sorted_games = sorted(game_list,
key=lambda g: self.get_region_priority(g[0]))
key=lambda g: self.get_region_priority(g.display_name))
result.append(sorted_games[0])
return result

View File

@@ -17,7 +17,7 @@ import re
import config # paths, settings, SAVE_FOLDER, etc.
from utils import load_api_keys as _prime_api_keys # ensure API key files are created
import network as network_mod # for progress_queues access
from utils import load_sources, load_games, is_extension_supported, load_extensions_json, sanitize_filename, extract_zip_data
from utils import load_sources, load_games, is_extension_supported, load_extensions_json, sanitize_filename
from history import load_history, save_history, add_to_history
from network import download_rom, download_from_1fichier, is_1fichier_url
from rgsx_settings import get_sources_zip_url
@@ -277,7 +277,7 @@ def cmd_games(args):
suggestions = [] # (priority, score, game_obj)
# 1) Substring match (full or sans extension) priority 0, score = position
for g in games:
title = g[0] if isinstance(g, (list, tuple)) and g else None
title = g.name
if not title:
continue
t_lower = title.lower()
@@ -303,7 +303,7 @@ def cmd_games(args):
# 2) Ordered non-contiguous tokens (priority 1)
if q_tokens:
for g in games:
title = g[0] if isinstance(g, (list, tuple)) and g else None
title = g.name
if not title:
continue
tt = _tokens(title)
@@ -313,7 +313,7 @@ def cmd_games(args):
# 3) All tokens present, any order (priority 2), score = token set size
if q_tokens:
for g in games:
title = g[0] if isinstance(g, (list, tuple)) and g else None
title = g.name
if not title:
continue
t_tokens = set(_tokens(title))
@@ -322,12 +322,12 @@ def cmd_games(args):
# Deduplicate by title keeping best (lowest priority, then score)
best = {}
for prio, score, g in suggestions:
title = g[0] if isinstance(g, (list, tuple)) and g else str(g)
title = g.name
key = title.lower()
cur = best.get(key)
if cur is None or (prio, score) < (cur[0], cur[1]):
best[key] = (prio, score, g)
ranked = sorted(best.values(), key=lambda x: (x[0], x[1], (x[2][0] if isinstance(x[2], (list, tuple)) and x[2] else str(x[2])).lower()))
ranked = sorted(best.values(), key=lambda x: (x[0], x[1], (x[2].name if isinstance(x[2], (list, config.Game)) and x[2] else str(x[2])).lower()))
games = [g for _, _, g in ranked]
# Table: Name (60) | Size (12) to allow "xxxx.xx MiB"
NAME_W = 60
@@ -344,7 +344,7 @@ def cmd_games(args):
print(header)
print(border)
for g in games:
title = g[0] if isinstance(g, (list, tuple)) and g else str(g)
title = g.name
size_val = ''
if isinstance(g, (list, tuple)) and len(g) >= 3:
size_val = display_size(g[2])
@@ -447,11 +447,11 @@ def cmd_download(args):
def _tokens(s: str) -> list[str]:
return re.findall(r"[a-z0-9]+", s.lower())
def _game_title(g) -> str | None:
return g[0] if isinstance(g, (list, tuple)) and g else None
def _game_title(g: config.Game) -> str | None:
return g.name
def _game_url(g) -> str | None:
return g[1] if isinstance(g, (list, tuple)) and len(g) > 1 else None
def _game_url(g: config.Game) -> str | None:
return g.url
# 1) Exact match (case-insensitive), with and without extension
match = None
@@ -561,8 +561,8 @@ def cmd_download(args):
size_val = ''
size_raw = None
for g in games:
if isinstance(g, (list, tuple)) and g and g[0] == title and len(g) >= 3:
size_raw = g[2]
if g.name == title:
size_raw = g.size
break
if size_raw is not None:
size_val = display_size(size_raw)

View File

@@ -25,6 +25,7 @@ from utils import load_sources, load_games, extract_data
from network import download_rom, download_from_1fichier
from pathlib import Path
from rgsx_settings import get_language
from config import Game
try:
from watchdog.observers import Observer # type: ignore
@@ -161,7 +162,7 @@ def get_cached_sources() -> tuple[list[dict], str, datetime]:
return copy.deepcopy(platforms), etag, last_modified
def get_cached_games(platform: str) -> tuple[list[tuple], str, datetime]:
def get_cached_games(platform: str) -> tuple[list[Game], str, datetime]:
"""Return cached games list for platform with metadata."""
now = time.time()
with cache_lock:
@@ -696,14 +697,14 @@ class RGSXHandler(BaseHTTPRequestHandler):
elif games_last_modified:
latest_modified = games_last_modified
for game in games:
game_name = game[0] if isinstance(game, (list, tuple)) else str(game)
game_name = game.name
game_name_lower = game_name.lower()
if all(word in game_name_lower for word in search_words):
matching_games.append({
'game_name': game_name,
'platform': platform_name,
'url': game[1] if len(game) > 1 and isinstance(game, (list, tuple)) else None,
'size': normalize_size(game[2] if len(game) > 2 and isinstance(game, (list, tuple)) else None, self._get_language_from_cookies())
'url': game.url,
'size': normalize_size(game.size, self._get_language_from_cookies())
})
except Exception as e:
logger.debug(f"Erreur lors de la recherche dans {platform_name}: {e}")
@@ -750,9 +751,9 @@ class RGSXHandler(BaseHTTPRequestHandler):
games, _, games_last_modified = get_cached_games(platform_name)
games_formatted = [
{
'name': g[0],
'url': g[1] if len(g) > 1 else None,
'size': normalize_size(g[2] if len(g) > 2 else None, lang)
'name': g.name,
'url': g.url,
'size': normalize_size(g.size, lang)
}
for g in games
]
@@ -1134,7 +1135,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
if game_name_param and game_index is None:
game_index = None
for idx, game in enumerate(games):
current_game_name = game[0] if isinstance(game, (list, tuple)) else str(game)
current_game_name = game.name
if current_game_name == game_name_param:
game_index = idx
break
@@ -1155,8 +1156,8 @@ class RGSXHandler(BaseHTTPRequestHandler):
return
game = games[game_index]
game_name = game[0]
game_url = game[1] if len(game) > 1 else None
game_name = game.name
game_url = game.url
if not game_url:
self._send_json({

View File

@@ -1,3 +1,4 @@
from pathlib import Path
import shutil
import requests # type: ignore
import re
@@ -7,7 +8,7 @@ import logging
import platform
import subprocess
import config
from config import HEADLESS
from config import HEADLESS, Game
try:
if not HEADLESS:
import pygame # type: ignore
@@ -1166,7 +1167,7 @@ def load_sources():
logger.error(f"Erreur fusion systèmes + détection jeux: {e}")
return []
def load_games(platform_id):
def load_games(platform_id:str) -> list[Game]:
try:
# Retrouver l'objet plateforme pour accéder éventuellement à 'folder'
platform_dict = None
@@ -1235,7 +1236,13 @@ def load_games(platform_id):
if getattr(config, "games_count_log_verbose", False):
logger.debug(f"{os.path.basename(game_file)}: {len(normalized)} jeux")
return normalized
games_list: list[Game] = []
for name, url, size in normalized:
display_name = Path(name).stem
display_name = display_name.replace(platform_id, "")
games_list.append(Game(name=name, url=url, size=size, display_name=display_name))
return games_list
except Exception as e:
logger.error(f"Erreur lors du chargement des jeux pour {platform_id}: {e}")
return []

View File

@@ -1,3 +1,3 @@
{
"version": "2.5.0.4"
"version": "2.5.0.5"
}