aboutsummaryrefslogblamecommitdiff
path: root/update.py
blob: a7d3ab213df70601422c8ab388848b018fdf30f2 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
                      

               
         
          

              
         

               
 








                                                                                                                        
 

































                                                                                                                  
 
                                      


                                                               
                            
                                       
                                    


                                                                 
         
                    
                                                           
              


                                                                          
             
                                                                         
                                                                    
                                                            
                                                    
                                                                
                                    
 


                                              
                                                                   

                                                            
 

                                      
 
 




                                                                                
                   
                                           
                                                                                  
                                                      
                        
                                                         



                                              
                           
                                                                     

                                    

                                                                                      
               





                                                                                            
                                   



                                                                    
                                      
                   
 
                                

                   
                      

                                                                            



                                          
                           

                                
                                                         
                            
               



                                                                                                         
 


                                                             







                                                                                                                                        



                                
                                                                                                                                                                  













                                                                                                                  




                                   
 






                                                 
 




























                                                                                           
#!/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)]

    i = 0
    for mod in mods:
        mod_path = os.path.join(args.pack_location, mod[0])
        i += 1
        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))
            print(' ({i} of {x})'.format(i=i,x=len(mods)), end='\r')
            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!" + " " * 8)

    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')
        ffx.get(url + '/files')
        #page_src = ffx.page_source
        #dl = re.search('Elerium.PublicProjectDownload.countdown\(".*?"\);', page_src)
        #if not dl:
        #    return None
        dl = ffx.find_element_by_xpath("html/body/div/main/div/div/section/div/div/div/section/article/div/div/a").get_attribute("href")
        dl = re.search('\d{7}', dl)
        dl = dl.group(0)
        four = str(int(dl[:4]))
        three = str(int(dl[4:]))

        file_name = ffx.find_elements_by_xpath("html/body/div/main/div/div/section/div/div/div/section/article/div/div/span[contains(@class, 'text-sm')]")[1].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)