From 77843daa56eac52af468082c9ebe657429cea865 Mon Sep 17 00:00:00 2001 From: TheTechnician27 Date: Wed, 17 Jul 2024 09:37:34 -0500 Subject: [PATCH] Tools: Port refraction's bulk compression script to Python --- bin/utils/bulk_compression.py | 426 ++++++++++++++++++++++++++++++++++ 1 file changed, 426 insertions(+) create mode 100755 bin/utils/bulk_compression.py diff --git a/bin/utils/bulk_compression.py b/bin/utils/bulk_compression.py new file mode 100755 index 00000000000000..ef9d9c89a9cd80 --- /dev/null +++ b/bin/utils/bulk_compression.py @@ -0,0 +1,426 @@ +# PCSX2 - PS2 Emulator for PCs +# Copyright (C) 2024 PCSX2 Dev Team +# +# PCSX2 is free software: you can redistribute it and/or modify it under the terms +# of the GNU Lesser General Public License as published by the Free Software Found- +# ation, either version 3 of the License, or (at your option) any later version. +# +# PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with PCSX2. +# If not, see . + +import sys +import os +import re +from subprocess import Popen, PIPE +from os.path import exists + +gamecount = [0] + +# ================================================================================================= + +def deletionChoice(file_type): # Choose to delete source files + yesno = { + "n" : 0, + "no" : 0, + "y" : 1, + "yes" : 1, + } + print("║ ") + print("║ ") + print(f"║ Do you want to delete the original {file_type.upper()} files as they are converted?") + choice = input("║ Type Y or N then press ENTER: ").lower() + if (not choice in yesno): + print("║ ") + print("║ ") + print("╠===============================================================================╗") + print("║ Invalid choice. ║") + print("╚===============================================================================╝") + sys.exit(1) + return (yesno[choice]) + +# ------------------------------------------------------------------------------------------------- + +def blockSizeChoice(is_cd): # Choose block size + sizes = { + "1" : 16384, + "2" : 131072, + "3" : 262144, + } if not is_cd else { + "1": 17136, + "2": 132192, + "3": 264384, + } + print("║ ") + print("║ ") + print("║ Please pick a block size you would like to use:") + print("║ ") + print("║ 1 - 16 kB (bigger files, faster access/less CPU, choose this if unsure)") + print("║ 2 - 128 kB (balanced)") + print("║ 3 - 256 kB (smaller files, slower access/more CPU)") + print("║ ") + blocksize = input("║ Type the number corresponding to your selection then press ENTER: ") + if (not blocksize in sizes): + print("║ ") + print("╠===============================================================================╗") + print("║ Invalid block size option. ║") + print("╚===============================================================================╝") + sys.exit(1) + return (sizes[blocksize]) + +# ------------------------------------------------------------------------------------------------- + +def returnFilteredPwdContents(file_extension): # Get files in pwd with extension + extension_pattern = r".*\." + file_extension.lower() + extension_reg = re.compile(extension_pattern) + return [fname for fname in os.listdir('.') if extension_reg.match(fname)] + +# ------------------------------------------------------------------------------------------------- + +def checkSuccess(error_code, decompressing, fname, extension): # Ensure file created properly + + if (error_code): + + print("╠===============================================================================╣") + if (decompressing): + print(f"║ {extension.upper()} decompress failed.{(61 - len(extension)) * ' '}║") + print(f"║ Extracted file {fname}.{extension} missing or empty.{(26 - (len(extension) * 2) - len(fname)) * ' '}║") + else: + print(f"║ {extension.upper()} compress failed.{(61 - len(extension)) * ' '}║") + print(f"║ New file {fname}.{extension} missing or empty.{(50 - len(extension) - len(fname)) * ' '}║") + print("╚===============================================================================╝") + sys.exit(1) + + print(f"║ {fname}.{extension} successfully created.{(55 - len(fname) - len(extension)) * ' '}║") + +# ------------------------------------------------------------------------------------------------- + +def checkProgramMissing(program): + + if (sys.platform.startswith('win32') and exists(f"./{program}.exe")): + return + + else: + from shutil import which + if (which(program) is not None): + return + + print("║ ") + print("╠===============================================================================╗") + print(f"║ {program} failed, {program} file is missing.{(34 - (len(program) * 2)) * ' '}║") + print("╚===============================================================================╝") + sys.exit(1) + +# ------------------------------------------------------------------------------------------------- + +def printBinCueMismatch(): + print("║ ") + print("╠===============================================================================╗") + print("║ All BIN files must have a matching CUE. ║") + print("╚===============================================================================╝") + sys.exit(1) + +# ------------------------------------------------------------------------------------------------- + +def checkBinCueMismatch(bin_files, cue_files): # Ensure all bins and cues match + if (len(bin_files) == len(cue_files)): + for fname in bin_files: + if (f"{fname[:-4]}.cue" not in cue_files): + printBinCueMismatch() + else: + printBinCueMismatch() + +# ------------------------------------------------------------------------------------------------- + +def printStatus(decompressing, fname, extension): + if (gamecount[0] != 0): + print("╟-------------------------------------------------------------------------------╢") + gamecount[0] += 1 + if (decompressing): + print(f"║ Extracting to {fname}.{targets[selection]}... ({gamecount[0]}){(57 - len(fname) - len(targets[selection]) - len(str(gamecount[0]))) * ' '}║") + else: + print(f"║ Compressing to {fname}.{targets[selection]}... ({gamecount[0]}){(56 - len(fname) - len(targets[selection]) - len(str(gamecount[0]))) * ' '}║") + +# ------------------------------------------------------------------------------------------------- + +def deleteFile(fname): # Delete a file in pwd + print(f"║ Deleting {fname}...{(66 - len(fname)) * ' '}║") + os.remove(f"./{fname}") + + +# ================================================================================================= + +options = { # Options listings + "1" : "Convert ISO to CSO", + "2" : "Convert ISO to CHD", + "3" : "Convert CUE/BIN to CHD", + "4" : "Convert CSO to CHD", + "5" : "Convert DVD CHD to CSO", + "6" : "Extract DVD CHD to ISO", + "7" : "Extract CD CHD to CUE/BIN", + "8" : "Extract CSO to ISO", + "9" : "Exit script", +} + +sources = { # Source file extensions + "1" : "iso", + "2" : "iso", + "3" : "cue/bin", + "4" : "cso", + "5" : "chd", + "6" : "chd", + "7" : "chd", + "8" : "cso", +} + +targets = { # Target file extensions + "1" : "cso", + "2" : "chd", + "3" : "chd", + "4" : "chd", + "5" : "cso", + "6" : "iso", + "7" : "cue/bin", + "8" : "iso", +} + +reqs = { # Selection dependencies + "1" : ["maxcso"], + "2" : ["chdman"], + "3" : ["chdman"], + "4" : ["maxcso", "chdman"], + "5" : ["maxcso", "chdman"], + "6" : ["chdman"], + "7" : ["chdman"], + "8" : ["maxcso"], +} + +# ------------------------------------------------------------------------------------------------- + +print("╔===============================================================================╗") +print("║ CSO/CHD/ISO/CUEBIN Conversion by Refraction and TheTechnician27 ║") +print("║ (Version Jul 16 2024) ║") +print("╠===============================================================================╣") +print("║ ║") +print("║ PLEASE NOTE: This will affect all files in this folder! ║") +print("║ Be sure to run this from the same directory as the files you wish to convert. ║") +print("║ ║") + +for number, message in options.items(): + print("║ ", number, " - ", message, f"{(70 - len(message)) * ' '}║") + +print("║ ║") +print("╠===============================================================================╝") +print("║ ") +selection = input("║ Type the number corresponding to your selection then press ENTER: ") + +# ------------------------------------------------------------------------------------------------- + +selection_num = 0 +try: + selection_num = int(selection) + +except ValueError: + print("║ ") + print("╠===============================================================================╗") + print("║ Invalid option. ║") + print("╚===============================================================================╝") + sys.exit(1) + +if (selection_num < 9 and selection_num > 0): + + for program in reqs[selection]: # Check for dependencies + checkProgramMissing(program) + + delete = deletionChoice(sources[selection]) # Choose to delete source files + + if (selection_num < 6): + blocksize = blockSizeChoice(selection_num == 3) # Choose block size if compressing + + if (selection_num != 3): + source_files = returnFilteredPwdContents(sources[selection]) # Get files in pwd + else: + bin_files = returnFilteredPwdContents("bin") # Get all BIN files in pwd + cue_files = returnFilteredPwdContents("cue") # Get all CUE files in pwd + checkBinCueMismatch(bin_files, cue_files) + + print("║ ") + print("╠===============================================================================╗") + +# ------------------------------------------------------------------------------------------------- + +match selection: + + # ===== COMPRESS ISO TO CSO =========================================================== + + case "1": + + for fname in source_files: + printStatus(False, fname[:-4], targets[selection]) + process = Popen(["maxcso", f"--block={blocksize}", # Compress to new CSO file + fname], stdout=PIPE, stderr=PIPE) + stdout, stderr = process.communicate() + checkSuccess(process.returncode, False, # Ensure CSO file created + fname[:-4], targets[selection]) + + if (delete): # Delete source if requessted + deleteFile(fname) + + # ===== COMPRESS ISO TO CHD ========================================================== + + case "2": + + for fname in source_files: + printStatus(False, fname[:-4], targets[selection]) + process = Popen(["chdman", "createraw", "-us", # Compress to new CHD file + "2048", "-hs", f"{blocksize}", + "-f", "-i", fname, "-o", + f"{fname[:-4]}.chd"], + stdout=PIPE, stderr=PIPE) + stdout, stderr = process.communicate() + checkSuccess(process.returncode, False, # Ensure CHD file created + fname[:-4], targets[selection]) + if (delete): # Delete source if requested + deleteFile(fname) + + # ===== COMPRESS CUE/BIN TO CHD ======================================================= + + case "3": + + for fname in cue_files: + printStatus(False, fname[:-4], targets[selection]) + process = Popen(["chdman", "createcd", "-hs", # Compress to new CHD file + f"{blocksize}", "-i", fname, + "-o", f"{fname[:-4]}.chd"], + stdout=PIPE, stderr=PIPE) + stdout, stderr = process.communicate() + checkSuccess(process.returncode, False, # Ensure CHD file created + fname[:-4], targets[selection]) + + if (delete): # Delete source if requested + deleteFile(fname) + deleteFile(f"{fname[:-4]}.bin") + + # ===== COMPRESS CSO TO CHD ========================================================= + + case "4": + + for fname in source_files: + printStatus(False, fname[:-4], targets[selection]) + process = Popen(["maxcso", "--decompress", fname], # Create intermediate file + stdout=PIPE, stderr=PIPE) + stdout, stderr = process.communicate() + checkSuccess(process.returncode, False, # Ensure temp ISO created + fname[:-4], "iso") + + process = Popen(["chdman", "createraw", "-us", # Compress to new CHD + "2048", "-hs", f"{blocksize}", + "-f", "-i", f"{fname[:-4]}.iso", + "-o", f"{fname[:-4]}.chd"], + stdout=PIPE, stderr=PIPE) + stdout, stderr = process.communicate() + + checkSuccess(process.returncode, False, # Ensure CHD file created + fname[:-4], targets[selection]) + deleteFile(f"{fname[:-4]}.iso") # Delete intermediate file + + if (delete): # Delete source if requested + deleteFile(fname) + + # ===== COMPRESS CHD TO CSO =========================================================== + + case "5": + + for fname in source_files: + printStatus(False, fname[:-4], targets[selection]) + process = Popen(["chdman", "extractraw", "-i", + fname, "-o", f"{fname[:-4]}.iso"], + stdout=PIPE, stderr=PIPE) + stdout, stderr = process.communicate() + checkSuccess(process.returncode, False, # Ensure temp ISO created + fname[:-4], "iso") + + process = Popen(["maxcso", f"--block={blocksize}", # Compress to new CSO file + f"{fname[:-4]}.iso"], + stdout=PIPE, stderr=PIPE) + stdout, stderr = process.communicate() + checkSuccess(process.returncode, False, # Ensure CSO file created + fname[:-4], targets[selection]) + deleteFile(f"{fname[:-4]}.iso") # Delete intermediate file + + if (delete): # Delete source if requested + deleteFile(fname) + + # ===== EXTRACT DVD CHD TO ISO ======================================================== + + case "6": + + for fname in source_files: + printStatus(True, fname[:-4], targets[selection]) + process = Popen(["chdman", "extractraw", "-i", # Extract to new ISO file + fname, "-o", f"{fname[:-4]}.iso"], + stdout=PIPE, stderr=PIPE) + stdout, stderr = process.communicate() + checkSuccess(process.returncode, True, # Ensure ISO file created + fname[:-4], targets[selection]) + + if (delete): # Delete source if requested + deleteFile(fname) + + # ===== EXTRACT CD CHD TO CUE/BIN ===================================================== + + case "7": + + for fname in source_files: + printStatus(True, fname[:-4], "bin") + print(f"Extracting to {fname[:-4]}.bin") + process = Popen(["chdman", "extractcd", # Extract to new BIN/CUE pair + "-i", fname, "-o", + f"{fname[:-4]}.cue"], + stdout=PIPE, stderr=PIPE) + stdout, stderr = process.communicate() + checkSuccess(process.returncode, False, # Ensure BIN/CUE files created + fname[:-4], "bin") + + if (delete): # Delete source if requested + deleteFile(fname) + + # ===== EXTRACT CSO TO ISO ============================================================= + + case "8": + + for fname in source_files: + printStatus(True, fname[:-4], targets[selection]) + process = Popen(["maxcso", "--decompress", fname], # Extract to new ISO file + stdout=PIPE, stderr=PIPE) + stdout, stderr = process.communicate() + checkSuccess(process.returncode, True, # Ensure ISO file present + fname[:-4], targets[selection]) + if (delete): # Delete source if requested + deleteFile(fname) + + # ===== EXIT SCRIPT =================================================================== + + case "9": + print("║ ") + print("╠===============================================================================╗") + print("║ Goodbye! :) ║") + print("╚===============================================================================╝") + sys.exit(0) + + # ===== DEFAULT (ERROR) =============================================================== + + case _: + print("║ Invalid option. ║") + print("╚===============================================================================╝") + sys.exit(1) + +# ------------------------------------------------------------------------------------------------- + +print("╠===============================================================================╣") +print("║ Process complete! ║") +print("╚===============================================================================╝") +sys.exit(0)