mirror of https://github.com/PCSX2/pcsx2.git
Tools: Port refraction's bulk compression script to Python
This commit is contained in:
parent
cd3e11bff7
commit
86195171ce
|
@ -0,0 +1,420 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# 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 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
|
from os.path import exists
|
||||||
|
|
||||||
|
gamecount = [0]
|
||||||
|
|
||||||
|
# =================================================================================================
|
||||||
|
|
||||||
|
def deletionChoice(source_extension): # Choose to delete source files
|
||||||
|
|
||||||
|
yesno = {
|
||||||
|
"n" : 0,
|
||||||
|
"no" : 0,
|
||||||
|
"y" : 1,
|
||||||
|
"yes" : 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
print("╟-------------------------------------------------------------------------------╢")
|
||||||
|
print(f"║ Do you want to delete the original {source_extension.upper()} files as they are converted?")
|
||||||
|
choice = input("║ Type Y or N then press ENTER: ").lower()
|
||||||
|
|
||||||
|
if (not choice in yesno):
|
||||||
|
exitInvalidOption()
|
||||||
|
|
||||||
|
return (yesno[choice])
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def blockSizeChoice(is_cd, decompressing): # Choose block size
|
||||||
|
|
||||||
|
if (decompressing):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
sizes = {
|
||||||
|
"1" : 16384,
|
||||||
|
"2" : 131072,
|
||||||
|
"3" : 262144,
|
||||||
|
} if not is_cd else {
|
||||||
|
"1": 17136,
|
||||||
|
"2": 132192,
|
||||||
|
"3": 264384,
|
||||||
|
}
|
||||||
|
|
||||||
|
print("╟-------------------------------------------------------------------------------╢")
|
||||||
|
print("║ Please pick the 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):
|
||||||
|
exitInvalidOption()
|
||||||
|
|
||||||
|
return (sizes[blocksize])
|
||||||
|
|
||||||
|
# =================================================================================================
|
||||||
|
|
||||||
|
def checkSuccess(compressing, fname, extension, error_code): # Ensure file created properly
|
||||||
|
|
||||||
|
target_fname = f"{fname}.{extension}"
|
||||||
|
if (error_code):
|
||||||
|
|
||||||
|
print("╠===============================================================================╣")
|
||||||
|
|
||||||
|
if (compressing):
|
||||||
|
print(f"║ Compression to {extension.upper()} failed for the following:{(37 - len(extension)) * ' '}║")
|
||||||
|
else:
|
||||||
|
print(f"║ Extraction to {extension.upper()} failed for the following:{(38 - len(extension)) * ' '}║")
|
||||||
|
|
||||||
|
print(f"║ {target_fname}{(77 - len(target_fname)) * ' '}║")
|
||||||
|
print("╚===============================================================================╝")
|
||||||
|
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"║ {target_fname} created.{(69 - len(target_fname)) * ' '}║")
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def checkProgramMissing(program):
|
||||||
|
|
||||||
|
if (sys.platform.startswith('win32') and exists(f"./{program}.exe")):
|
||||||
|
return # Windows
|
||||||
|
|
||||||
|
else: # Linux, macOS
|
||||||
|
from shutil import which
|
||||||
|
if (which(program) is not None):
|
||||||
|
return
|
||||||
|
|
||||||
|
print("╠===============================================================================╗")
|
||||||
|
print(f"║ {program} failed, {program} is missing.{(39 - (len(program) * 2)) * ' '}║")
|
||||||
|
print("╚===============================================================================╝")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def checkBinCueMismatch(bin_files, cue_files): # Ensure all bins and cues match
|
||||||
|
|
||||||
|
if (len(bin_files) != len(cue_files)): # Ensure numerical parity
|
||||||
|
exitBinCueMismatch()
|
||||||
|
|
||||||
|
for fname in bin_files: # Ensure filename parity
|
||||||
|
if (f"{fname[:-4]}.cue" not in cue_files):
|
||||||
|
exitBinCueMismatch()
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def checkDuplicates(source_files, target_extensions, crash_protection_type=0):
|
||||||
|
|
||||||
|
dupe_options = {
|
||||||
|
"s" : 0,
|
||||||
|
"skip" : 0,
|
||||||
|
"o" : 1,
|
||||||
|
"overwrite" : 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
dupe_files = []
|
||||||
|
dupe_names = []
|
||||||
|
target_files = []
|
||||||
|
|
||||||
|
for extension in target_extensions:
|
||||||
|
target_files[len(target_files):] = returnFilteredPwdContents(extension)
|
||||||
|
|
||||||
|
for fname in source_files:
|
||||||
|
for extension in target_extensions:
|
||||||
|
target_fname = f"{fname[:-4]}.{extension}"
|
||||||
|
if (target_fname in target_files):
|
||||||
|
dupe_files.append(target_fname)
|
||||||
|
|
||||||
|
match crash_protection_type:
|
||||||
|
case 0:
|
||||||
|
pass
|
||||||
|
case 1: # Skip any dupe files no matter what
|
||||||
|
[dupe_names.append(fname[:-4]) for fname in dupe_files if fname[:-4] not in dupe_names]
|
||||||
|
return dupe_names
|
||||||
|
case 2: # Only skip if intermediate .iso present
|
||||||
|
[dupe_names.append(fname[:-4]) for fname in dupe_files if fname[:-4] not in dupe_names and fname[-4:] == ".iso"]
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if (not dupe_files):
|
||||||
|
return dupe_names
|
||||||
|
|
||||||
|
print("╟-------------------------------------------------------------------------------╢")
|
||||||
|
print("║ The following files were found which would be overwritten:")
|
||||||
|
|
||||||
|
for fname in dupe_files:
|
||||||
|
print(f"║ - {fname}")
|
||||||
|
|
||||||
|
print("║")
|
||||||
|
print("║ You may choose to OVERWRITE or SKIP all of these.")
|
||||||
|
if (crash_protection_type == 2):
|
||||||
|
print("║ NOTE: chdman cannot overwrite .cso files. These will be skipped regardless.")
|
||||||
|
choice = input("║ Press 'O' to overwrite or 'S' to skip and press ENTER: ").lower()
|
||||||
|
|
||||||
|
if (choice in dupe_options):
|
||||||
|
if (not dupe_options[choice]): # Skip
|
||||||
|
[dupe_names.append(fname[:-4]) for fname in dupe_files if fname[:-4] not in dupe_names]
|
||||||
|
return dupe_names
|
||||||
|
else:
|
||||||
|
exitInvalidOption()
|
||||||
|
|
||||||
|
# =================================================================================================
|
||||||
|
|
||||||
|
def printInitialStatus(decompressing, target_fname):
|
||||||
|
|
||||||
|
if (gamecount[0] != 0):
|
||||||
|
print("╟-------------------------------------------------------------------------------╢")
|
||||||
|
gamecount[0] += 1
|
||||||
|
|
||||||
|
if (decompressing):
|
||||||
|
print(f"║ Extracting to {target_fname}... ({gamecount[0]}){(58 - len(target_fname) - len(str(gamecount[0]))) * ' '}║")
|
||||||
|
else:
|
||||||
|
print(f"║ Compressing to {target_fname}... ({gamecount[0]}){(57 - len(target_fname) - len(str(gamecount[0]))) * ' '}║")
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def printSkip(target_fname):
|
||||||
|
|
||||||
|
if (gamecount[0] != 0):
|
||||||
|
print("╟-------------------------------------------------------------------------------╢")
|
||||||
|
gamecount[0] += 1
|
||||||
|
print(f"║ Skipping creation of {target_fname}{(57 - len(target_fname)) * ' '}║")
|
||||||
|
|
||||||
|
# =================================================================================================
|
||||||
|
|
||||||
|
def createCommandList(mode, source_fname, target_fname, blocksize=0):
|
||||||
|
|
||||||
|
match mode:
|
||||||
|
case 1:
|
||||||
|
return [["maxcso", f"--block={blocksize}", source_fname]]
|
||||||
|
case 2:
|
||||||
|
return [["chdman", "createraw", "-us", "2048", "-hs", f"{blocksize}", "-f", "-i", source_fname, "-o", target_fname]]
|
||||||
|
case 3:
|
||||||
|
return [["chdman", "createcd", "-hs", f"{blocksize}", "-i", source_fname, "-o", f"{source_fname[:-4]}.chd"]]
|
||||||
|
case 4:
|
||||||
|
return [["maxcso", "--decompress", source_fname],
|
||||||
|
["chdman", "createraw", "-us", "2048", "-hs", f"{blocksize}", "-f", "-i", f"{source_fname[:-4]}.iso", "-o", f"{source_fname[:-4]}.chd"]]
|
||||||
|
case 5:
|
||||||
|
return [["chdman", "extractraw", "-i", source_fname, "-o", f"{source_fname[:-4]}.iso"],
|
||||||
|
["maxcso", f"--block={blocksize}", f"{source_fname[:-4]}.iso"]]
|
||||||
|
case 6:
|
||||||
|
return [["chdman", "extractraw", "-i", source_fname, "-o", target_fname]]
|
||||||
|
case 7:
|
||||||
|
return [["chdman", "extractcd", "-i", source_fname, "-o", target_fname]]
|
||||||
|
case 8:
|
||||||
|
return [["maxcso", "--decompress", source_fname]]
|
||||||
|
case _:
|
||||||
|
print("You have somehow chosen an invalid mode, and this was not correctly caught by the program.\nPlease report this as a bug.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# =================================================================================================
|
||||||
|
|
||||||
|
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 deleteFile(fname): # Delete a file in pwd
|
||||||
|
|
||||||
|
print(f"║ Deleting {fname}...{(66 - len(fname)) * ' '}║")
|
||||||
|
os.remove(f"./{fname}")
|
||||||
|
|
||||||
|
# =================================================================================================
|
||||||
|
|
||||||
|
def exitInvalidOption():
|
||||||
|
|
||||||
|
print("╠===============================================================================╗")
|
||||||
|
print("║ Invalid option. ║")
|
||||||
|
print("╚===============================================================================╝")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def exitBinCueMismatch():
|
||||||
|
|
||||||
|
print("╠===============================================================================╗")
|
||||||
|
print("║ All BIN files must have a matching CUE. ║")
|
||||||
|
print("╚===============================================================================╝")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# =================================================================================================
|
||||||
|
# /////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
# =================================================================================================
|
||||||
|
|
||||||
|
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 : ["iso", "chd"],
|
||||||
|
5 : ["iso", "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, RedDevilus 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("║")
|
||||||
|
mode = input("║ Type the number corresponding to your selection then press ENTER: ")
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
try:
|
||||||
|
mode = int(mode)
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
exitInvalidOption()
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if (mode < 9 and mode > 0):
|
||||||
|
|
||||||
|
for program in reqs[mode]: # Check for dependencies
|
||||||
|
checkProgramMissing(program)
|
||||||
|
|
||||||
|
delete = deletionChoice(sources[mode]) # Choose to delete source files
|
||||||
|
blocksize = blockSizeChoice(mode == 3, mode > 5) # Choose block size if compressing
|
||||||
|
|
||||||
|
match mode:
|
||||||
|
case 3:
|
||||||
|
bin_files = returnFilteredPwdContents("bin") # Get all BIN files in pwd
|
||||||
|
source_files = returnFilteredPwdContents("cue") # Get all CUE files in pwd
|
||||||
|
checkBinCueMismatch(bin_files, source_files)
|
||||||
|
dupe_list = checkDuplicates(source_files, targets[mode], 1)
|
||||||
|
case 5:
|
||||||
|
source_files = returnFilteredPwdContents(sources[mode]) # Get source files in pwd
|
||||||
|
dupe_list = checkDuplicates(source_files, targets[mode], 2)
|
||||||
|
case 6:
|
||||||
|
source_files = returnFilteredPwdContents(sources[mode]) # Get source files in pwd
|
||||||
|
dupe_list = checkDuplicates(source_files, targets[mode], 1)
|
||||||
|
case _:
|
||||||
|
source_files = returnFilteredPwdContents(sources[mode]) # Get source files in pwd
|
||||||
|
dupe_list = checkDuplicates(source_files, targets[mode])
|
||||||
|
|
||||||
|
|
||||||
|
print("╠===============================================================================╗")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
for fname in source_files:
|
||||||
|
|
||||||
|
target_fname = f"{fname[:-4]}.{targets[mode][0]}"
|
||||||
|
commands = createCommandList(mode, fname, target_fname, blocksize)
|
||||||
|
if (fname[:-4] in dupe_list):
|
||||||
|
printSkip(target_fname)
|
||||||
|
continue
|
||||||
|
|
||||||
|
printInitialStatus(mode > 5, f"{fname[:-4]}.{targets[mode][-1]}")
|
||||||
|
|
||||||
|
for step, command in enumerate (commands):
|
||||||
|
|
||||||
|
process = Popen(commands[step], stdout=PIPE, stderr=PIPE) # Execute process
|
||||||
|
stdout, stderr = process.communicate() # Suppress output
|
||||||
|
checkSuccess(mode < 6, fname[:-4], # Ensure target creation
|
||||||
|
targets[mode][step], process.returncode)
|
||||||
|
|
||||||
|
if (step == 1): # Delete intermediate file
|
||||||
|
deleteFile(f"{fname[:-4]}.iso")
|
||||||
|
|
||||||
|
if (delete): # Delete source requested
|
||||||
|
deleteFile(fname)
|
||||||
|
if (mode == 3):
|
||||||
|
deleteFile(f"{fname[:-4]}.bin")
|
||||||
|
|
||||||
|
# ===== EXIT SCRIPT ===============================================================================
|
||||||
|
|
||||||
|
elif (mode == 9):
|
||||||
|
print("╠===============================================================================╗")
|
||||||
|
print("║ Goodbye! :) ║")
|
||||||
|
print("╚===============================================================================╝")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# ===== EXIT SCRIPT WITH ERROR ====================================================================
|
||||||
|
|
||||||
|
else:
|
||||||
|
exitInvalidOption()
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
print("╠===============================================================================╣")
|
||||||
|
print("║ Process complete! ║")
|
||||||
|
print("╚===============================================================================╝")
|
||||||
|
sys.exit(0)
|
Loading…
Reference in New Issue