Skip to content

Instantly share code, notes, and snippets.

@mat-1
Last active February 21, 2022 23:01
Show Gist options
  • Save mat-1/03f332170fb3f06eeb3a1df981720e9a to your computer and use it in GitHub Desktop.
Save mat-1/03f332170fb3f06eeb3a1df981720e9a to your computer and use it in GitHub Desktop.
Put this in your mods folder and run it to automatically update your Fabric mods.
try:
from simplejson.errors import JSONDecodeError
except:
from json import JSONDecodeError
import traceback
import requests
import hashlib
import shutil
import array
import time
import sys
import os
def sha1_hash(mod_file_name):
with open(mod_file_name, 'rb') as f:
mod_file_buffer = f.read()
sha1 = hashlib.sha1()
sha1.update(mod_file_buffer)
return sha1.hexdigest()
def murmur2_hash(mod_file_name):
with open(mod_file_name, 'rb') as f:
input = bytearray()
for b in f.read():
if b not in { 9, 10, 13, 32 }:
input.append(b)
seed = 1
l = len(input)
m = 0x5bd1e995
h = seed ^ l
x = l % 4
o = l - x
for k in array.array('I', input[:o]):
k = (k * m) & 0xFFFFFFFF
h = (((k ^ (k >> 24)) * m) ^ (h * m)) & 0xFFFFFFFF
if x > 0:
if x > 2:
h ^= input[o+2] << 16
if x > 1:
h ^= input[o+1] << 8
h = ((h ^ input[o]) * m) & 0xFFFFFFFF
h = ((h ^ (h >> 13)) * m) & 0xFFFFFFFF
return (h ^ (h >> 15))
# auto update the auto updater
new_code_contents = requests.get('https://gist.githubusercontent.com/mat-1/03f332170fb3f06eeb3a1df981720e9a/raw/update.py?' + str(time.time())).text
with open(__file__, 'r') as f:
current_code_contents = f.read()
if new_code_contents != current_code_contents:
user_update_response = input('New version of update.py detected! Type anything that starts with "y" and enter to update: ')
if user_update_response and user_update_response.strip()[0] == 'y':
with open(__file__, 'w') as f:
f.write(new_code_contents)
exec(new_code_contents)
exit()
protocol_versions = requests.get('https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/pc/common/protocolVersions.json').json()
default_minecraft_version = ''
try:
# read their options.txt to get the minecraft version they're using
options_txt_location = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'options.txt')
with open(options_txt_location, 'r') as f:
options_txt_lines = f.read().splitlines()
options_txt = {}
for line in options_txt_lines:
key, value = line.split(':', 1)
options_txt[key] = value
if 'version' not in options_txt: raise FileNotFoundError
for protocol_version in protocol_versions:
if str(protocol_version['dataVersion']) == options_txt['version']:
default_minecraft_version = protocol_version['minecraftVersion']
break
except FileNotFoundError:
# if they don't have an options.txt, just go with the most recent release of Minecraft that's not a snapshot
for protocol_version in protocol_versions:
if protocol_version['version'] < 1000000000:
default_minecraft_version = protocol_version['minecraftVersion']
break
minecraft_version: str = input(f'Enter your desired Minecraft version (don\'t input anything to use {default_minecraft_version}): ') or default_minecraft_version
def modrinth_find_latest_download(mod_id, previous_hash):
mod_versions = requests.get(f'https://api.modrinth.com/api/v1/mod/{mod_id}/version').json()
previous_mod_version = None
best_mod_version = None
for mod_version in mod_versions:
if len(mod_version['files']) == 0: continue
if mod_version['files'][-1]['hashes']['sha1'] == previous_hash:
previous_mod_version = mod_version
if not minecraft_version in mod_version['game_versions']: continue
if not 'fabric' in mod_version['loaders']: continue
if not best_mod_version:
best_mod_version = mod_version
if best_mod_version:
return previous_mod_version, best_mod_version
return None, None
def curseforge_find_latest_download(mod_id, previous_hash):
mod_data = requests.get(
f'https://addons-ecs.forgesvc.net/api/v2/addon/{mod_id}',
headers={ 'User-Agent': 'Mozilla/5.0 update.py' }
).json()
mod_files = requests.get(
f'https://addons-ecs.forgesvc.net/api/v2/addon/{mod_id}/files',
headers={ 'User-Agent': 'Mozilla/5.0 update.py' }
).json()
best_version = None
previous_version = None
for mod_version in sorted(mod_files, key=lambda i: i['id'], reverse=True):
if mod_version['packageFingerprint'] == previous_hash:
previous_version = mod_version
if minecraft_version not in mod_version['gameVersion']: continue
is_fabric = False
for module in mod_version['modules']:
if module['foldername'] == 'fabric.mod.json':
is_fabric = True
break
if not is_fabric: continue
if not best_version:
best_version = mod_version
if best_version:
return str(mod_data['name']), previous_version, best_version
return None, None, None
class ModUpdateResponse:
def __init__(
self,
status_code,
mod_name,
mod_file_name,
mod_download_url,
old_version,
new_version
):
self.status_code = status_code
self.mod_name = mod_name
self.mod_file_name = mod_file_name
self.mod_download_url = mod_download_url
self.old_version = old_version
self.new_version = new_version
def guess_version_from_filename(file_name):
file_name = file_name[:-4]
split_file_name = file_name.split('-')
if split_file_name[-1] != 'fabric':
return split_file_name[-1]
else:
return split_file_name[-2]
def find_latest_download(mod_file_name):
mod_file_hash_sha1 = sha1_hash(mod_file_name)
mod_file_hash_murmur2 = murmur2_hash(mod_file_name)
try:
modrinth_version = requests.get(f'https://api.modrinth.com/v2/version_file/{mod_file_hash_sha1}?algorithm=sha1').json()
previous_modrinth_download, latest_modrinth_download = modrinth_find_latest_download(modrinth_version['id'], previous_hash=mod_file_hash_sha1)
if latest_modrinth_download:
mod_name = requests.get('https://api.modrinth.com/v2/project/' + modrinth_version['id']).json()['title']
latest_modrinth_download_hash = latest_modrinth_download['files'][-1]['hashes']['sha1']
latest_modrinth_download_url = latest_modrinth_download['files'][-1]['url']
latest_modrinth_download_file_name = latest_modrinth_download['files'][-1]['filename']
previous_modrinth_download_version_number = previous_modrinth_download['version_number'] if previous_modrinth_download else '???'
latest_modrinth_download_version_number = latest_modrinth_download['version_number']
if latest_modrinth_download_hash == mod_file_hash_sha1:
return ModUpdateResponse('already_latest', mod_name, latest_modrinth_download_file_name, latest_modrinth_download_url, previous_modrinth_download_version_number, latest_modrinth_download_version_number)
else:
return ModUpdateResponse('updated', mod_name, latest_modrinth_download_file_name, latest_modrinth_download_url, previous_modrinth_download_version_number, latest_modrinth_download_version_number)
except JSONDecodeError:
pass
curseforge_version = requests.post('https://addons-ecs.forgesvc.net/api/v2/fingerprint', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 update.py'
}, json=[mod_file_hash_murmur2])
curseforge_match = curseforge_version.json()
if len(curseforge_match['exactMatches']) > 0:
mod_id = curseforge_match['exactMatches'][0]['id']
mod_name, previous_curseforge_download, latest_curseforge_download = curseforge_find_latest_download(mod_id, previous_hash=mod_file_hash_murmur2)
if latest_curseforge_download:
latest_curseforge_download_hash = latest_curseforge_download['packageFingerprint']
latest_curseforge_download_url = latest_curseforge_download['downloadUrl']
latest_curseforge_download_file_name = latest_curseforge_download['fileName']
previous_curseforge_download_version_number = guess_version_from_filename(previous_curseforge_download['fileName']) if previous_curseforge_download else '???'
latest_curseforge_download_version_number = guess_version_from_filename(latest_curseforge_download['fileName'])
if latest_curseforge_download_hash == mod_file_hash_murmur2:
return ModUpdateResponse('already_latest', mod_name, latest_curseforge_download_file_name, latest_curseforge_download_url, previous_curseforge_download_version_number, latest_curseforge_download_version_number)
else:
return ModUpdateResponse('updated', mod_name, latest_curseforge_download_file_name, latest_curseforge_download_url, previous_curseforge_download_version_number, latest_curseforge_download_version_number)
return ModUpdateResponse('not_found', None, None, None, None, None)
mods_dir = os.path.dirname(os.path.realpath(__file__))
# make a backup of the mods first
try: shutil.rmtree(os.path.join(mods_dir, 'backup-mods'))
except FileNotFoundError: pass
os.makedirs(os.path.join(mods_dir, 'backup-mods'))
mod_file_names = os.listdir(mods_dir)
for mod_file_name in mod_file_names:
if mod_file_name.endswith('.jar'):
with open(os.path.join(mods_dir, mod_file_name), 'rb') as f:
with open(os.path.join(mods_dir, 'backup-mods/' + mod_file_name), 'wb') as f2:
f2.write(f.read())
os.system('')
def load_backup():
try:
for mod_file_name in os.listdir(mods_dir):
if mod_file_name.endswith('.jar'):
os.remove(os.path.join(mods_dir, mod_file_name))
for mod_file_name in os.listdir(os.path.join(mods_dir, 'backup-mods')):
with open(os.path.join(mods_dir, f'backup-mods/{mod_file_name}'), 'rb') as f:
with open(os.path.join(mods_dir, mod_file_name), 'wb') as f2:
f2.write(f.read())
except KeyboardInterrupt:
return load_backup()
while True:
try:
shutil.rmtree(os.path.join(mods_dir, 'backup-mods'))
return
except KeyboardInterrupt: pass
try:
no_mods_updated = True
for mod_file_name in mod_file_names:
if mod_file_name.endswith('.jar'):
update_response = find_latest_download(os.path.join(mods_dir, mod_file_name))
if update_response.status_code == 'already_latest':
print(f'\033[90m{update_response.mod_name} is already on the latest version. ({update_response.old_version})\033[0m')
elif update_response.status_code == 'updated':
updated_mod_contents = requests.get(str(update_response.mod_download_url)).content
os.remove(os.path.join(mods_dir, mod_file_name))
with open(os.path.join(mods_dir, str(update_response.mod_file_name)), 'wb') as f:
f.write(updated_mod_contents)
print(f'\033[92m{update_response.mod_name} has been updated!\033[0m ({update_response.old_version} -> {update_response.new_version})')
no_mods_updated = False
else:
print(f'\033[31mCouldn\'t find suitable version for {mod_file_name} on Modrinth or Curseforge.\033[0m')
if no_mods_updated:
input('No mods updated. Press enter to finish.\n')
else:
input('Finished updating mods! Press enter to finish or Ctrl+C to undo.\n')
except BaseException as e:
if isinstance(e, KeyboardInterrupt):
print('Loading backup...', end=' ', flush=True)
load_backup()
try:
input('Backup loaded.\nPress enter to finish.\n')
except (KeyboardInterrupt, EOFError):
pass
else:
traceback.print_exception(type(e), e, e.__traceback__, file=sys.stderr)
print('Something went wrong (traceback above), loading backup.')
load_backup()
try:
input('Backup loaded, put a comment on the Gist to report this bug. https://gist.github.com/mat-1/03f332170fb3f06eeb3a1df981720e9a\nPress enter to finish.\n')
except (KeyboardInterrupt, EOFError):
pass
# we no longer need this file
try: shutil.rmtree(os.path.join(mods_dir, 'backup-mods'))
except FileNotFoundError: pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment