#!/usr/bin/env python3
import argparse
import os
import sys
import hashlib
import shutil
import re
import requests
parser = argparse.ArgumentParser(
description="A Simple Git-Based Modpack Manager",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''\
Available commands:
install : Downloads mods listed in version.txt and populates the mods folder specified in pack-location.txt
apply_updates : Using the urls in mods.txt, repopulates version.txt to reflect the most recent mod versions
check_updates : Compares version.txt and mods.txt to see if any mods can be updated
''')
parser.add_argument('command',
nargs='?',
default='install',
help="The action to perform (default: install)")
parser.add_argument('filename',
nargs='?',
default="mods.txt",
help="Optional filename to specify latest mods (default: mods.txt)")
parser.add_argument('--version-file',
type=str,
default="version.txt",
help="Optional custom version file to download mods from (default: version.txt)")
parser.add_argument('--pack-location',
type=str,
help="Optional custom modpack folder location (default: read from pack-location.txt)")
parser.add_argument('--whitelist-file',
type=str,
default="whitelist.txt",
help="Optional custom whitelist file that tells 'install' which files not to remove (default: whitelist.txt)")
## loaded from version.txt
VERSION = 0
def read_file(fil):
strings = []
with open(fil) as f:
for line in f:
string = line.strip().split()
if len(line) > 1 and line[0] != '#':
# run strip on each element
string = tuple(map(lambda x: x.strip(), string))
strings.append(string)
return strings
# Apply updates to the actual mod pack
def install(args):
print("Updating pack with version " + str(VERSION) + "...")
print()
# (fname, checksum, url)
mods = read_file(args.version_file)
names = [mod[0] for mod in mods]
# whitelist client mods (e.g. optifine)
names += [line[0] for line in read_file(args.whitelist_file)]
for mod in mods:
mod_path = os.path.join(args.pack_location, mod[0])
if os.path.exists(mod_path) and os.path.isfile(mod_path) and \
hashlib.sha1(open(mod_path, 'rb').read()).hexdigest() == mod[1]:
print("Skipping {mod[0]}, already up to date".format(mod=mod))
else:
print('Installing {mod[0]} from {mod[2]}...'.format(mod=mod))
download_obj = requests.get(mod[2], stream=True)
with open(mod_path, "wb") as write_file:
shutil.copyfileobj(download_obj.raw, write_file)
print("Done!")
print()
print("Removing old mods...")
for jar in os.listdir(args.pack_location):
if jar not in names and os.path.splitext(jar)[1] == ".jar":
os.remove(os.path.join(args.pack_location, jar))
print("Removing '{jar}'".format(jar=jar))
print()
print("Finished installing mods!")
# Using the latest urls, update downloads.txt to match and have the correct sha1
def apply_updates(args):
print("Populating version File...")
mods = read_file(args.filename)
print("Getting new versions of all mods...")
ffx = firefox()
with open(args.version_file, 'w') as f:
f.write('# Format: <jarname> <hex digested sha1> <direct download url>\n')
f.write("#VERSION " + str(VERSION + 1) + "\n")
for mod in mods:
print("Fetching {mod[0]}...".format(mod=mod))
if 'curseforge' in mod[1]:
url = find_cdn(ffx, mod[1])
else:
url = requests.get(mod[1]).url
if url is None:
print('Failed to fetch {mod[0]}!'.format(mod=mod))
continue
resp = requests.get(url)
hsh = hashlib.sha1(resp.content).hexdigest()
f.write('{mod[0]} {hsh} {resp.url}\n'.format(mod=mod, hsh=hsh, resp=resp))
ffx.close()
print()
print("Done!")
print("Updates applied to {args.version_file}".format(args=args))
print("New pack version is " + str(VERSION + 1))
print("[!] No mods were installed. To update your mods folder, run 'update.py install'")
# Find if any updates are available
def check_updates(args):
print("Checking for updates to version " + str(VERSION) + "...")
latest = read_file(args.filename)
old = read_file(args.version_file)
old_urls = [mod[2] for mod in old]
num_updates = 0
print("Checking updates...")
ffx = firefox()
for mod in latest:
print("Checking for updates to {mod[0]}...".format(mod=mod), end="")
sys.stdout.flush() # takes care of line-buffered terminals
if 'curseforge' in mod[1]:
url = find_cdn(ffx, mod[1])
else:
url = requests.get(mod[1]).url
if url in old_urls:
print(" No updates")
else:
print(" Found update: " + url.split('/')[-1])
num_updates += 1
ffx.close()
print("Finished checking for updates. {num} mods can be updated".format(num=num_updates))
if num_updates >= 0:
print("Run 'python update.py apply_updates' to create a new version with these updates applied.")
# Use selenium to find curseforge CDN links around cloudflare
def find_cdn(ffx, url):
try:
ffx.get(url + '/download')
page_src = ffx.page_source
dl = re.search('Elerium.PublicProjectDownload.countdown\(".*?"\);', page_src)
if not dl:
return None
dl = re.search('\d+', dl.group(0).split('"')[1])
dl = dl.group(0)
four = str(int(dl[:4]))
three = str(int(dl[4:]))
ffx.get(url + '/files')
elements = ffx.find_elements_by_tag_name('tr')
file_name = elements[1].find_element_by_tag_name('a').text
return 'https://media.forgecdn.net/files/{four}/{three}/{jar}'.format(four=four,three=three,jar=file_name)
except:
return None
def firefox():
print("Starting Selenium...")
try:
from selenium.webdriver import Firefox
except:
print("Applying updates requires the `selenium` package")
os.exit(0)
return Firefox()
COMMAND_MAP = {
'install': install,
'apply_updates': apply_updates,
'check_updates': check_updates,
}
if __name__ == "__main__":
args = parser.parse_args()
if not args.pack_location:
# initialize from config
with open("pack-location.txt", "r") as f:
args.pack_location = f.read().strip()
if not os.path.exists(args.version_file):
print("Error: version file\"" + args.version_file + "\" does not exist.")
parser.print_help()
sys.exit(1)
if not os.path.exists(args.pack_location):
print("Error: mod folder \"" + args.pack_location + "\" does not exist.")
parser.print_help()
sys.exit(1)
elif not os.path.isdir(args.pack_location):
print("Error: mod folder \"" + args.pack_location + "\" is not actually a folder.")
parser.print_help()
sys.exit(1)
if not os.path.exists(args.whitelist_file):
print("Error: whitelist file \"" + args.whitelist_file + "\" does not exist.")
sys.exit(1)
if not (args.command in COMMAND_MAP):
print("Error: command \"" + args.command + "\" does not exist")
parser.print_help()
sys.exit(1)
# fetch version
with open(args.version_file) as f:
for line in f:
if line.strip().split()[0] == "#VERSION":
VERSION = int(line.strip().split()[1])
break
# run the command
COMMAND_MAP[args.command](args)