initial commit

This commit is contained in:
Brennen Raimer
2026-08-26 19:27:08 -04:00
commit fb78292b63
6 changed files with 134 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
+1
View File
@@ -0,0 +1 @@
3.14
+20
View File
@@ -0,0 +1,20 @@
# ES Multi-Disc Game Organizer
Organizes multi-disc game files in an ES ROMs system directory so that ES will treat them as a single game with many discs rather than many single-disc games
## Usage
### Pre-requisites
* [uv](https://docs.astral.sh/uv/)
### Instructions
1. Clone this repository or download and unzip it
2. Open a terminal change to the directory you just cloned or unzipped
3. Run `uv run es-multi-disc-game-organizer /path/to/ROMs/system` replacing the path with the full path to a system folder in your ES-DE ROMs folder containing multi-disc games
### Important Caveats
* This tool supports .iso, .zip, .chd, and .cso ROM files (sorry, bin/cue is just not easy to work with, and you can easily convert those to .chd using [chdman](https://wiki.recalbox.com/en/tutorials/utilities/rom-conversion/chdman))
* This tool assumes that your ROM files are named in the format "Game Name (disc 1).ext" or "Game Name (disk 1 of 2).ext". Many ROMs will come with a lot of superfluous information (region, software version, disk pressing, the name of the person who ripped the disc, etc.). There's just far too many naming conventions out there for me to even begin to try to support them all.
+19
View File
@@ -0,0 +1,19 @@
[project]
name = "es-multi-disc-game-organizer"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
{ name = "Brennen Raimer", email = "5969754+norweeg@users.noreply.github.com" }
]
requires-python = ">=3.14"
dependencies = [
"toolz>=1.1.0",
]
[project.scripts]
es-multi-disc-game-organizer = "es_multi_disc_game_organizer:main"
[build-system]
requires = ["uv_build>=0.12.5,<0.13.0"]
build-backend = "uv_build"
@@ -0,0 +1,61 @@
from argparse import ArgumentParser
from operator import methodcaller, attrgetter
from pathlib import Path
from shutil import move
from re import compile, IGNORECASE
from toolz import compose_left, first, groupby
from toolz.curried.operator import contains
# Extensions typically used for rom files of disc-based games. This is not an exhaustive list, but it should cover most cases.
ROM_FILE_EXTS = (".cso", ".chd", ".zip", ".iso")
# This regex expects ROM files named in the format "Game Name (disc 1).ext" or "Game Name (disk 1 of 2).ext". The regex is case-insensitive.
# The game name is captured in group 1, which is used to create the playlist and subdirectory and m3u playlist names.
DISC_REGEX = compile(r"(.+) \(dis[ck] \d+( of \d+)?\)$", IGNORECASE)
# filter predicates for identifying rom files
is_romfile = compose_left(
attrgetter("suffix"), methodcaller("lower"), contains(ROM_FILE_EXTS)
)
# filter predicate for identifying multi-disc games
is_multidisc = compose_left(attrgetter("stem"), DISC_REGEX.match)
def organize_multidisc_games(folder: Path) -> None:
# Get all the rom files in the folder, filter for multi-disc games, and group them by game name
rom_files = filter(is_romfile, folder.glob("*"))
multi_disc_games = filter(is_multidisc, rom_files)
games = groupby(
compose_left(attrgetter("stem"), DISC_REGEX.match, methodcaller("group", 1)),
multi_disc_games,
)
# Create a playlist for each multi-disc game and move the disc files into a subdirectory
for game, discs in games.items():
first_file = first(discs)
playlist_path = first_file.parent / f"{game}.m3u"
discs_path = first_file.parent / game
discs_path.mkdir(exist_ok=True)
# noload.txt tells ES to ignore this directory for scanning, so we don't get duplicate entries in the gamelist.xml
(discs_path / "noload.txt").touch(exist_ok=True)
with open(playlist_path, "w") as playlist:
for file in sorted(discs, key=attrgetter("name")):
playlist.write(f"{game}/{file.name}\n")
move(file, discs_path / file.name)
def main() -> None:
parser = ArgumentParser(description="create playlists for multdisc games")
parser.add_argument("folder", type=Path)
args = parser.parse_args()
organize_multidisc_games(args.folder)
if __name__ == "__main__":
main()
Generated
+23
View File
@@ -0,0 +1,23 @@
version = 1
revision = 3
requires-python = ">=3.14"
[[package]]
name = "es-multi-disc-game-organizer"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "toolz" },
]
[package.metadata]
requires-dist = [{ name = "toolz", specifier = ">=1.1.0" }]
[[package]]
name = "toolz"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" },
]