igir
Version:
🕹 A zero-setup ROM collection manager that sorts, filters, extracts or archives, patches, and reports on collections of any size on any OS.
483 lines (422 loc) • 29.7 kB
Markdown
<div align="center">
<img src="static/logo-dark.svg" height="200px" width="auto" alt="igir logo">
</div>
<br>
<p align="center"><b>Pronounced "eager," Igir is a zero-setup ROM collection manager that sorts, filters, extracts or archives, patches, and reports on collections of any size on any OS.</b></p>
<p align="center">
<a href="https://www.npmjs.com/package/igir"><img alt="npm: version" src="https://img.shields.io/npm/v/igir?color=%23cc3534&label=version&logo=npm&logoColor=white"></a>
<a href="https://www.npmjs.com/package/igir"><img alt="npm: downloads" src="https://img.shields.io/npm/dt/igir?color=%23cc3534&logo=npm&logoColor=white"></a>
<a href="https://github.com/emmercm/igir/releases"><img alt="GitHub: downloads" src="https://img.shields.io/github/downloads/emmercm/igir/total?color=%236e5494&logo=github&logoColor=white"></a>
<a href="https://github.com/emmercm/igir"><img alt="GitHub: stars" src="https://img.shields.io/github/stars/emmercm/igir?style=flat&logo=github&logoColor=white&color=%236e5494"></a>
<a href="https://github.com/emmercm/igir/discussions"><img alt="GitHub: discussions" src="https://img.shields.io/github/discussions/emmercm/igir?logo=github&logoColor=white"></a>
</p>
<p align="center">
<a href="https://snyk.io/test/npm/igir"><img alt="Snyk: vulnerabilities" src="https://snyk.io/test/npm/igir/badge.svg"></a>
<a href="https://codecov.io/gh/emmercm/igir"><img alt="codecov: coverage" src="https://img.shields.io/codecov/c/github/emmercm/igir?logo=codecov&logoColor=white"></a>
<a href="https://qlty.sh/gh/emmercm/projects/igir"><img src="https://qlty.sh/gh/emmercm/projects/igir/maintainability.svg" alt="Maintainability" /></a>
<a href="https://github.com/emmercm/igir/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/github/license/emmercm/igir?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NDAgNTEyIj48IS0tIUZvbnQgQXdlc29tZSBGcmVlIDYuNS4yIGJ5IEBmb250YXdlc29tZSAtIGh0dHBzOi8vZm9udGF3ZXNvbWUuY29tIExpY2Vuc2UgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbS9saWNlbnNlL2ZyZWUgQ29weXJpZ2h0IDIwMjQgRm9udGljb25zLCBJbmMuLS0%2BPHBhdGggZD0iTTM4NCAzMkg1MTJjMTcuNyAwIDMyIDE0LjMgMzIgMzJzLTE0LjMgMzItMzIgMzJIMzk4LjRjLTUuMiAyNS44LTIyLjkgNDcuMS00Ni40IDU3LjNWNDQ4SDUxMmMxNy43IDAgMzIgMTQuMyAzMiAzMnMtMTQuMyAzMi0zMiAzMkgzMjAgMTI4Yy0xNy43IDAtMzItMTQuMy0zMi0zMnMxNC4zLTMyIDMyLTMySDI4OFYxNTMuM2MtMjMuNS0xMC4zLTQxLjItMzEuNi00Ni40LTU3LjNIMTI4Yy0xNy43IDAtMzItMTQuMy0zMi0zMnMxNC4zLTMyIDMyLTMySDI1NmMxNC42LTE5LjQgMzcuOC0zMiA2NC0zMnM0OS40IDEyLjYgNjQgMzJ6bTU1LjYgMjg4SDU4NC40TDUxMiAxOTUuOCA0MzkuNiAzMjB6TTUxMiA0MTZjLTYyLjkgMC0xMTUuMi0zNC0xMjYtNzguOWMtMi42LTExIDEtMjIuMyA2LjctMzIuMWw5NS4yLTE2My4yYzUtOC42IDE0LjItMTMuOCAyNC4xLTEzLjhzMTkuMSA1LjMgMjQuMSAxMy44bDk1LjIgMTYzLjJjNS43IDkuOCA5LjMgMjEuMSA2LjcgMzIuMUM2MjcuMiAzODIgNTc0LjkgNDE2IDUxMiA0MTZ6TTEyNi44IDE5NS44TDU0LjQgMzIwSDE5OS4zTDEyNi44IDE5NS44ek0uOSAzMzcuMWMtMi42LTExIDEtMjIuMyA2LjctMzIuMWw5NS4yLTE2My4yYzUtOC42IDE0LjItMTMuOCAyNC4xLTEzLjhzMTkuMSA1LjMgMjQuMSAxMy44bDk1LjIgMTYzLjJjNS43IDkuOCA5LjMgMjEuMSA2LjcgMzIuMUMyNDIgMzgyIDE4OS43IDQxNiAxMjYuOCA0MTZTMTEuNyAzODIgLjkgMzM3LjF6IiBzdHlsZT0iZmlsbDojZmZmZmZmIiAvPjwvc3ZnPg%3D%3D&color=blue"></a>
</p>
<p align="center"><i>See the <a href="https://igir.io/">project website</a> for complete documentation, installation & usage instructions, and examples!</i></p>
<br>
## What does Igir do?
A video of an example use case:
<a href="https://asciinema.org/a/xE6kMquCPFYtpvhsiQfS0eS7c" target="_blank"><img src="https://asciinema.org/a/xE6kMquCPFYtpvhsiQfS0eS7c.svg" alt="asciicast" style="width:100%" /></a>
With Igir you can manage a ROM collection of any size:
- 🔍 Scan for DATs, ROMs, and ROM patches—including those in archives (see [scanning](https://igir.io/input/file-scanning) & [archive docs](https://igir.io/input/reading-archives))
- 📂 Organize ROM files by console (see [DAT docs](https://igir.io/dats/overview))
- 🪄 Name ROM files consistently, including the right extension (see [DAT docs](https://igir.io/dats/overview))
- ✂️ Filter out duplicate ROMs, or ROMs in languages you don't understand (see [filtering docs](https://igir.io/roms/filtering-preferences))
- 🗜️ Extract or archive ROMs in mass (see [archive docs](https://igir.io/output/writing-archives))
- 🔢 Create playlists for multi-disc games in mass (see [playlist docs](https://igir.io/output/playlists))
- 🩹 Patch ROMs automatically in mass (see [scanning](https://igir.io/input/file-scanning) & [patching docs](https://igir.io/roms/patching))
- 🎩 Parse ROMs with headers and optionally remove them (see [header docs](https://igir.io/roms/headers))
- ↔️ Build & re-build (un-merge, split, or merge) MAME ROM sets (see [arcade docs](https://igir.io/usage/arcade))
- 🔮 Report on what ROMs are present or missing for each console and create fixdats for missing ROMs (see [reporting](https://igir.io/output/reporting) & [DAT docs](https://igir.io/dats/overview))
## How do I run Igir?
Either download the latest version for your OS from the [releases page](https://github.com/emmercm/igir/releases/latest), or if you have Node.js installed you can use [`npx`](https://docs.npmjs.com/cli/v9/commands/npx) to always run the latest version from the command line:
```shell
npx igir@latest [commands..] [options]
```
Here is the full help message which shows all available options and a number of common use case examples:
<!-- WARN: everything below is automatically updated! Update src/modules/argumentsParser.ts instead! -->
```help
$ igir --help
@@@@@@ @@@@@@ @@@@@@ @@@@@@@@
@@ @@ @@ @@ @@ @@
@@ @@ @@ @@ @@ @@ ROM collection manager
@@@@@@ @@ @@@@@@ @@@@@@@@@ https://igir.io/
@@@ @@@@ @@@
@@ @@ @@ @@ @@ @@ v5.4.0 (Node.js v26.4.0)
@@ @@ @@ @@ @@ @@
@@ @@@@@@@@@@@@ @@ @@ @@
Usage: igir [commands..] [options]
Commands (can specify multiple):
igir copy Copy ROM files from the input to output directory
igir move Move ROM files from the input to output directory
igir link Create links in the output directory to ROM files in
the input directory
igir extract Extract ROM files in archives when copying or moving
igir zip Create zip archives of ROMs when copying or moving
igir playlist Create playlist files for multi-disc games
igir test Test ROMs for accuracy after writing them to the output
directory
igir dir2dat Generate a DAT from all input files
igir fixdat Generate a fixdat of any missing games for every DAT
processed (requires --dat)
igir clean Recycle unknown files in the output directory
igir report Generate a CSV report on the known & unknown ROM files
found in the input directories (requires --dat)
ROM input options:
-i, --input Path(s) to ROM files or archives
(supports globbing) [array]
-I, --input-exclude Path(s) to ROM files or archives to
exclude from processing (supports
globbing) [array]
--input-checksum-quick Only read checksums from archive
headers, don't decompress to calculate
[boolean]
--input-checksum-min The minimum checksum level to calculate
and use for matching
[choices: "CRC32", "MD5", "SHA1", "SHA256"] [default: "CRC32"]
--input-checksum-max The maximum checksum level to calculate
and use for matching
[choices: "CRC32", "MD5", "SHA1", "SHA256"] [default: "SHA1"]
--input-checksum-archives Calculate checksums of archive files
themselves, allowing them to match
files in DATs
[choices: "never", "auto", "always"] [default: "auto"]
DAT input options:
-d, --dat Path(s) to DAT files or archives
(supports globbing) [array]
--dat-exclude Path(s) to DAT files or archives
to exclude from processing
(supports globbing) [array]
--dat-name-regex Regular expression of DAT names
to process [array]
--dat-name-regex-exclude Regular expression of DAT names
to exclude from processing[array]
--dat-description-regex Regular expression of DAT
descriptions to process [array]
--dat-description-regex-exclude Regular expression of DAT
descriptions to exclude from
processing [array]
--dat-combine Combine every game from every
found & filtered DAT into one DAT
[boolean]
--dat-ignore-parent-clone Ignore any parent/clone
information found in DATs
[boolean]
Patch input options:
-p, --patch Path(s) to ROM patch files or archives (supports
globbing) (supported: .aps, .bps, .dps, .ebp,
.ips, .ips32, .ppf, .rup, .ups, .vcdiff, .xdelta)
[array]
-P, --patch-exclude Path(s) to ROM patch files or archives to exclude
from processing (supports globbing) [array]
--patch-only Only write patched ROMs to the output directory
[boolean]
ROM output path options (processed in order):
-o, --output Path to the ROM output directory
(supports replaceable symbols, see below)
[string]
--dir-mirror Use the input subdirectory structure for
the output directory [boolean]
--dir-dat-mirror Use the DAT subdirectory structure for
the output directory [boolean]
-D, --dir-dat-name Use the DAT name as the output
subdirectory [boolean]
--dir-dat-description Use the DAT description as the output
subdirectory [boolean]
--dir-letter Group games in an output subdirectory by
the first --dir-letter-count letters in
their name [boolean]
--dir-letter-count How many game name letters to use for the
subdirectory name [number] [default: 1]
--dir-letter-limit Limit the number of games in letter
subdirectories, splitting into multiple
subdirectories if necessary [number]
--dir-letter-group Group letter subdirectories into ranges,
combining multiple letters together
(requires --dir-letter-limit) [boolean]
--dir-game-subdir Append the name of the game as an output
subdirectory depending on how many ROMs a
game has
[choices: "never", "multiple", "always"] [default: "multiple"]
--output-console-tokens Path to a JSON file of custom console
token definitions to use instead of the
built-in definitions [string]
ROM writing options:
--fix-extension Read files for known signatures and use the
correct extension (also affects dir2dat)
[choices: "never", "auto", "always"] [default: "auto"]
-O, --overwrite Overwrite any files in the output directory
[boolean]
--overwrite-invalid Overwrite files in the output directory that
are the wrong filesize, checksum, or zip
contents [boolean]
--write-retry Number of additional retries to attempt when
writing a file has failed (0 disables
retries) [number] [default: 2]
move command options:
--move-delete-dirs Delete empty subdirectories from the input
directories after moving ROMs
[choices: "never", "auto", "always"] [default: "auto"]
clean command options:
-C, --clean-exclude Path(s) to files to exclude from cleaning
(supports globbing) [array]
--clean-backup Directory to move cleaned files to (instead of
being recycled) [string]
--clean-dry-run Don't clean any files and instead only print what
files would be cleaned [boolean]
zip command options:
--zip-format The structure format to use for written zip files
[choices: "torrentzip", "rvzstd"] [default: "torrentzip"]
-Z, --zip-exclude Glob pattern of ROM filenames to exclude from
zipping [string]
--zip-dat-name Group all ROMs from the same DAT into the same zip
archive, if not excluded from zipping (enforces
--dat-threads 1) [boolean]
link command options:
--link-mode File linking mode
[choices: "hardlink", "symlink", "reflink"] [default: "hardlink"]
--symlink-relative Create symlinks as relative to the target
path, as opposed to absolute [boolean]
ROM header options:
--header Glob pattern of input filenames to force header
detection for [string]
-H, --remove-headers Remove known headers from ROMs, optionally
limited to a list of comma-separated file
extensions (supported: .a78, .fds, .lnx, .nes,
.smc) [string]
Trimmed ROM options:
--trimmed-glob Glob pattern of input filenames to force
trimming detection for (overriding all
options below) [string]
--trim-scan-files Detect trimming for uncompressed files
[choices: "never", "auto", "always"] [default: "auto"]
--trim-scan-archives Detect trimming for files within archives
(off by default) [boolean]
--trim-add-padding When writing matched trimmed ROMs, add back
their trailing padding so the output matches
the DAT checksum [boolean]
ROM set options (requires DATs):
--merge-roms ROM merge/split mode (requires DATs with
parent/clone information)
[choices: "fullnonmerged", "nonmerged", "split", "merged"] [default:
"fullnonmerged"]
--merge-discs Merge multi-disc games into one game
[boolean]
--exclude-disks Exclude CHD disks in DATs from processing
& writing [boolean]
--allow-excess-sets Allow writing archives that have excess
files when not extracting or zipping
[boolean]
--allow-incomplete-sets Allow writing games that don't have all
of their ROMs [boolean]
ROM filtering options:
-x, --filter-regex Regular expression of game names to
filter to [array]
-X, --filter-regex-exclude Regular expression of game names to
exclude [array]
-L, --filter-language List of comma-separated languages to
filter to (supported: DA, DE, EL, EN, ES,
FI, FR, IT, JA, KO, NL, NO, PT, RU, SV,
ZH) [string]
-R, --filter-region List of comma-separated regions to filter
to (supported: ARG, ASI, AUS, BEL, BRA,
CAN, CHN, DAN, EUR, FRA, FYN, GER, GRE,
HK, HOL, ITA, JPN, KOR, MEX, NOR, NZ,
POR, RUS, SPA, SWE, TAI, UK, UNK, USA,
WORLD) [string]
--filter-category-regex Regular expression of categories to
filter to [array]
--no-bios Filter out BIOS files, opposite of
--only-bios [boolean]
--no-device Filter out MAME devies, opposite of
--only-device [boolean]
--no-unlicensed Filter out unlicensed ROMs, opposite of
--only-unlicensed [boolean]
--only-retail Filter to only retail releases, enabling
all the following "no" options [boolean]
--no-debug Filter out debug ROMs, opposite of
--only-debug [boolean]
--no-demo Filter out demo ROMs, opposite of
--only-demo [boolean]
--no-beta Filter out beta ROMs, opposite of
--only-beta [boolean]
--no-sample Filter out sample ROMs, opposite of
--only-sample [boolean]
--no-prototype Filter out prototype ROMs, opposite of
--only-prototype [boolean]
--no-program Filter out program application ROMs,
opposite of --only-program [boolean]
--no-aftermarket Filter out aftermarket ROMs, opposite of
--only-aftermarket [boolean]
--no-homebrew Filter out homebrew ROMs, opposite of
--only-homebrew [boolean]
--no-unverified Filter out unverified ROMs, opposite of
--only-unverified [boolean]
--no-bad Filter out bad ROM dumps, opposite of
--only-bad [boolean]
One game, one ROM (1G1R) options (applied in order):
-s, --single Output only a single game per parent (1G1R)
(required for all options below, requires
DATs with parent/clone information) [boolean]
--prefer-game-regex Regular expression of DAT game names to
prefer [array]
--prefer-rom-regex Regular expression of DAT ROM filenames to
prefer [array]
--prefer-verified Prefer verified ROM dumps over unverified
[boolean]
--prefer-good Prefer good ROM dumps over bad [boolean]
-l, --prefer-language List of comma-separated languages in priority
order (supported: DA, DE, EL, EN, ES, FI, FR,
IT, JA, KO, NL, NO, PT, RU, SV, ZH) [string]
-r, --prefer-region List of comma-separated regions in priority
order (supported: ARG, ASI, AUS, BEL, BRA,
CAN, CHN, DAN, EUR, FRA, FYN, GER, GRE, HK,
HOL, ITA, JPN, KOR, MEX, NOR, NZ, POR, RUS,
SPA, SWE, TAI, UK, UNK, USA, WORLD) [string]
--prefer-revision Prefer older or newer revisions, versions, or
ring codes [choices: "older", "newer"]
--prefer-retail Prefer retail releases (see --only-retail)
[boolean]
--prefer-parent Prefer parent ROMs over clones [boolean]
Input file preferences (applied in order):
--prefer-filetype Prefer input files of a type
[choices: "plain", "archive"] [default: "plain"]
--prefer-filename-regex Regular expression of filenames to prefer
[array]
playlist command options:
--playlist-mode Generate playlists depending on how many
ROMs a game has
[choices: "multiple", "always"] [default: "multiple"]
--playlist-extensions List of comma-separated file extensions to
generate multi-disc playlists for
[string] [default: ".ccd,.cdi,.chd,.cue,.gdi,.iso,.mdf,.toc"]
dir2dat command options:
--dir2dat-output dir2dat output directory [string]
fixdat command options:
--fixdat-output Fixdat output directory [string]
report command options:
--report-output Report output file location (formatted with
Moment.js date/time tokens)
[string] [default: "igir_%YYYY-%MM-%DDT%HH:%mm:%ss.csv"]
Concurrency options:
--dat-threads Number of DATs to process in parallel
[number] [default: 2]
--reader-threads Maximum number of ROMs to read in parallel per
disk [number] [default: 6]
--writer-threads Maximum number of ROMs to write in parallel
[number] [default: 3]
Cache & directory options:
--temp-dir Path to a directory for temporary files [string]
--disable-cache Disable loading or saving the cache file[boolean]
--cache-path Location for the cache file [string]
Help & debug options:
-v, --verbose Enable verbose logging, can specify up to three times
(-vvv) [count]
--debug-log Generate a debug log file to attach to bug reports
[string]
-h, --help Show help [boolean]
------------------------------------------------------------------------
Advanced usage:
Tokens that are replaced when generating the output (--output) path of
a ROM:
{datName} The name of the DAT that contains the ROM (e.g.
"Nintendo - Game Boy")
{datDescription} The description of the DAT that contains the ROM
{region} The region of the ROM release (e.g. "USA"), each
ROM can have multiple
{language} The language of the ROM release (e.g. "En"), each
ROM can have multiple
{type} The type of the game (e.g. "Retail", "Demo",
"Prototype")
{category} The DAT-defined category of the game (e.g.
"Games", "Demos", "Multimedia")
{genre} The DAT-defined genre of the game
{inputDirname} The input file's dirname
{outputBasename} Equivalent to "{outputName}.{outputExt}"
{outputName} The output file's filename without extension
{outputExt} The output file's extension
{adam} The ROM's emulator-specific /ROMS/* directory for the
'Adam' image (e.g. "GB")
{batocera} The ROM's emulator-specific /roms/* directory for
Batocera (e.g. "gb")
{crossmix} The ROM's emulator-specific /Roms/* directory for
CrossMix-OS (e.g. "GB")
{es} The ROM's emulator-specific /roms/* directory for the
'EmulationStation' image (e.g. "gb")
{funkeyos} The ROM's emulator-specific /* directory for FunKey OS
(e.g. "Game Boy")
{minui} The ROM's emulator-specific /Roms/* directory for MinUI
(e.g. "Game Boy (GB)")
{mister} The ROM's core-specific /games/* directory for the
MiSTer FPGA (e.g. "Gameboy")
{miyoocfw} The ROM's emulator-specific /roms/* directory for
MiyooCFW (e.g. "GB")
{onion} The ROM's emulator-specific /Roms/* directory for
OnionOS/GarlicOS (e.g. "GB")
{pocket} The ROM's core-specific /Assets/* directory for the
Analogue Pocket (e.g. "gb")
{retrodeck} The ROM's emulator-specific /roms/* directory for the
'RetroDECK' image (e.g. "gb")
{rocknix} The ROM's emulator-specific /roms/* directory for
ROCKNIX (e.g. "gb")
{romm} The ROM's manager-specific /roms/* directory for 'RomM'
(e.g. "gb")
{spruce} The ROM's emulator-specific /Roms/* directory for
SpruceOS (e.g. "GB")
{twmenu} The ROM's emulator-specific /roms/* directory for
TWiLightMenu++ on the DSi/3DS (e.g. "gb")
Example use cases:
Merge new ROMs into an existing ROM collection and delete any
unrecognized files:
igir copy clean --dat "*.dat" --input New-ROMs --input ROMs --output
ROMs
Organize and zip an existing ROM collection:
igir move zip --dat "*.dat" --input ROMs --output ROMs
Generate a report on an existing ROM collection, without copying or
moving ROMs (read only):
igir report --dat "*.dat" --input ROMs
Produce a 1G1R set per console, preferring English ROMs from
USA>WORLD>EUR>JPN:
igir copy --dat "*.dat" --input "**/*.zip" --output 1G1R
--dir-dat-name --single --prefer-language EN --prefer-region
USA,WORLD,EUR,JPN
Copy all Mario, Metroid, and Zelda games to one directory:
igir copy --input ROMs --output Nintendo --filter-regex
"/(Mario|Metroid|Zelda)/i"
Copy all BIOS files into one directory, extracting if necessary:
igir copy extract --dat "*.dat" --input "**/*.zip" --output BIOS
--only-bios
Create playlist files for all multi-disc games in an existing
collection:
igir playlist --input ROMs
Create patched copies of ROMs in an existing collection, not
overwriting existing files:
igir copy extract --input ROMs --patch Patches --output ROMs
Re-build a MAME ROM set for a specific version of MAME:
igir copy zip --dat "MAME 0.258.dat" --input MAME --output
MAME-0.258 --merge-roms split
Copy ROMs to an Analogue Pocket and test they were written correctly:
igir copy extract test --dat "*.dat" --input ROMs --output
/Assets/{pocket}/common --dir-letter
```
<!-- WARN: everything above is automatically updated! Update src/modules/argumentsParser.ts instead! -->
## Feature requests, bug reports, and contributing
Feedback is a gift! Your feature requests and bug reports help improve the project for everyone. Feel free to [submit an issue](https://github.com/emmercm/igir/issues/new/choose) on GitHub using one of the templates.
Even better, if you feel comfortable writing code, please feel free to submit a pull request against the project! Please see the full [contribution guidelines](https://igir.io/contributing) for rules to follow.
<br>
<p align="center">
<a href="https://github.com/emmercm/igir/graphs/contributors"><img alt="GitHub: contributors" src="https://img.shields.io/github/contributors/emmercm/igir?logo=github&logoColor=white"></a>
<a href="https://github.com/emmercm/igir/issues?q=is%3Aopen+is%3Aissue+label%3Abug"><img alt="GitHub: bugs" src="https://img.shields.io/github/issues/emmercm/igir/bug?color=%23d73a4a&label=bugs&logo=github&logoColor=white"></a>
<a href="https://github.com/emmercm/igir/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement"><img alt="GitHub: feature requests" src="https://img.shields.io/github/issues/emmercm/igir/enhancement?color=%234BBCBC&label=feature%20requests&logo=github&logoColor=white"></a>
<a href="https://github.com/emmercm/igir/discussions"><img alt="GitHub: discussions" src="https://img.shields.io/github/discussions/emmercm/igir?logo=github&logoColor=white"></a>
<a href="https://hacktoberfest.com/"><img alt="Hacktoberfest: participant" src="https://img.shields.io/badge/hacktoberfest-participant-orange?logo=digitalocean&logoColor=white"></a>
</p>
<p align="center">
<a href="https://github.com/emmercm/igir/graphs/contributors">
<img src="https://contrib.rocks/image?repo=emmercm/igir" alt="List of contributors" />
</a>
</p>