Skip to content

Instantly share code, notes, and snippets.

@i30817
Last active April 21, 2026 17:19
Show Gist options
  • Select an option

  • Save i30817/ed0ba4e7ee3108e3d850ba74e9e998e6 to your computer and use it in GitHub Desktop.

Select an option

Save i30817/ed0ba4e7ee3108e3d850ba74e9e998e6 to your computer and use it in GitHub Desktop.
chd converter of redump dumps (dats from https://github.com/MetalSlug/MAMERedump). Hacks will not be recognized, if you place them in a subdir you can use --skip '*/Hacks/*' to skip a sub-directory if a tool has otherwise a method to get hack names (either by a dedicated dat or another trick like rxdelta files from my other tool rhdndat-rn).
#!/bin/bash
# chd-convert.sh
#use a absolute path if you want a newer self compiled chdman
CHDMAN_PATH="chdman"
SEARCH_DIR="."
SKIP_DIRS=()
DRY_RUN=false
LOG_FILE="conversion.log"
# Counters
fail_count=0
success_count=0
total_saved_bytes=0
processed_count=0
usage() {
echo "Usage: chd-convert.sh [options] [search_directory]"
echo ""
echo "Options:"
echo " -h, --help Show help message"
echo " -s, --skip DIR Dir glob to skip (can be used multiple times)"
echo " -d, --dry-run Show what would be done without making changes"
echo ""
echo "Compression Codecs used:"
echo " CD: cdzs,cdzl,cdfl (Zstandard, Deflate, FLAC)"
echo " DVD: zstd,zlib,huff (Zstandard, Deflate, Huffman)"
exit 0
}
for cmd in "$CHDMAN_PATH" setfattr sha1sum awk sed find; do
if ! command -v "$cmd" &> /dev/null; then
echo "ERROR: Required command '$cmd' is not found."
exit 1
fi
done
# Parse Command Line Arguments
while [[ "$#" -gt 0 ]]; do
case $1 in
-h|--help) usage ;;
-d|--dry-run) DRY_RUN=true ;;
-s|--skip) SKIP_DIRS+=("$2"); shift ;;
-*) usage ;;
*) SEARCH_DIR="$1" ;;
esac
shift
done
# Initialize Log
> "$LOG_FILE"
shopt -s nullglob nocaseglob
status() {
local prefix=""
$DRY_RUN && prefix="[DRY-RUN] "
echo -e "${prefix}$1" | tee -a "$LOG_FILE"
}
human_readable() {
local bytes=$1
if [ "$bytes" -lt 1024 ]; then echo "${bytes}B"
elif [ "$bytes" -lt 1048576 ]; then echo "$((bytes/1024))KB"
elif [ "$bytes" -lt 1073741824 ]; then echo "$((bytes/1048576))MB"
else echo "$((bytes/1073741824))GB"
fi
}
convert_game() {
local source_file="$1"
local chdman_mode="$2"
local codec="$3"
local parent_dir="${source_file%/*}"
local output_chd="${source_file%.*}.chd"
# Internal check: ensure source exists before proceeding
if [ ! -f "$source_file" ]; then
status " ERROR: Source file \"$source_file\" no longer exists. Skipping."
((fail_count++))
return 1
fi
status "Processing: $source_file"
((processed_count++))
if [ -f "$output_chd" ]; then
status " Warning: Skipping, \"$output_chd\" already exists."
return
fi
local cmd_args=("-c" "$codec")
local total_source_size=$(stat -c%s "$source_file")
local tracks=()
# Unified track validation loop
if [[ "$chdman_mode" == "createcd" ]]; then
while read -r track_name; do
local track_path=$([[ "$track_name" == /* ]] && echo "$track_name" || echo "$parent_dir/$track_name")
if [ ! -f "$track_path" ]; then
status " ERROR: Required track file missing: \"$track_path\". Aborting."
((fail_count++))
return 1
fi
tracks+=("$track_path")
total_source_size=$((total_source_size + $(stat -c%s "$track_path")))
done < <(sed -n 's/^[[:space:]]*FILE[[:space:]]\+"//p' "$source_file" | sed 's/"[[:space:]]\+[A-Z]\+[[:space:]]*$//')
fi
# PSP specific hunksize check
if [[ "$chdman_mode" == "createdvd" ]] && head -c 500000 "$source_file" | grep -aqE "PSP[ _]GAME"; then
status " Note: PSP signature detected. Applying hunksize 2048."
cmd_args+=("-hs" "2048")
fi
if $DRY_RUN; then
status " Would run: $CHDMAN_PATH $chdman_mode -i \"$source_file\" -o \"$output_chd\" ${cmd_args[*]}"
((success_count++))
else
if "$CHDMAN_PATH" "$chdman_mode" -i "$source_file" -o "$output_chd" "${cmd_args[@]}"; then
local chd_size=$(stat -c%s "$output_chd")
local saved_bytes=$((total_source_size - chd_size))
local saved_percent=$(( 100 - (chd_size * 100 / total_source_size) ))
total_saved_bytes=$((total_saved_bytes + saved_bytes))
# Cleanup
for track in "${tracks[@]}"; do rm -- "$track"; done
rm -- "$source_file"
status " Success: Created $output_chd ($saved_percent% smaller, saved $(human_readable $saved_bytes))"
((success_count++))
else
status " ERROR: chdman failed for $source_file"
((fail_count++))
fi
fi
}
# Construct find prune arguments
FIND_SKIP_ARGS=()
for skip in "${SKIP_DIRS[@]}"; do
FIND_SKIP_ARGS+=(-path "$skip" -prune -o)
done
# Main Execution Loop
while IFS= read -r -d '' unique_dir; do
for f in "$unique_dir"/*.cue; do
convert_game "$f" "createcd" "cdzs,cdzl,cdfl"
done
for f in "$unique_dir"/*.iso; do
convert_game "$f" "createdvd" "zstd,zlib,huff"
done
done < <(find "$SEARCH_DIR" "${FIND_SKIP_ARGS[@]}" -type f \( -iname "*.cue" -o -iname "*.iso" \) -printf "%h\0" | sort -zu)
# Final Summary
status "-----------------------------------"
status "Finished!"
status "Successful: $success_count | Failures: $fail_count"
if ! $DRY_RUN && [ "$processed_count" -gt 0 ]; then
status "Total space saved: $(human_readable $total_saved_bytes)"
fi
[ "$processed_count" -eq 0 ] && rm "$LOG_FILE"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment