Created
April 24, 2023 00:05
-
-
Save Bluscream/8404c5cb8899625ec93f7d2e7ccb7c45 to your computer and use it in GitHub Desktop.
Minecraft Mod Sorter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import hashlib | |
import os | |
from pathlib import Path | |
from pprint import pformat | |
import json | |
import urllib3 | |
import logging | |
import shutil | |
from packaging.version import parse as _parse_version | |
from packaging.version import InvalidVersion | |
from re import compile | |
from sanitize_filename import sanitize | |
from minecraft_mod_manager.gateways.jar_parser import JarParser | |
from minecraft_mod_manager.core.entities.mod import Mod, ModLoaders | |
logging.basicConfig(filename=os.path.splitext(__file__)[0]+'.log', encoding='utf-8', level=logging.DEBUG, format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s') | |
log = logging.getLogger(__name__) | |
log.addHandler(logging.StreamHandler()) | |
modlog = open(os.path.dirname(__file__)+"/mods.csv", mode="w") | |
modlog.write("hash;path;id;version;name;mod loader;mod loader version;mc version\n") # [';'.join(mod.__members())] | |
seenmd5s = set() | |
def dispose(error = False): | |
log.info("END; ERROR=%s", error) | |
modlog.close() | |
input("done") | |
exit(1 if error else 0) | |
def hash_file(path, hash_type='md5', size=65536): | |
func = getattr(hashlib, hash_type)() | |
log.debug("Hashing file %s with algorithm %s", path, hash_type) | |
with open(path, 'rb') as f: | |
b = f.read(size) | |
while len(b) > 0: | |
func.update(b) | |
b = f.read(size) | |
return func.hexdigest() | |
versionPattern = compile(r'\d+(=?\.(\d+(=?\.(\d+)*)*)*)*') | |
def extract_version(v): | |
result = versionPattern.search(v) | |
if result: return result.group(0) | |
return str(v) | |
def parse_version(v:str): | |
if not v: return v | |
if isinstance(v, list): v = v[0] | |
try: v = _parse_version(extract_version(v).strip("><=~").removesuffix(".x").removesuffix(".*").split("-",1)[0]) # "v"+version | |
except InvalidVersion as ex: | |
log.error("_parse_version(%s) failed:", v, ex.args[0]) | |
return v | |
log.debug("Parsed version %s to %s",v,_v) | |
return v | |
def parse_forge_version(v:str): | |
return int(v.strip('[]()').split(",")[0]) | |
# _t = mod.loader_version.split(",")[0] | |
# _t = ''.join(ch for ch in _t if ch.isalnum()) | |
# Get Forge versions so we can resolve mc_version | |
forge_version_api_url = "https://meta.prismlauncher.org/v1/net.minecraftforge/index.json" | |
def fetch_list(location=forge_version_api_url, cache_file="forge.versions.json"): | |
"""Goes to the default location and returns a python list. | |
""" | |
try: | |
http = urllib3.PoolManager() | |
request = http.request("GET", location) | |
raw_data = request.data.decode("utf-8") | |
data = json.loads(raw_data) | |
with open(cache_file, "w") as cache: cache.write(raw_data) | |
return data | |
except (urllib3.exceptions.HTTPError, json.JSONDecodeError) as exc: | |
log.exception("Cannot access Intranet List Location - fetching cache") | |
try: | |
data = json.loads(open(cache_file).readlines()) | |
return data | |
except (IOError, json.JSONDecodeError): | |
log.exception("Cache File not found or broken") | |
raise | |
forge_versions_data = fetch_list() | |
forge_versions = {} | |
for version in forge_versions_data["versions"]: | |
for req in version['requires']: | |
if req['uid'] == 'net.minecraft': | |
forge_versions[version['version']] = req['equals'] | |
log.info("Got %i forge versions from %s", len(forge_versions), forge_version_api_url) | |
# The directory to move the mods to | |
mod_dir = "D:\\OneDrive\\Games\\Minecraft\\mods\\" | |
# Search for .jar files on the system | |
output = os.popen('es ext:jar').read()+"\n"+os.popen('es ext:jar.disabled').read() | |
# log.debug("output:",output) | |
jar_files = [path.strip() for path in output.split('\n') if path] | |
jar_folders: set[str] = set() | |
for jar_file in jar_files: | |
jar_folder = os.path.dirname(jar_file.strip()) | |
if jar_folder: jar_folders.add(jar_folder) | |
log.info(f"Found {len(jar_files)} JAR files in {len(jar_folders)} folders.") | |
# Loop through each .jar folder | |
for jar_folder in sorted(jar_folders): | |
try: | |
# log.debug("Processing folder %s", jar_folder) | |
# Ignore folders within target folder | |
if mod_dir in jar_folder: continue | |
folder_path = Path(jar_folder) | |
try: is_dir = folder_path.is_dir() | |
except PermissionError as ex: | |
log.error("Not enough permissions to scan folder %s",folder_path) | |
continue | |
if not is_dir: continue | |
log.debug("Processing folder %s", jar_folder) | |
# Create a new instance of the jar parser | |
jar_parser = JarParser(folder_path) | |
# Parse the metadata of the .jar file | |
for mod in jar_parser.mods: | |
try: | |
mod.path = folder_path / mod.file | |
mod.hash = hash_file(mod.path) | |
if mod.hash in seenmd5s: continue | |
seenmd5s.add(mod.hash) | |
if not mod.mc_version and mod.loader_version: | |
if mod.mod_loader == ModLoaders.Forge: | |
forge_version = parse_forge_version(mod.loader_version) | |
mod.mc_version = forge_versions[parse_forge_version(mod.loader_version)] if forge_version in forge_versions else None | |
mod.loader_version = parse_version(mod.loader_version) or "" | |
mod.mc_version = parse_version(mod.mc_version) or "" | |
modlog.write(f"{mod.hash};{mod.path};{mod.id};{mod.version};{mod.name};{mod.mod_loader};{mod.loader_version};{mod.mc_version}\n") # [';'.join(mod.__members())] | |
# Create the directory for the Minecraft version if it doesn't already exist | |
version_dir = os.path.join(mod_dir, sanitize(str(mod.mc_version) or "Unknown Version")) | |
loader_dir = os.path.join(version_dir, sanitize(mod.mod_loader.value.title() or "Unknown Loader")) | |
if not os.path.isdir(loader_dir): | |
log.info("Creating %s", loader_dir) | |
os.makedirs(loader_dir, exist_ok=True) | |
# Move the .jar file to the appropriate directory | |
dest_file = os.path.join(loader_dir, mod.file) | |
log.info("Copying %s to %s", mod.path, dest_file) | |
shutil.copyfile(mod.path, dest_file) | |
except Exception as ex: | |
log.error("GOT ERROR LOL") | |
log.exception(ex) | |
input("error in mods for loop") | |
# input("folder done") | |
# # Skip files that are not recognized as Minecraft mods | |
# if not metadata or not metadata["minecraft_version"]: | |
# continue | |
except Exception as ex: | |
log.error("GOT ERROR LOL") | |
log.exception(ex) | |
input("error in jar_folders loop") | |
log.info("Got %s unique md5 hashes",len(seenmd5s)) | |
dispose() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment